* 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:
@@ -61,15 +61,26 @@ MVP_MODE=$(gsd_run query phase.mvp-mode "${phase_number}" ${GSD_WS} --pick activ
|
||||
</step>
|
||||
|
||||
<step name="verify_pre_hooks">
|
||||
**Verify:pre gate dispatch.** Before verification begins, dispatch every active
|
||||
gate hook registered at the `verify:pre` loop extension point. Each gate is
|
||||
data-driven — resolved from the capability registry, not hardcoded here.
|
||||
**Verify:pre capability dispatch.** Before verification begins, dispatch every
|
||||
active hook registered at the `verify:pre` loop extension point — of **every**
|
||||
kind, not gates alone. Each hook is data-driven — resolved from the capability
|
||||
registry, not hardcoded here.
|
||||
|
||||
```bash
|
||||
VERIFY_PRE_HOOKS_JSON=$(gsd_run loop render-hooks verify:pre --raw)
|
||||
PHASE_DIR=$(printf '%s' "$INIT" | jq -r '.phase_dir // empty')
|
||||
```
|
||||
|
||||
Read the `activeHooks` array from `VERIFY_PRE_HOOKS_JSON` in-context (do NOT pipe through a shell parser).
|
||||
|
||||
**If `activeHooks` is empty or absent:** skip silently to `check_active_session`.
|
||||
|
||||
**Contribution dispatch:** inject every `kind == "contribution"` fragment per @gsd-core/references/loop-hook-dispatch.md (skip when none), before the steps and gates below.
|
||||
|
||||
**Step dispatch:** dispatch every `kind == "step"` hook per @gsd-core/references/loop-hook-dispatch.md (skip when none) — not one shape of one. A step here is advisory: it never blocks the start of UAT, and a step that errors is routed by its own `onError` without failing verification. ⚠ **Validate `ref.command` in-context before any shell use** (third-party manifest input) — loop-hook-dispatch.md § `step`.
|
||||
|
||||
Record the union of `produces` artefact names declared by the active step entries as `VERIFY_PRE_PRODUCED` — `extract_tests` consumes it below. An entry declaring `produces: []` contributes nothing, which is the normal case.
|
||||
|
||||
⚠ **Validate `check` before shell use** (third-party manifest input) — `loop-hook-dispatch.md` § `gate`.
|
||||
|
||||
Resolve active gate hooks from `VERIFY_PRE_HOOKS_JSON` where `kind == "gate"`.
|
||||
@@ -187,6 +198,35 @@ Read the JSON result (`mode`, `total`, `all_auto_covered`, `auto_passed[]`, `pre
|
||||
|
||||
The cold-start smoke test injection below still applies in `coverage` mode.
|
||||
|
||||
**Verify:pre produced-artefact seam (#3866).** If `VERIFY_PRE_PRODUCED` (recorded in
|
||||
`verify_pre_hooks`) is empty or absent, skip this paragraph entirely — derivation is unchanged.
|
||||
Otherwise, for each artefact name in it, locate the artefact the producing step wrote under
|
||||
`$PHASE_DIR` and merge its checkpoints into the test list **additively**.
|
||||
|
||||
**The artefact contract.** A consumable artefact is a Markdown file holding a list of checkpoint
|
||||
entries in the **same shape `extract_tests` already emits and `create_uat_file` already consumes** —
|
||||
each entry a `name` (brief test name) and an `expected` (specific, user-observable outcome).
|
||||
Nothing else is read: extra fields are ignored, not an error. There is no new schema and no new
|
||||
parser — a producing step writes what a checkpoint already looks like. An artefact that yields zero
|
||||
parseable entries is treated exactly like an absent one (see below).
|
||||
|
||||
Merge rules:
|
||||
|
||||
- ⚠ **Validate the artefact name in-context before resolving it** (third-party manifest input).
|
||||
An artefact name is a registry-declared name, **not** a path: check the value you read from
|
||||
`produces` against `^[A-Za-z0-9][A-Za-z0-9._-]*$` yourself — **never** by pasting it into a
|
||||
shell command to be tested there. A name carrying `/`, `..`, a leading `-`, a leading path
|
||||
separator, or any shell metacharacter is a malformed manifest: record a warning, skip that
|
||||
name, continue. Only a validated name is resolved, and only against what the step wrote inside
|
||||
`$PHASE_DIR` — never above it, and never through a symlink that leaves it.
|
||||
- A name with no artefact on disk means that step was inactive, skipped, or failed. Note it to the
|
||||
user and derive normally — **never drop a deliverable and never block UAT over it.**
|
||||
- Merged entries are added to, never subtracted from, what `coverage:` classification and the
|
||||
prose fallback produce. An `auto_passed[]` entry stays un-presented; a `present[]` entry stays a
|
||||
human checkpoint. This seam can deepen UAT, not suppress it.
|
||||
- Deduplicate against already-derived checkpoints by `name`, keeping the earlier entry's
|
||||
`expected` text so a produced artefact cannot silently rewrite a criterion.
|
||||
|
||||
**Extract testable deliverables from SUMMARY.md (legacy fallback — used when `mode: legacy`):**
|
||||
|
||||
Parse for:
|
||||
|
||||
Reference in New Issue
Block a user