* chore(#3560): delete two unreachable workflows, gate reachability in lint discovery-phase.md and plan-milestone-gaps.md shipped to all 19 runtime install trees with no command, agent, or skill referencing them. plan-milestone-gaps' command was deleted by #2790 and the workflow was left behind; discovery-phase's own header claimed a caller in plan-phase.md's mandatory_discovery step, and that step does not exist — plan-phase.md contains zero occurrences of "discovery". docs/INVENTORY.md asserted discovery-phase.md was an alternate entry for /gsd-new-project. new-project.md never referenced it. The row and the matching note sentence are removed across all five locales rather than corrected. Adds rule 6 to lint-command-contract: every shipped workflow must be reachable from a loader, walking the transitive closure over the three reference shapes this repo uses. The closure seeds ONLY from commands/agents/skills, so a workflow that references only itself and a pair that reference only each other are both correctly reported rather than satisfying themselves; a visited set makes reference cycles terminate. The measure is a mention in a LOADER — docs/ and install-tree fixtures deliberately do not count, because scan.md proved a file can be documented and shipped while entirely unreached. Ships blocking, not report-only: #3561 is in this branch's base, so the tree reports 0 unreachable from the start. Closes #3560 * test(#3560): drive rule 6 end-to-end, sweep a stale allowlist, update ADR-0002 Review findings. Rule 6 had no end-to-end coverage: the tests exercised the pure closure with in-memory data, so the wiring — file collection, exit code, diagnostic — was unproven, and #3560's acceptance list explicitly wants a fixture showing the rule FAILS on a planted orphan. Adds an optional --root to lint-command-contract (default behavior unchanged) and four tests driving the real CLI through the process seam against a temp fixture: clean=0, planted orphan=1, orphan referenced only from docs/=1, orphan reachable transitively=0. The docs/ case is what pins the Goodhart defense — a mention outside a loader must not confer reachability. Deletes two tests that were byte-identical to a third and could not assert anything loader-specific, since the closure is source-agnostic by design; that distinction lives in the lint script's file collection and is now covered above. Removes a stale ALLOWLIST entry for discovery-phase.md in planner-language-regression — the exact sweep-miss class rule 6 exists to catch, found in the PR that adds the rule. ADR-0002 described five per-file frontmatter checks; rule 6 is a repo-level reachability graph, so the Decision section now says so. Refs #3560 * test(#3560): cut the bug-3298 test pin on the deleted plan-milestone-gaps workflow The remote runner went red with four failures: tests/phase.test.cjs asserted the plan-milestone-gaps workflow exists and checked its mkdir patterns, so deleting the file broke the test that pinned it. This is the fence the epic describes — the content-sync test IS what keeps an unreachable file alive — and cutting the coupling is what makes the deletion safe. Removes only that arm. The bug-3298 block guards three workflows against phase-dir prefix drift; the import and add-backlog arms and both shared mkdir-pattern helpers are untouched. Worth recording where the sweep failed: my reachability walk covered commands, agents, skills, gsd-core and docs, and lint-removed-but-needed covers .github/workflows, gsd-core, docs and package.json. Neither looks at tests/, so a test-pinned deletion is invisible to both and surfaces only on the remote runner. The how-to added by this PR names that gap explicitly so the next deletion searches tests/ by hand. Refs #3560 * docs(#3560): add a how-to for resolving unreachable-workflow findings * chore(#3560): backfill changeset pr number to 3564 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -124,4 +124,58 @@ function workflowPathRefs(content) {
|
||||
return refs;
|
||||
}
|
||||
|
||||
module.exports = { CANONICAL_TOOLS, parseFrontmatter, executionContextRefs, workflowPathRefs };
|
||||
/**
|
||||
* unreachableWorkflows(loaderContents, gsdFiles, workflowPaths)
|
||||
*
|
||||
* Computes reachability over the gsd-core file graph and reports which
|
||||
* `workflowPaths` are never reached, starting only from `loaderContents`
|
||||
* (commands/agents/skills — the files a runtime actually loads) and walking
|
||||
* `workflowPathRefs` edges transitively through `gsdFiles`.
|
||||
*
|
||||
* The seed set is deliberately restricted to loaders and never includes a
|
||||
* workflow's own content. Seeding from workflows too would let two failure
|
||||
* modes hide: a workflow that only references itself would satisfy its own
|
||||
* reachability, and a pair of workflows that reference only each other would
|
||||
* form an island that looks connected from the inside but that no command,
|
||||
* agent, or skill ever actually opens. Both are orphans in every sense that
|
||||
* matters — nothing external can reach them — and both must be reported.
|
||||
* Requiring every path to originate at a loader is what makes "reachable"
|
||||
* mean "a runtime can actually get here," not merely "something points to
|
||||
* it."
|
||||
*
|
||||
* `gsdFiles` covers all of `gsd-core/**`, not just `workflows/`, because a
|
||||
* `references/` or `templates/` file can itself name a workflow path and
|
||||
* needs to be walked through to propagate reachability — restricting the map
|
||||
* to `workflows/` would silently break any chain that passes through a
|
||||
* non-workflow file.
|
||||
*
|
||||
* `visited` guards the walk against reference cycles (including the
|
||||
* mutual/self cases above) so traversal always terminates.
|
||||
*/
|
||||
function unreachableWorkflows(loaderContents, gsdFiles, workflowPaths) {
|
||||
const visited = new Set();
|
||||
const queue = [];
|
||||
|
||||
for (const content of loaderContents) {
|
||||
for (const ref of workflowPathRefs(content)) queue.push(ref);
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const p = queue.pop();
|
||||
if (visited.has(p)) continue;
|
||||
visited.add(p);
|
||||
if (gsdFiles.has(p)) {
|
||||
for (const ref of workflowPathRefs(gsdFiles.get(p))) queue.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
return workflowPaths.filter(p => !visited.has(p));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CANONICAL_TOOLS,
|
||||
parseFrontmatter,
|
||||
executionContextRefs,
|
||||
workflowPathRefs,
|
||||
unreachableWorkflows,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user