{"record":{"id":"66f5d28ee5bd47fb","repo":"pbakaus/impeccable","slug":"writesnapshot-requires-a-slug","errorCode":null,"errorMessage":"writeSnapshot requires a slug","messagePattern":"writeSnapshot requires a slug","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/skills/impeccable/scripts/critique-storage.mjs","lineNumber":61,"sourceCode":" */\n/**\n * Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.\n * Plain colons aren't allowed on Windows filesystems.\n */\nexport function nowFilenameStamp(date = new Date()) {\n  const iso = date.toISOString();           // 2026-05-12T18:30:00.123Z\n  return iso.replace(/[:.]/g, '-').replace(/-\\d+Z$/, 'Z');\n}\n\n/**\n * Write a snapshot for `slug`. `meta` carries the small structured frontmatter\n * keys read back by readTrend(). `body` is the human-readable critique\n * report (everything below the frontmatter).\n *\n * Returns the absolute path written.\n */\nexport function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {\n  if (!slug) throw new Error('writeSnapshot requires a slug');\n  const dir = getCritiqueDir(cwd);\n  fs.mkdirSync(dir, { recursive: true });\n  const timestamp = nowFilenameStamp(now);\n  const filePath = path.join(dir, `${timestamp}__${slug}.md`);\n  // Spread `meta` first so internally computed `timestamp` and `slug`\n  // always win. Otherwise a caller-supplied meta blob (parsed from the\n  // IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the\n  // filename in disagreement with its frontmatter and corrupting trends.\n  const front = serializeFrontmatter({ ...meta, timestamp, slug });\n  fs.writeFileSync(filePath, `${front}\\n${body.trim()}\\n`, 'utf-8');\n  return filePath;\n}\n\nfunction serializeFrontmatter(obj) {\n  const lines = ['---'];\n  for (const [key, value] of Object.entries(obj)) {\n    if (value === undefined || value === null) continue;\n    const str = typeof value === 'string' ? value : String(value);","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/pbakaus/impeccable/blob/d14711ae3d1a1dd62dee61a358d27f107c51ccd0/plugin/skills/impeccable/scripts/critique-storage.mjs#L43-L79","documentation":"writeSnapshot() persists a critique snapshot to disk as `${timestamp}__${slug}.md` and writes `slug` into the YAML-style frontmatter. The slug is load-bearing: it names the file and is read back by readTrend() to group snapshots over time. The guard throws synchronously when slug is falsy because an empty slug would produce a malformed filename (`<timestamp>__.md`) and a frontmatter `slug:` key with no value, corrupting trend aggregation.","triggerScenarios":"Calling writeSnapshot({ meta, body }) with the slug key omitted, set to null/undefined, or passed as an empty string. Also reached when slug is derived upstream from a field that is absent (e.g. a design key or page URL that was never resolved) and the caller forwards undefined without checking.","commonSituations":"Programmatic batch writes where some entries lack a slug; a caller that reads slug from an env var (IMPECCABLE_CRITIQUE_META) or CLI arg that was not supplied; refactors that rename the slug source field but forget the writeSnapshot call site.","solutions":["Pass a non-empty slug string, e.g. writeSnapshot({ slug: 'hero-redesign', meta, body }).","If slug is derived, resolve and validate it before calling writeSnapshot (fall back to a stable id or skip the write with a warning).","Confirm the caller is forwarding the correct field name from its source object."],"exampleFix":"// before\nwriteSnapshot({ meta, body }); // slug omitted -> throws\n\n// after\nwriteSnapshot({ slug: 'hero-redesign', meta, body });","handlingStrategy":"validation","validationCode":"function requireSlug(slug) {\n  if (typeof slug !== 'string' || slug.trim() === '') {\n    throw new Error('snapshot slug is required');\n  }\n  return slug;\n}\n// before calling writeSnapshot:\nwriteSnapshot({ slug: requireSlug(maybeSlug), meta, body });","typeGuard":"function isNonEmptyString(v) {\n  return typeof v === 'string' && v.trim().length > 0;\n}\n\nif (!isNonEmptyString(slug)) {\n  // skip or assign a stable fallback id\n}","tryCatchPattern":null,"preventionTips":["Treat slug as a required field in the calling code's own type/contract, not an optional one.","If slug is derived from another value, resolve and validate it at the boundary before forwarding.","In batch loops, filter or skip entries lacking a slug and log them rather than letting writeSnapshot throw."],"tags":["validation","critique","filesystem"],"backgroundTag":null,"analyzedSha":"d14711ae3d1a1dd62dee61a358d27f107c51ccd0","analyzedAt":"2026-08-13T00:52:25.771Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}