Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ _produces:
---
# Assembler
prose body.
`,
// A partial-rollout stub: 'clarify' is a REAL downstream phase whose file
// exists but carries NO frontmatter yet (mid phase-by-phase rollout). It is
// invisible to the typed model (bindSkill skips no-frontmatter files) but makes
// discover's `_advances_to: clarify` resolve on disk — so the dangling-edge
// check tolerates it (a real phase, not a typo).
'references/phases/clarify/clarify.md':
`# Clarify

prose-only phase (no frontmatter yet).
`,
};
}
Expand Down Expand Up @@ -171,13 +181,29 @@ _contributes:
});

it('does NOT fail an _advances_to that points at a phase without frontmatter (partial rollout)', () => {
// goodSkill includes a frontmatter-less clarify.md stub — a real phase mid-
// rollout. discover._advances_to: clarify must resolve (dir exists) and NOT be
// flagged as dangling, even though clarify has no typed frontmatter yet.
const findings = validateFixture(goodSkill());
assert.ok(
!findings.some((f) => /advances_to|clarify/.test(f.message)),
'should not fail on an unverifiable forward reference',
);
});

it('rejects an _advances_to that names a phase with no file on disk (dangling forward edge)', () => {
const files = goodSkill();
// Point discover at a phase that does not exist at all (a typo, not rollout).
files['references/phases/discover/discover.md'] = files[
'references/phases/discover/discover.md'
].replace('_advances_to: clarify', '_advances_to: clarify_TYPO');
const findings = validateFixture(files);
assert.match(
findings.map((f) => f.message).join('\n'),
/_advances_to 'clarify_TYPO' names neither a terminal.*nor an existing phase.*dangling forward edge/s,
);
});

// ---- backbone / checkpoint + chain-consistency ----

// A fully-declared 2-phase backbone (discover -> clarify -> complete) plus a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,27 @@ export function check(skill: BoundSkill): Finding[] {
}
}

// ---- _advances_to membership (dangling forward-edge check) ----
// A phase's _advances_to must name either a terminal (complete/done/end) or a
// phase that EXISTS ON DISK (a references/phases/<name>/<name>.md file). This is
// independent of the chain-consistency block below (which is gated on the whole
// backbone being frontmatter-present and self-SKIPS the moment any _advances_to
// is unresolvable — so a dangling edge would otherwise slip through as a false
// OK). Resolving against the phase DIRECTORY (not the frontmatter-declared set)
// preserves partial-rollout tolerance: an edge to a real phase that has no
// frontmatter yet still resolves; only an edge to a phase that does not exist at
// all fails.
for (const phase of skill.phases) {
if (!phase.advancesTo || TERMINALS.has(phase.advancesTo)) continue;
const targetFile = join(skill.referencesRoot, "phases", phase.advancesTo, `${phase.advancesTo}.md`);
if (!existsSync(targetFile)) {
add(
skill.rel(phase.sourceFile),
`_advances_to '${phase.advancesTo}' names neither a terminal (complete/done/end) nor an existing phase (no references/phases/${phase.advancesTo}/${phase.advancesTo}.md) — dangling forward edge`,
);
}
}

// ---- Backbone chain-consistency (backbone phases only; checkpoints excluded) ----
// The backbone is the linear lifecycle wired by _advances_to (forward) and
// _requires_phase (backward). Checkpoints (_kind: checkpoint) are off-backbone and
Expand Down
Loading