* fix(#2723): reconcile emitted manifest families as a set, not a shared count EXPECTED_MANIFEST_COUNT was a single literal 19 asserted against both the baseline (built at the base ref) and the current tree (built at PR HEAD). Those sides legitimately differ by one family whenever a PR adds or removes a runtime, so no value satisfied both: 19 rejected the current side, 20 rejected the baseline side. Every runtime-adding PR was hard-blocked. Replace the shared literal with three independent signals - the derived family set, the recorded fixture set, and the families present at the base ref - reconciled as sets in both directions. A family may appear or vanish only when the diff plausibly touches the runtime registry, and the failure names the family rather than a count. An absolute floor catches the uniformly shrunken universe a same-count self-check passes vacuously. Found by tracing #2005 (Qoder runtime) through the gate during the ADR-2719 dual-run window. * fix(#2723): read the baseline family set from the ref, not HEAD's registry Review found three defects in the first cut. Blocker: baselineManifestsAtRef enumerated MANIFEST_FAMILIES, which is imported at module load and therefore describes PR HEAD. A runtime REMOVED by the PR is already absent from that list, so the base ref was never asked for it, the baseline silently omitted a family that genuinely existed, and the dropped-family check could never fire in production - while its unit tests passed, because they inject the baseline directly. Enumerate from the ref with git ls-tree instead. Also: narrow the registry-signal set to the two surfaces that actually define the family set, since every extra path widens what excuses an unattributed delta; drop the ack bypass, which was a one-sided escape hatch making removals easier to wave through than additions; and gate the derived/fixtures inputs so malformed values return a verdict rather than an unhandled TypeError. * fix(#2723): filter prototype-shaped family names read from git output baselineFamilyNamesAtRef derives object keys from git ls-tree output rather than a trusted constant, so a fixture committed as __proto__.json would turn the manifests[name] assignment into a prototype write. Compared inline rather than through a Set, which is the form the prototype-pollution analysis recognizes. * fix(#2723): require an exact capability path depth and make the ref test hermetic The remote runner went red on both linux lanes with two real defects. The capability signal matched by prefix+suffix, so 'capabilities/capability.json' (no runtime segment) and 'capabilities/a/b/capability.json' (wrong depth) both attributed a family change and would have excused an unattributed delta. Anchored to an exact single-segment pattern. The ref-derivation test reached for this repo's root commit, which is not stable: the remote runner shallow-clones, so rev-list --max-parents=0 returns the grafted boundary carrying every fixture, and this repo has two root commits locally anyway. It now builds its own git repo containing a family absent from the current registry - the real discriminator, and one the root-commit version could never assert. Lint then caught a third: the test called t.after() without declaring t. * fix(#2723): stop asserting ref enumeration against the ambient checkout The remote runner returned [] for the repo's own HEAD while every hermetic temp-repo assertion in the same test passed. That is this function's documented behavior when git cannot read the ref - the runner works from a shallow clone under a bind-mounted workdir - so the assertion was testing the checkout rather than the code. Dropped it. The temp repo already proves the property that matters, and proves it more strongly: it contains a family absent from the current registry, which a registry-derived implementation could never report. The ambient path stays covered by the real-tree test, which skips explicitly when no base ref is resolvable. A git failure is not silently permissive downstream: baselineManifestsAtRef returns null on an empty family set and the real-tree test asserts the baseline is non-empty.
This commit is contained in:
@@ -72,6 +72,42 @@ const RUNTIME_META = {
|
||||
zcode: { localDir: '.zcode', globalSuffix: '.zcode' },
|
||||
};
|
||||
|
||||
/**
|
||||
* The emitted manifest families, as (fixtureName -> install spec).
|
||||
*
|
||||
* NOT simply `Object.keys(RUNTIME_META)`: that has 18 entries while the fixture set has
|
||||
* 19. The extra one is `claude-local` — claude is the reference host and the ONLY
|
||||
* runtime with a distinct LOCAL "legacy flat-commands" layout (`commands/gsd-*.md` +
|
||||
* `agents/gsd-*.md` at project scope), which `golden-install-parity.test.cjs` guards
|
||||
* with a hand-coded test outside its RUNTIME_META loop (#2086).
|
||||
*
|
||||
* Enumerating from RUNTIME_META alone dropped that family from BOTH sides of the
|
||||
* differential, so a same-count self-check (18 === 18) passed vacuously and a PR
|
||||
* changing Claude's local-scope output would fail the golden while the attribution
|
||||
* check reported ok.
|
||||
*
|
||||
* Lives HERE, beside RUNTIME_META, so the emitted-attribution helpers and the
|
||||
* emitted-provenance table read ONE derivation rather than each carrying a literal.
|
||||
* Two surfaces sharing a hand-maintained count is what produced the #2723 deadlock:
|
||||
* a single constant was asserted against both the base ref and the PR head, which
|
||||
* legitimately differ whenever a PR adds or removes a runtime.
|
||||
*/
|
||||
const MANIFEST_FAMILIES = [
|
||||
...Object.keys(RUNTIME_META).map((runtime) => ({ name: runtime, runtime, scope: 'global' })),
|
||||
{ name: 'claude-local', runtime: 'claude', scope: 'local' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Absolute floor on the family set, independent of any derivation.
|
||||
*
|
||||
* A pure equality between "derived" and "recorded" cannot catch a universe that shrank
|
||||
* on BOTH sides at once (drop a RUNTIME_META entry and delete its fixture together, and
|
||||
* 18 === 18 passes over a smaller world). This floor is the one number that must not be
|
||||
* derived — it ratchets, and lowering it is a deliberate, reviewable act. It never
|
||||
* blocks ADDING a runtime, which is the asymmetry the old shared literal lacked.
|
||||
*/
|
||||
const MINIMUM_MANIFEST_FAMILIES = 19;
|
||||
|
||||
// Runtimes that emit per-skill files under skills/ (not rules-based or commands-based)
|
||||
const SKILL_RUNTIMES = [
|
||||
'claude', 'opencode', 'kilo', 'codex', 'copilot', 'antigravity',
|
||||
@@ -396,6 +432,8 @@ module.exports = {
|
||||
EXPECTED_SH_HOOKS,
|
||||
EXPECTED_ALL_HOOKS,
|
||||
RUNTIME_META,
|
||||
MANIFEST_FAMILIES,
|
||||
MINIMUM_MANIFEST_FAMILIES,
|
||||
SKILL_RUNTIMES,
|
||||
PKG_VERSION,
|
||||
VOLATILE_FILES,
|
||||
|
||||
Reference in New Issue
Block a user