* test(#3866): pin that verify:pre must dispatch every hook kind verify-work.md's verify_pre_hooks step dispatches only `kind == "gate"`, so getWiredKinds reports verify:pre -> {gate} and gen-capability-registry rejects any capability declaring a step or contribution there. The verify lane is therefore closed to capabilities that want to contribute to what UAT covers rather than refuse to let it start. Failing-first: the step, contribution, and exact-kind-set rows are RED; the pre-existing gate row is a green regression pin so the new arms cannot orphan the arm verify:pre already had. Refs #3866 * feat(#3866): dispatch step and contribution hooks at verify:pre verify_pre_hooks dispatched `kind == "gate"` only, so getWiredKinds reported verify:pre -> {gate} and gen-capability-registry's validateHooksWired rejected any capability declaring a step or contribution there. A capability could refuse to let UAT start; it could not contribute to what UAT covers. Add contribution and step arms mirroring execute:wave:post, deferring to references/loop-hook-dispatch.md and carrying its ref.command in-context validation guard ahead of any shell-use prose. A verify:pre step is advisory: it never blocks the start of UAT and an erroring step is routed by its own onError. The gate arm and its check guard are untouched. Give extract_tests an additive consumption seam for the artefacts those steps declare via the existing steps[].produces field -- no new registry field, no new ordering, no invented filename. Manifest-supplied artefact names are validated in-context against an allowlist and resolved only inside PHASE_DIR. With no producing step the derivation is unchanged, pinned by test rather than asserted in prose. Review findings folded in: the artefact-name allowlist (isolated adversarial pass), the artefact-shape contract and the seam-inertness tests (spec axis), and the reference/how-to split so one constraint has one source of truth (standards axis). Closes #3866 * chore(#3866): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -4997,6 +4997,59 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// ─── #3866: the verify lane must be open to every hook kind ────────────────
|
||||
//
|
||||
// verify-work.md's verify_pre_hooks step historically dispatched only
|
||||
// `kind == "gate"`, so getWiredKinds reported verify:pre → {gate} and any
|
||||
// capability wanting to DO something before UAT (rather than block it) was
|
||||
// rejected at registry-build time. The rows below are the machine-observable
|
||||
// contract: what a capability may declare at verify:pre.
|
||||
|
||||
test('boundary: cap declaring a verify:pre step is accepted against real getWiredKinds(ROOT) (#3866)', () => {
|
||||
const cap = makeCapWithStep('verify:pre');
|
||||
const { getWiredKinds } = require('../scripts/gen-loop-host-contract.cjs');
|
||||
const errs = validateHooksWired(cap, getWiredKinds(ROOT));
|
||||
assert.deepEqual(
|
||||
errs, [],
|
||||
'verify:pre must dispatch step hooks — a capability can contribute to what UAT covers, ' +
|
||||
`not only refuse to let it start. Errors: ${errs.join('; ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('boundary: cap declaring a verify:pre contribution is accepted against real getWiredKinds(ROOT) (#3866)', () => {
|
||||
const cap = makeCapWithContribution('verify:pre');
|
||||
const { getWiredKinds } = require('../scripts/gen-loop-host-contract.cjs');
|
||||
const errs = validateHooksWired(cap, getWiredKinds(ROOT));
|
||||
assert.deepEqual(
|
||||
errs, [],
|
||||
`verify:pre must dispatch contribution hooks. Errors: ${errs.join('; ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('regression: cap declaring a verify:pre gate stays accepted after the step/contribution arms land (#3866)', () => {
|
||||
// The pre-existing gate arm is the one behavior verify:pre already had.
|
||||
// Adding arms above it must not orphan or narrow it.
|
||||
const cap = makeCapWithGate('verify:pre');
|
||||
const { getWiredKinds } = require('../scripts/gen-loop-host-contract.cjs');
|
||||
const errs = validateHooksWired(cap, getWiredKinds(ROOT));
|
||||
assert.deepEqual(
|
||||
errs, [],
|
||||
`the verify:pre gate arm must survive the new kind arms. Errors: ${errs.join('; ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('verify:pre dispatch text covers exactly the three hook kinds, no more (#3866)', () => {
|
||||
// Asserts the VALUE, not `.has(...)`: a scanner that over-credits (or a
|
||||
// future fourth token creeping into HOOK_KINDS) fails here too.
|
||||
const { getWiredKinds, HOOK_KINDS } = require('../scripts/gen-loop-host-contract.cjs');
|
||||
const covered = getWiredKinds(ROOT).get('verify:pre');
|
||||
assert.ok(covered, 'verify:pre must have a render-hooks call site in the host loop');
|
||||
assert.deepEqual(
|
||||
[...covered].sort(), [...HOOK_KINDS].sort(),
|
||||
`verify:pre must cover every hook kind; got ${JSON.stringify([...covered].sort())}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid points (not in VALID_LOOP_POINTS) are not flagged as "unwired" (already caught by schema validator)', () => {
|
||||
const cap = {
|
||||
id: 'test-cap',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$comment": "Growth ack (#2914 fragment). Reason: #3866 opens the verify lane to capabilities. verify-work.md's verify_pre_hooks step dispatched `kind == \"gate\"` only, so getWiredKinds reported verify:pre -> {gate} and gen-capability-registry's validateHooksWired rejected any capability declaring a step or contribution there — a capability could refuse to let UAT start but never contribute to what UAT covers. The growth is the two new dispatch arms (contribution + step, deferring to gsd-core/references/loop-hook-dispatch.md and carrying its ref.command in-context validation guard) plus the additive extract_tests consumption seam for the produces[] artefact names those steps declare. Prose is the product here: the arms ARE the dispatch contract an executing agent reads, so there is no smaller form. Includes the in-context allowlist for manifest-supplied produces names (isolated security review) and the artefact-shape contract (spec review), both of which a capability author must be able to read at the point of use. verify-work.md 35973 -> 39212 LF bytes (+3239).",
|
||||
"version": 1,
|
||||
"paths": {
|
||||
"verify-work.md": {
|
||||
"reason": "#3866: verify:pre gains contribution + step dispatch arms and extract_tests gains the produces[] consumption seam with its in-context name allowlist and artefact-shape contract; the dispatch contract is executable prose, so the arms cannot be expressed shorter; +3239 bytes"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,6 +297,81 @@ describe('ADR-857 phase 6 — capabilities must not bake install paths into the
|
||||
}
|
||||
});
|
||||
|
||||
// ─── #3866: the verify:pre produced-artefact seam must be strictly additive ──
|
||||
//
|
||||
// The lane opened at verify:pre lets a capability step produce an artefact that
|
||||
// extract_tests consumes. The contract that makes that safe is that the seam is
|
||||
// INERT when nothing is produced: derivation must be unchanged for every project
|
||||
// that has no such capability — which is every project on `next` today. These
|
||||
// assert the workflow prose an executing agent actually reads (verify-work.md is
|
||||
// Markdown, not a source path, so local/no-source-grep does not apply).
|
||||
|
||||
test('the verify:pre produced-artefact seam is conditional, and the pre-existing derivation paths are not nested inside it (#3866)', () => {
|
||||
const wf = readRepoFile('gsd-core/workflows/verify-work.md');
|
||||
|
||||
const seamIdx = wf.indexOf('Verify:pre produced-artefact seam');
|
||||
assert.ok(seamIdx > 0, 'verify-work.md must carry the verify:pre produced-artefact seam');
|
||||
|
||||
// The seam must open with its own skip-when-absent guard, so an agent reading
|
||||
// it top-down never falls into the merge on a project with no producing step.
|
||||
const seamHead = wf.slice(seamIdx, seamIdx + 400);
|
||||
assert.match(
|
||||
seamHead, /VERIFY_PRE_PRODUCED/,
|
||||
'the seam must name the variable it is conditional on',
|
||||
);
|
||||
assert.match(
|
||||
seamHead, /empty or absent/,
|
||||
'the seam must state the empty/absent case before describing any merge',
|
||||
);
|
||||
|
||||
// Both pre-existing derivation paths must still exist, and must sit OUTSIDE the
|
||||
// seam: the coverage classifier before it, the legacy prose fallback after it.
|
||||
// If either migrated inside the seam it would become conditional on a producing
|
||||
// step existing — the exact regression "byte-identical when no artefact exists"
|
||||
// rules out.
|
||||
const coverageIdx = wf.indexOf('uat.classify-coverage');
|
||||
const legacyIdx = wf.indexOf('Extract testable deliverables from SUMMARY.md');
|
||||
assert.ok(coverageIdx > 0, 'the #1602 coverage classifier must still be invoked');
|
||||
assert.ok(legacyIdx > 0, 'the legacy prose-extraction fallback must still exist');
|
||||
assert.ok(
|
||||
coverageIdx < seamIdx,
|
||||
'coverage classification must run before the seam, not inside it',
|
||||
);
|
||||
assert.ok(
|
||||
legacyIdx > seamIdx,
|
||||
'the legacy fallback must follow the seam and stay unguarded by it',
|
||||
);
|
||||
});
|
||||
|
||||
test('the verify:pre produced-artefact seam validates manifest-supplied artefact names in-context (#3866)', () => {
|
||||
const wf = readRepoFile('gsd-core/workflows/verify-work.md');
|
||||
const seamIdx = wf.indexOf('Verify:pre produced-artefact seam');
|
||||
assert.ok(seamIdx > 0, 'verify-work.md must carry the verify:pre produced-artefact seam');
|
||||
const seam = wf.slice(seamIdx, legacyEnd(wf, seamIdx));
|
||||
|
||||
// `produces` names come from a third-party capability manifest. The seam must
|
||||
// carry an explicit in-context allowlist, the same shape loop-hook-dispatch.md
|
||||
// requires of `ref.command` — not a vague "it is a name, not a path".
|
||||
assert.match(
|
||||
seam, /\^\[A-Za-z0-9\]\[A-Za-z0-9\._-\]\*\$/,
|
||||
'the seam must pin an explicit allowlist regex for artefact names',
|
||||
);
|
||||
assert.match(
|
||||
seam, /never\*{0,2}\s*by pasting it into a\s*\n?\s*shell command|never\*{0,2} by pasting it into a shell/,
|
||||
'the seam must forbid shell-side validation of the manifest value',
|
||||
);
|
||||
assert.match(
|
||||
seam, /\$PHASE_DIR/,
|
||||
'the seam must confine resolution to the phase directory',
|
||||
);
|
||||
});
|
||||
|
||||
/** End of the seam region: the next top-level bold heading after it. */
|
||||
function legacyEnd(wf, seamIdx) {
|
||||
const next = wf.indexOf('**Extract testable deliverables', seamIdx);
|
||||
return next > seamIdx ? next : Math.min(wf.length, seamIdx + 3000);
|
||||
}
|
||||
|
||||
test('every declared gate check.query returns a uniform boolean `block` field', () => {
|
||||
// FIX A regression guard: every gate check command must return a top-level
|
||||
// boolean `block` field so the host-loop dispatch can read a single consistent
|
||||
|
||||
Reference in New Issue
Block a user