From ffd7ebc09de2c572b9c8c0d67bea8f2f492786bb Mon Sep 17 00:00:00 2001 From: reference-week Date: Sat, 5 Sep 2026 19:06:19 +0200 Subject: [PATCH] fix(telemetry): a backlog declaration the report can actually read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `by_backlog` axis rests on one file per task folder, `backlog-link.json`, and nothing checked that the file was readable. Of the three this repository held, two carried `writtenAt` and `writtenBy` while `task-backlog-adapter.ts` reads `written_at` and `written_by`, so those two parsed as nothing at all. Measured on the real sink before the fix, 30 days: {"backlog":"ai-driven-dev/framework#694"} 4 {"declaration":"unreadable"} 130 {"declaration":"none"} 2918 and after it, the same window and the same records: {"backlog":"ai-driven-dev/framework#746"} 133 {"backlog":"ai-driven-dev/framework#694"} 4 {"declaration":"none"} 2918 The axis was never broken — it reported `unreadable` truthfully, on its own row, exactly as `TaskBacklogDeclaration` requires of a file it cannot parse. What was missing is anything that reads that row before a person does. Both wrong files were written by `aidd-orchestrator:01-sdlc`, which `01-frame.md` tells to carry the resolved ticket so that "whichever of Spec or Plan first creates the delivery folder can declare it there". It wrote the file itself instead, and took the field names from the TypeScript interface rather than from what either skill teaches. The guard, not a fourth copy of the JSON block, is the durable answer: a third skill inventing a fourth spelling now fails here rather than in a report nobody reads. The guard restates the reader's rule, because the reader is a `cli/` module and this is a repository script test; the second case is what keeps that restatement honest, asserting the same three names are the ones `aidd-pm:04-spec` and `aidd-dev:01-plan` actually teach. Mutations run, both killed: a declaration put back in camelCase, and a skill that stops teaching one of the three fields. 373 repository script tests pass, 0 broken links in 798 files. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp --- .../backlog-link.json | 4 +- .../backlog-link.json | 4 +- ...a-backlog-link-the-reader-can-read.test.js | 80 +++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 scripts/__tests__/a-backlog-link-the-reader-can-read.test.js diff --git a/aidd_docs/tasks/2026_09/2026_09_02_install-surfaces-agree/backlog-link.json b/aidd_docs/tasks/2026_09/2026_09_02_install-surfaces-agree/backlog-link.json index 4c491a50c..c8075e675 100644 --- a/aidd_docs/tasks/2026_09/2026_09_02_install-surfaces-agree/backlog-link.json +++ b/aidd_docs/tasks/2026_09/2026_09_02_install-surfaces-agree/backlog-link.json @@ -1,5 +1,5 @@ { "backlog": "ai-driven-dev/framework#703", - "writtenAt": "2026-09-02T20:15:00Z", - "writtenBy": "aidd-orchestrator:01-sdlc" + "written_at": "2026-09-02T20:15:00Z", + "written_by": "aidd-orchestrator:01-sdlc" } diff --git a/aidd_docs/tasks/2026_09/2026_09_03_trailer-survives-a-regenerated-hook/backlog-link.json b/aidd_docs/tasks/2026_09/2026_09_03_trailer-survives-a-regenerated-hook/backlog-link.json index 0402ee9a2..3f08a3d5d 100644 --- a/aidd_docs/tasks/2026_09/2026_09_03_trailer-survives-a-regenerated-hook/backlog-link.json +++ b/aidd_docs/tasks/2026_09/2026_09_03_trailer-survives-a-regenerated-hook/backlog-link.json @@ -1,5 +1,5 @@ { "backlog": "ai-driven-dev/framework#746", - "writtenAt": "2026-09-03T06:40:00Z", - "writtenBy": "aidd-orchestrator:01-sdlc" + "written_at": "2026-09-03T06:40:00Z", + "written_by": "aidd-orchestrator:01-sdlc" } diff --git a/scripts/__tests__/a-backlog-link-the-reader-can-read.test.js b/scripts/__tests__/a-backlog-link-the-reader-can-read.test.js new file mode 100644 index 000000000..aa18649b8 --- /dev/null +++ b/scripts/__tests__/a-backlog-link-the-reader-can-read.test.js @@ -0,0 +1,80 @@ +const assert = require("node:assert/strict"); +const cp = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); +const { describe, it } = require("node:test"); + +const ROOT = path.resolve(__dirname, "../.."); + +/** + * A task folder declares the backlog item it delivers in `backlog-link.json`, and the whole + * `by_backlog` axis rests on that one file being readable. + * + * Nothing checked that it was. Of the three declarations this repository held, two carried + * `writtenAt` and `writtenBy` while `task-backlog-adapter.ts` reads `written_at` and + * `written_by`, so the report answered `declaration: unreadable` for 130 records and named + * the item for 4. Both were written by `aidd-orchestrator:01-sdlc`, which is told to let + * Spec or Plan declare the item and instead wrote the file itself, taking the field names + * from the TypeScript interface rather than from what either skill teaches. + * + * The reader is a `cli/` module and this is a repository script test, so the rule is + * restated here rather than imported across that boundary — and the second case below is + * what keeps the restatement honest. + */ +const REQUIRED_FIELDS = ["backlog", "written_at", "written_by"]; + +function trackedBacklogLinks() { + return cp + .execSync("git ls-files '*backlog-link.json'", { cwd: ROOT, encoding: "utf8" }) + .trim() + .split(/\r?\n/) + .filter(Boolean); +} + +describe("every backlog declaration in this repository is one the report can read", () => { + it("names the fields the reader looks for, in the spelling it looks for them", () => { + const unreadable = []; + + for (const file of trackedBacklogLinks()) { + let parsed; + try { + parsed = JSON.parse(fs.readFileSync(path.join(ROOT, file), "utf8")); + } catch (error) { + unreadable.push(`${file} is not JSON: ${error.message}`); + continue; + } + const missing = REQUIRED_FIELDS.filter( + (field) => typeof parsed[field] !== "string" || parsed[field] === "" + ); + if (missing.length > 0) { + unreadable.push(`${file} is missing ${missing.join(", ")} (has ${Object.keys(parsed).join(", ")})`); + } + } + + assert.deepEqual( + unreadable, + [], + `A task folder declares a backlog item the report cannot read, so its work counts as ` + + `\`unreadable\` and the item is never named.\n${unreadable.join("\n")}` + ); + }); + + /** The two skills that teach the file are the only things that decide what gets written, + * so they have to teach the same three names — and the same three this test asks for. + * Read from the skills rather than trusted: a taught shape drifting away from the reader + * is exactly what produced the two unreadable files, and a guard restating the fields + * without checking the lesson would have stayed green through it. */ + it("is the shape both skills that write it actually teach", () => { + const TEACHING_ACTIONS = [ + "plugins/aidd-pm/skills/04-spec/actions/01-build.md", + "plugins/aidd-dev/skills/01-plan/actions/04-plan.md", + ]; + + for (const action of TEACHING_ACTIONS) { + const text = fs.readFileSync(path.join(ROOT, action), "utf8"); + for (const field of REQUIRED_FIELDS) { + assert.ok(text.includes(`"${field}"`), `${action} must teach the field "${field}"`); + } + } + }); +});