fix(self-feedback): JSONL header is JSON-valid meta marker, not # comment
Some checks are pending
CI / detect-changes (push) Waiting to run
CI / docs-check (push) Blocked by required conditions
CI / lint (push) Blocked by required conditions
CI / build (push) Blocked by required conditions
CI / integration-tests (push) Blocked by required conditions
CI / windows-portability (push) Blocked by required conditions
CI / rtk-portability (linux, blacksmith-4vcpu-ubuntu-2404) (push) Blocked by required conditions
CI / rtk-portability (macos, macos-15) (push) Blocked by required conditions
CI / rtk-portability (windows, blacksmith-4vcpu-windows-2025) (push) Blocked by required conditions

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) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-15 12:39:16 +02:00
parent 216b1d43f1
commit 8b4123cccc
2 changed files with 39 additions and 22 deletions

View file

@ -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 */
}

View file

@ -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"),