From 8b4123cccc33120e080464fd17a9db6fa90d4657 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 12:39:16 +0200 Subject: [PATCH] fix(self-feedback): JSONL header is JSON-valid meta marker, not # comment Phase 2 (216b1d43f) wrote "# generated from .sf/sf.db ..." as line 1 of .sf/self-feedback.jsonl. readJsonl tolerated it via try/catch around JSON.parse, but the doctor's stricter JSONL syntax check flagged it as "invalid jsonl syntax: line 1: Unexpected token '#'". Replace the # comment with a JSON-valid meta marker: {"_meta":"generated from .sf/sf.db","_warning":"do not edit directly; use the resolve_issue tool or sf headless triage --apply"} readJsonl now skips entries carrying `_meta` so downstream consumers don't see the marker as a self-feedback record. Tests updated to match the new marker shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/resources/extensions/sf/self-feedback.js | 17 +++++-- .../sf/tests/self-feedback-db.test.mjs | 44 +++++++++++-------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/resources/extensions/sf/self-feedback.js b/src/resources/extensions/sf/self-feedback.js index 0ea323500..7cfe3cbec 100644 --- a/src/resources/extensions/sf/self-feedback.js +++ b/src/resources/extensions/sf/self-feedback.js @@ -56,8 +56,14 @@ const SELF_FEEDBACK_HEADER = "durable source of truth is `.sf/sf.db`.\n\n" + "Blocking entries (severity high+) remain active until an sf fix explicitly\n" + "marks them resolved with evidence.\n\n"; -const JSONL_GENERATED_COMMENT = - "# generated from .sf/sf.db — do not edit directly; use the resolve_issue tool or sf headless triage --apply"; +// JSONL marker is a valid JSON object so strict JSONL parsers (e.g. the +// doctor's syntax check) accept line 1. The _meta sentinel keeps the +// "do not hand-edit" intent visible to anyone opening the file. +const JSONL_GENERATED_COMMENT = JSON.stringify({ + _meta: "generated from .sf/sf.db", + _warning: + "do not edit directly; use the resolve_issue tool or sf headless triage --apply", +}); const RECENT_RESOLVED_MARKDOWN_LIMIT = 20; const MARKDOWN_DETAIL_CHAR_LIMIT = 2_000; const SELF_FEEDBACK_SCHEMA_VERSION = 1; @@ -225,7 +231,12 @@ function readJsonl(path) { for (const line of readFileSync(path, "utf-8").split("\n")) { if (!line.trim()) continue; try { - out.push(JSON.parse(line)); + const parsed = JSON.parse(line); + // Skip meta header lines emitted by regenerateSelfFeedbackJsonl + // — those are generator metadata, not self-feedback entries. + // Entries always carry an `id` string; meta lines have `_meta`. + if (parsed && typeof parsed === "object" && parsed._meta) continue; + out.push(parsed); } catch { /* skip malformed lines */ } diff --git a/src/resources/extensions/sf/tests/self-feedback-db.test.mjs b/src/resources/extensions/sf/tests/self-feedback-db.test.mjs index 198f91352..e1596b48f 100644 --- a/src/resources/extensions/sf/tests/self-feedback-db.test.mjs +++ b/src/resources/extensions/sf/tests/self-feedback-db.test.mjs @@ -780,8 +780,10 @@ test("markResolved_regenerates_jsonl_from_db_with_resolved_state", () => { ) .split("\n") .filter((l) => l.trim()); - // First non-empty line is the generated comment (starts with #) - assert.ok(lines[0].startsWith("#"), "expected generated comment header"); + // First non-empty line is the generated meta marker (valid JSON object so + // strict JSONL parsers like the doctor accept it; carries _meta sentinel). + const meta = JSON.parse(lines[0]); + assert.ok(meta._meta, "expected JSONL meta header on line 1"); // Second line is the resolved entry const entry = JSON.parse(lines[1]); assert.equal(entry.id, result.entry.id); @@ -982,16 +984,16 @@ test("markResolved_db_only_entry_populates_jsonl_and_markdown_after_resolution", join(project, ".sf", "self-feedback.jsonl"), "utf-8", ); - // Comment header must be present + // Generated meta marker must be present on line 1 (JSON object with _meta key) assert.ok( - jsonlContent.includes("# generated from .sf/sf.db"), - "JSONL must have generated comment header", + jsonlContent.includes('"_meta":"generated from .sf/sf.db"'), + "JSONL must have generated meta header", ); - // Entry must appear as resolved + // Entry must appear as resolved — filter out the _meta header line const jsonlLines = jsonlContent.split("\n").filter((l) => { try { - JSON.parse(l); - return true; + const parsed = JSON.parse(l); + return parsed && typeof parsed === "object" && !parsed._meta; } catch { return false; } @@ -1045,9 +1047,14 @@ test("backfillSelfFeedbackJsonl_makes_jsonl_equal_to_db_render", () => { join(project, ".sf", "self-feedback.jsonl"), "utf-8", ); - const firstEntryLines = firstContent - .split("\n") - .filter((l) => { try { JSON.parse(l); return true; } catch { return false; } }); + const firstEntryLines = firstContent.split("\n").filter((l) => { + try { + const parsed = JSON.parse(l); + return parsed && typeof parsed === "object" && !parsed._meta; + } catch { + return false; + } + }); assert.equal(firstEntryLines.length, 2, "JSONL must have both DB entries"); // Second backfill is idempotent — exact same content @@ -1064,8 +1071,9 @@ test("backfillSelfFeedbackJsonl_makes_jsonl_equal_to_db_render", () => { }); test("regenerateSelfFeedbackJsonl_includes_generated_comment_header", () => { - // The JSONL file must start with a # comment line marking it as generated. - // readJsonl already skips non-JSON lines (the comment) gracefully. + // The JSONL file must start with a JSON-valid meta marker (so strict + // JSONL parsers like the doctor accept the file). readJsonl skips entries + // carrying the _meta sentinel so consumers don't see them as records. const project = makeForgeProject(); recordSelfFeedback( { kind: "gap:header-test", severity: "low", summary: "header check" }, @@ -1080,13 +1088,11 @@ test("regenerateSelfFeedbackJsonl_includes_generated_comment_header", () => { "utf-8", ); const firstLine = content.split("\n")[0]; + const meta = JSON.parse(firstLine); + assert.ok(meta._meta, "JSONL first line must be a JSON object with _meta key"); assert.ok( - firstLine.startsWith("#"), - "JSONL first line must be a # comment (generated header)", - ); - assert.ok( - firstLine.includes("generated from .sf/sf.db"), - "generated header must mention sf.db", + meta._meta.includes("generated from .sf/sf.db"), + "_meta must mention sf.db", ); assert.ok( firstLine.includes("resolve_issue"),