Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-4946-planner-key-links-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
type: Fixed
pr: 5149
---
<!-- docs-exempt: prompt guidance, reference table alignment, and schema doc table; no public CLI change -->
**Planners emitted symbols or endpoints in from:/to:, failing `verify key-links`** — `agents/gsd-planner.md` and `gsd-core/references/planner-guidance.md` now explicitly specify that `from:` and `to:` must be project-relative file paths only, with symbols and endpoints placed in `via:` (#4946).
5 changes: 4 additions & 1 deletion agents/gsd-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ estimate: # Projected execution cost (see Estimate Emission)
must_haves:
truths: [] # Observable behaviors
artifacts: [] # Files that must exist
key_links: [] # Critical connections
key_links: [] # Critical connections (from/to MUST be project-relative file paths; symbols/endpoints go in via:)
---

<objective>
Expand Down Expand Up @@ -480,6 +480,9 @@ For each artifact: "What must be CONNECTED for this to function?"

**Step 5: Identify Key Links**
"Where is this most likely to break?" Key links = critical connections where breakage causes cascading failures.
- `from` and `to` MUST be project-relative file paths only. Never put symbols, functions, or endpoints in `from:` or `to:`.
- Symbols, functions, endpoints, and narrative details MUST be placed in `via:` (e.g. `via: "fetch in useEffect — calls /api/feed"`).
- Verification requires `to` to appear as text in the `from` file, or a specific `pattern` regex to match either file. Set `pattern` unless you know `from` will contain the `to` path string.

See @~/.claude/gsd-core/references/planner-guidance.md for a worked example and the `must_haves` YAML format.

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/plan-md.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ A phase with 13 plans, two of them `superseded`, then reads `11/11 → complete`
| `key_links[].from` | string | Source file (relative path from project root). Must be a literal file path — describe components or symbols in `via:`. |
| `key_links[].to` | string | Target file (relative path from project root). Must be a literal file path — describe endpoints, modules, or APIs in `via:`. |
| `key_links[].via` | string | Description of how they connect, including any endpoint, component, or symbol name (e.g. `fetch in useEffect — calls /api/feed`, `Prisma query via prisma.message`, `import`). |
| `key_links[].pattern` | string (optional) | Regex to verify the connection exists in source. |
| `key_links[].pattern` | string (optional) | Regex to verify the connection exists in source. When omitted, verification requires the `to` path string to appear in `from`. |

---

Expand Down
12 changes: 12 additions & 0 deletions gsd-core/references/planner-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@ Message list component wiring:

"Where is this most likely to break?" Key links = critical connections where breakage causes cascading failures.

### Key Links Field Contract

| Field | Description |
|---|---|
| `key_links` | Critical connections between artifacts. |
| `key_links[].from` | Source file (relative path from project root). Describe components or symbols in `via:`. |
| `key_links[].to` | Target file (relative path from project root). Describe endpoints, APIs, or modules in `via:`. |
| `key_links[].via` | How they connect, including any endpoint or symbol name (e.g. `fetch in useEffect — calls /api/chat`, `Prisma query via prisma.message`). |
| `key_links[].pattern` | Optional. Regex to verify connection exists. When omitted, verification requires the `to` path string to appear in `from`. |

> **Important:** `from:` and `to:` MUST be literal, project-relative file paths. Never place functions, symbols (`file.ts::symbol`), HTTP endpoints (`/api/...`), or prose descriptions in `from:` or `to:`. All symbols, methods, and endpoints belong in `via:`. When `pattern` is omitted, `verify key-links` checks that the `to` path string appears in the `from` file; set `pattern` unless you know `from` will contain the `to` path string (for example when `from` refers to `to` indirectly or via an import that does not include the full relative path). See gsd-core/templates/phase-prompt.md for the complete `must_haves` schema.

### Must-Haves Output Format

```yaml
Expand Down
2 changes: 1 addition & 1 deletion gsd-core/templates/phase-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ must_haves:
| `key_links[].from` | Source file (relative path from project root). Describe components or symbols in `via:`. |
| `key_links[].to` | Target file (relative path from project root). Describe endpoints, APIs, or modules in `via:`. |
| `key_links[].via` | How they connect, including any endpoint or symbol name (e.g. `fetch in useEffect — calls /api/chat`, `Prisma query via prisma.message`). |
| `key_links[].pattern` | Optional. Regex to verify connection exists. |
| `key_links[].pattern` | Optional. Regex to verify connection exists. When omitted, verification requires the `to` path string to appear in `from`. |

**Why this matters:**

Expand Down
5 changes: 4 additions & 1 deletion pi/package-agents/gsd-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ estimate: # Projected execution cost (see Estimate Emission)
must_haves:
truths: [] # Observable behaviors
artifacts: [] # Files that must exist
key_links: [] # Critical connections
key_links: [] # Critical connections (from/to MUST be project-relative file paths; symbols/endpoints go in via:)
---

<objective>
Expand Down Expand Up @@ -475,6 +475,9 @@ For each artifact: "What must be CONNECTED for this to function?"

**Step 5: Identify Key Links**
"Where is this most likely to break?" Key links = critical connections where breakage causes cascading failures.
- `from` and `to` MUST be project-relative file paths only. Never put symbols, functions, or endpoints in `from:` or `to:`.
- Symbols, functions, endpoints, and narrative details MUST be placed in `via:` (e.g. `via: "fetch in useEffect — calls /api/feed"`).
- Verification requires `to` to appear as text in the `from` file, or a specific `pattern` regex to match either file. Set `pattern` unless you know `from` will contain the `to` path string.

See @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-guidance.md for a worked example and the `must_haves` YAML format.

Expand Down
2 changes: 1 addition & 1 deletion pi/package-agents/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"gsd-pattern-mapper.md": "3f51a38c2a1e8657190ba88a314fb2c9e66e25f5ab0e3edbc4e2f649b2702d01",
"gsd-phase-researcher.md": "2215ab8205f1bcb8b826870a4b8e9a949b6f77778d1bf75fae5ce4a07dd459ba",
"gsd-plan-checker.md": "1734489d067538c0fb818768aae8f336d83d4d1b2e81286eff84b6c512c67cfb",
"gsd-planner.md": "709f81898bf60f17eea9a150098c5ba2e31fddb38ea062fa0b015136502045b4",
"gsd-planner.md": "ee010efc3cc043eefd982040364b5bfbd51f703cd0c0a5da1aad826bbfb28ec3",
"gsd-project-researcher.md": "ac7e0065778a3ec40e023af48faa57f715c3fa13900975ef7b8d50142e44cf3c",
"gsd-research-synthesizer.md": "d20735f8dc591af470c4cbb892284863c5976e2bd8a77512bf146469158d051a",
"gsd-roadmapper.md": "c5cf5fa9df6bcbd7acdc1ec62c20b6e713ed6b6f2b032b17ca3044eebd0626d3",
Expand Down
212 changes: 212 additions & 0 deletions tests/verify.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2707,6 +2707,218 @@ describe('bug-967 verify key-links strict file-path contract', () => {
});
}

// ── 7. Doc-contract guard #4946: gsd-planner.md, planner-guidance.md & parity across surfaces ──
// Markdown prompt files are runtime contracts executed by LLM agents.
// No per-line allow-test-rule markers needed since eslint-rules/no-source-grep.cjs inspects only .cjs/.cts/.js/.mjs/.mts/.ts source files, not markdown reads.
describe('bug #4946: planner key_links schema contract and cross-surface parity', () => {
function verifyPlannerContractContent(label, content) {
assert.match(
content,
/`from`.*`to`.*relative file path/i,
`${label} must state that key_links from/to must be project-relative file paths only`,
);
assert.match(
content,
/never (?:put|place).*(?:symbols?|endpoints?).*(?:in `from:`|in `to:`)/i,
`${label} must instruct never to put symbols or endpoints in from/to`,
);
assert.match(
content,
/(?:symbols?|endpoints?).*(?:placed in `via:`|belong in `via:`|in `via:`)/i,
`${label} must instruct that symbols, endpoints, and details belong in via:`,
);
assert.match(
content,
/`to`.*appear.*`from`/i,
`${label} must document that verification requires to to appear in from`,
);
assert.match(
content,
/set `pattern` unless you know `from` will contain the `to` path string/i,
`${label} must document default rule to set pattern unless from contains to`,
);
assert.match(
content,
/key_links:\s*\[\]\s*#.*\(from\/to MUST be project-relative file paths; symbols\/endpoints go in via:\)/,
`${label} must carry the key_links contract comment on its frontmatter stub`,
);
}

test('#4946: agents/gsd-planner.md states that key_links from/to must be relative file paths only and documents pattern rule', () => {
const plannerPath = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
assert.ok(fs.existsSync(plannerPath), `gsd-planner.md not found at ${plannerPath}`);
const content = fs.readFileSync(plannerPath, 'utf-8');
verifyPlannerContractContent('agents/gsd-planner.md', content);
});

test('#4946: pi/package-agents/gsd-planner.md mirrors the key_links schema contract', () => {
const piPlannerPath = path.join(__dirname, '..', 'pi', 'package-agents', 'gsd-planner.md');
assert.ok(fs.existsSync(piPlannerPath), `pi gsd-planner.md not found at ${piPlannerPath}`);
const content = fs.readFileSync(piPlannerPath, 'utf-8');
verifyPlannerContractContent('pi/package-agents/gsd-planner.md', content);
});

test('#4946: gsd-core/references/planner-guidance.md carries key_links field schema contract table and pattern rule', () => {
const guidancePath = path.join(__dirname, '..', 'gsd-core', 'references', 'planner-guidance.md');
assert.ok(fs.existsSync(guidancePath), `planner-guidance.md not found at ${guidancePath}`);
const content = fs.readFileSync(guidancePath, 'utf-8');

assert.ok(
content.includes('`key_links[].from`') && content.includes('`key_links[].to`'),
'planner-guidance.md must include key_links[].from and key_links[].to in its schema table',
);
assert.match(
content,
/`from:` and `to:` MUST be literal, project-relative file paths/i,
'planner-guidance.md must specify that from and to must be literal project-relative file paths',
);
assert.match(
content,
/Never place functions, symbols.*HTTP endpoints.*in `from:` or `to:`/i,
'planner-guidance.md must instruct never to place symbols or endpoints in from/to',
);
assert.match(
content,
/All symbols, methods, and endpoints belong in `via:`/i,
'planner-guidance.md must explicitly instruct that symbols and endpoints belong in via:',
);
assert.match(
content,
/When `pattern` is omitted, `verify key-links` checks that the `to` path string appears in the `from` file/i,
'planner-guidance.md must document that verify key-links requires to in from when pattern is omitted',
);
assert.match(
content,
/set `pattern` unless you know `from` will contain the `to` path string/i,
'planner-guidance.md must document the default to set pattern unless from contains to',
);
assert.match(
content,
/See gsd-core\/templates\/phase-prompt\.md for the complete `must_haves` schema\./,
'planner-guidance.md must point to shipped phase-prompt.md template rather than docs/ path',
);
});

test('#4946: parity: planner-guidance.md, phase-prompt.md, and plan-md.md agree on key_links contract', () => {
const tableSurfaces = [
['planner-guidance.md', path.join(__dirname, '..', 'gsd-core', 'references', 'planner-guidance.md')],
['phase-prompt.md', path.join(__dirname, '..', 'gsd-core', 'templates', 'phase-prompt.md')],
['plan-md.md', path.join(__dirname, '..', 'docs', 'reference', 'plan-md.md')],
];

function checkTableSurface(name, content) {
const lines = content.split(/\r?\n/);

const fromRow = lines.find((l) => /^\|\s*`key_links\[\]\.from`\s*\|/.test(l));
assert.ok(fromRow, `${name} must include a field table row for key_links[].from`);
assert.match(
fromRow,
/^\|\s*`key_links\[\]\.from`\s*\|.*relative path from project root/i,
`${name} key_links[].from row must require relative path from project root`,
);
assert.match(
fromRow,
/via:/i,
`${name} key_links[].from row must specify that components/symbols belong in via:`,
);

const toRow = lines.find((l) => /^\|\s*`key_links\[\]\.to`\s*\|/.test(l));
assert.ok(toRow, `${name} must include a field table row for key_links[].to`);
assert.match(
toRow,
/^\|\s*`key_links\[\]\.to`\s*\|.*relative path from project root/i,
`${name} key_links[].to row must require relative path from project root`,
);
assert.match(
toRow,
/via:/i,
`${name} key_links[].to row must specify that endpoints/modules belong in via:`,
);

const viaRow = lines.find((l) => /^\|\s*`key_links\[\]\.via`\s*\|/.test(l));
assert.ok(viaRow, `${name} must include a field table row for key_links[].via`);
assert.match(
viaRow,
/endpoint/i,
`${name} key_links[].via row must document endpoint names belong in via`,
);
assert.match(
viaRow,
/symbol/i,
`${name} key_links[].via row must document symbol names belong in via`,
);

const patternRow = lines.find((l) => /^\|\s*`key_links\[\]\.pattern`\s*\|/.test(l));
assert.ok(patternRow, `${name} must include a field table row for key_links[].pattern`);
assert.match(
patternRow,
/regex/i,
`${name} key_links[].pattern row must document regex verification`,
);
assert.match(
patternRow,
/when omitted.*`to`.*`from`/i,
`${name} key_links[].pattern row must state the fallback rule that to must appear in from when omitted`,
);
}

for (const [name, filePath] of tableSurfaces) {
assert.ok(fs.existsSync(filePath), `${name} must exist at ${filePath}`);
const content = fs.readFileSync(filePath, 'utf-8');
checkTableSurface(name, content);
}

// Negative controls: run checkTableSurface over mutated copies across all surfaces and rows
for (const [name, filePath] of tableSurfaces) {
const surfaceContent = fs.readFileSync(filePath, 'utf-8');

// 1. Mutate from-row (remove relative path)
const mutatedFrom = surfaceContent.replace(
/(\|\s*`key_links\[\]\.from`\s*\|.*)relative path from project root/i,
'$1arbitrary path',
);
assert.throws(
() => checkTableSurface(`${name}-mutated-from`, mutatedFrom),
(err) => err instanceof assert.AssertionError,
`Negative control: ${name} without relative path in from must fail checkTableSurface`,
);

// 2. Mutate to-row (remove relative path)
const mutatedTo = surfaceContent.replace(
/(\|\s*`key_links\[\]\.to`\s*\|.*)relative path from project root/i,
'$1arbitrary path',
);
assert.throws(
() => checkTableSurface(`${name}-mutated-to`, mutatedTo),
(err) => err instanceof assert.AssertionError,
`Negative control: ${name} without relative path in to must fail checkTableSurface`,
);

// 3. Mutate via-row (remove endpoint/symbol reference)
const mutatedVia = surfaceContent.replace(
/(\|\s*`key_links\[\]\.via`\s*\|.*)(endpoint|symbol)/gi,
'$1other',
);
assert.throws(
() => checkTableSurface(`${name}-mutated-via`, mutatedVia),
(err) => err instanceof assert.AssertionError,
`Negative control: ${name} without endpoint/symbol in via must fail checkTableSurface`,
);

// 4. Mutate pattern-row (remove fallback rule)
const mutatedPattern = surfaceContent.replace(
/When omitted, verification requires the `to` path string to appear in `from`\./,
'Optional parameter.',
);
assert.throws(
() => checkTableSurface(`${name}-mutated-pattern`, mutatedPattern),
(err) => err instanceof assert.AssertionError,
`Negative control: ${name} without pattern fallback rule must fail checkTableSurface`,
);
}
});
});

// ────────────────────────────────────────────────────────────────────────
// Folded from tests/enh-2446-milestones-drift.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
Expand Down
Loading