* feat(#3840): generate docs/FEATURES.md from per-feature fragments docs/FEATURES.md was hand-maintained, and every feature PR wrote into two shared mutable cells: the '### N.' heading whose integer was hand-allocated at authoring time, and the hand-maintained table of contents. Concurrent PRs all picked the same next integer, and two PRs adding differently numbered features still collided on the TOC. #3831 was renumbered 165 -> 166 -> 167 -> 168 across successive rebases, each collision also costing a full matrix verification run because the sha-keyed pass marker dies with the rebase. Mechanism: one fragment per feature at docs/features/<slug>.md carrying id/title/group (and an optional order) in frontmatter, consolidated by scripts/gen-features.cjs --write|--check into a marker-delimited region of docs/FEATURES.md that holds BOTH the TOC and every section body. Group headings and their order are derived too - a group sorts by its lowest-ordered member - so there is no shared registry to edit either; optional per-group prose lives in docs/features/_groups/<slug>.md. A contributor adds exactly one new file. Wired into regen:derived and lint:generated-sync alongside the eight existing generators, matching gen-adr-index.cjs's CLI shape and typed-REASON reporting. Migration froze all 168 existing numbers verbatim: identical section set, identical order, identical bodies. Two defects found in the tree are fixed inline rather than carried forward - the '## Related' block had been spliced into the middle of the document, orphaning §142's Reference line, and four inbound anchors were already broken on next (FEATURES.md#runtime-identity in two files, and #143-spec-phase-edge-completeness-probe off by one). Since the repo has no link checker, --check now validates every inbound FEATURES.md#anchor by resolved target, so that class cannot ship silently again; locale FEATURES.md files resolve elsewhere and stay out of scope. Refs #3840 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#3840): carry upstream §69 delta into its fragment and harden the generator Review found section 69 missing '[--strict]' and REQ-STATE-05/06 versus origin/next. Root cause was a stale base, not extraction loss: those lines landed in394bf384b(#3844) AFTER this branch forked at63abcface, and 'git diff63abcfaceorigin/next -- docs/FEATURES.md' is exactly that hunk. Merging origin/next auto-applied the hunk into the GENERATED region, which --check immediately reported as stale; the delta is now carried in docs/features/statemd-consistency-gates.md and regenerated from there. --write is now fail-closed. It previously rendered the region even with violations outstanding, warning only on stderr and exiting 0, so a '--write && git commit' chain could commit a FEATURES.md carrying two colliding sections. It now refuses and exits 1; --force is the explicit override and says so in the report. The test that pinned the old behavior now pins the refusal, plus the --force override and its scoping. Marker forgery is rejected at two layers. A fragment body containing '<!-- FEATURES:START' or '<!-- FEATURES:END' is a typed body_forges_region_marker violation (fragments and group notes alike), and spliceIntoFeatures anchors the end boundary with lastIndexOf instead of indexOf, so a marker that reaches the document by any other route can only make the generated region grow, never shrink. Matching is on marker PREFIXES, so a decorated variant comment cannot slip past. Symlinked corpus entries are refused with a typed dirent_not_regular_file rather than read. A fork PR could otherwise commit docs/features/evil.md as a symlink to any readable path and have the generator inline those bytes into the committed docs/FEATURES.md on the next regen. Equivalence re-verified with a method that cannot cancel out. The first check extracted both operands with the same body-normalising helper, so anything that helper dropped was dropped on both sides. The replacement runs two independent passes: a global content-line multiset diff with no per-section logic at all (0 gained, 19 lost, all 19 the stale hand-written mini-TOC links this change deliberately deletes), and a per-section byte-exact body diff carrying a coverage assertion that fails loudly per file when the extractor accounts for fewer lines than the file contains. That assertion caught two blind spots in the checker itself. 168/168 sections present, order identical, one intended body difference (§142 regains the Reference line orphaned by the misplaced '## Related' block). Refs #3840 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#3840): backfill changeset PR number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
40 lines
5.1 KiB
Markdown
40 lines
5.1 KiB
Markdown
---
|
|
id: 146
|
|
title: Spec-Phase Prohibition Probe
|
|
group: v1.43.0 Features
|
|
---
|
|
|
|
**Command:** `/gsd-spec-phase`
|
|
|
|
**Purpose:** Surface the unwritten *must-NOT* constraints — the values/safety/ethics interpretations a feature could silently become that the author would never want but the spec does not forbid — before any code is written. The edge probe reaches data-shape edges; it structurally cannot reach prohibitions. This is the missing instrument, running as `Step 5.6` of spec-phase, after the edge probe.
|
|
|
|
**Behavior:** A two-stage, prose-orchestrated pass per requirement (no compiled recall engine — recall is inherently model-driven, ADR-550 D7b):
|
|
|
|
1. **Recall (adversarial probe):** *"What could this feature silently become that the author would NOT want, but the spec does not forbid?"* — model-robust open-vocabulary elicitation across values/safety/ethics.
|
|
2. **Precision (one-pass classifier):** drop routine-engineering items, keep genuine values/safety/ethics prohibitions — collapses the raw list to the load-bearing few.
|
|
|
|
Each surfaced prohibition is resolved to exactly one of three states:
|
|
|
|
| State | Meaning | Downstream effect |
|
|
|-------|---------|-------------------|
|
|
| `resolved` | Confirmed a real must-NOT | NEGATIVE acceptance criterion written into the SPEC `## Prohibitions (must-NOT)` section; lifted into `plan-phase` `must_haves.prohibitions` (its own sibling block, never `truths`) |
|
|
| `dismissed` | Not a genuine prohibition (requires a non-empty reason) | Recorded with its reason; empty dismissals are rejected |
|
|
| `unresolved` | Deferred | Soft-gates the spec; surfaced as a planner assumption |
|
|
|
|
Each resolved prohibition carries a `verification` tier — `test` (a negative test can enforce it) or `judgment` (only human/LLM judgment can). At verify time, judgment-tier prohibitions route to a never-silent / never-hard-halt soft gate (autonomous emits an `unverified-prohibition — human review recommended` flag); test-tier prohibitions are enforced via the deterministic `check prohibition-enforcement` gate — green when the wired negative test / lint rule passes, hard-gate (flagged, non-green) when missing or failing, in both interactive and autonomous modes (#1259, ADR-550 D5d). Under `--auto`, the probe **never auto-dismisses**. Canon-bound concerns (OWASP / GDPR / fairness) are referred to `/gsd-secure-phase` rather than minting SPEC prohibitions (ADR-550 D6).
|
|
|
|
The load-bearing wire is the `plan-phase` lift into `must_haves.prohibitions`, so the section is not merely documentation.
|
|
|
|
**Deterministic prohibition-check descriptor source (#1278).** A resolved `test`-tier prohibition MAY carry an optional **`check` descriptor** — the flat-scalar keys `check_kind` (`node-test` | `lint-rule`), `check_target`, and `check_rule` (lint-rule only) — authored at spec-phase. `projectProhibitions` projects these scalars deterministically and verify-phase reads them back to locate the check handed to `check prohibition-enforcement`, so a wired, passing test closes the gap with **zero manual descriptor authoring** (previously the verify-phase LLM had to invent `{kind, target, rule}` each run, #1259). The descriptor is **optional and backward-compatible** — a descriptor-less prohibition parses and disposes byte-identically to today — and **fail-closed**: a partial, invalid, or absent descriptor falls through to the producer's existing fail-closed locate, never a silent green. `failFirst` stays a verify-time caller attestation (machine-proven fail-first is tracked in #1279).
|
|
|
|
**Requirements:**
|
|
- REQ-PROHIB-01: The prohibition pass MUST run after the edge probe and emit a `## Prohibitions (must-NOT)` SPEC section.
|
|
- REQ-PROHIB-02: Stage 1 MUST ask the adversarial recall question; Stage 2 MUST drop routine-engineering items and keep values/safety/ethics prohibitions.
|
|
- REQ-PROHIB-03: A `dismissed` resolution MUST require a non-empty reason.
|
|
- REQ-PROHIB-04: `--auto` MUST never auto-dismiss.
|
|
- REQ-PROHIB-05: `plan-phase` MUST lift resolved prohibitions into `must_haves.prohibitions` (never `truths`).
|
|
- REQ-PROHIB-06: A well-formed but unwired `test`-tier prohibition MUST fail closed at verify time — never a silent pass.
|
|
- REQ-PROHIB-07: A `test`-tier prohibition with a **machine-proven-fail-first**, genuinely-passing (non-vacuous) wired mechanical check (a `node --test` negative test OR a lint/AST rule) MUST dispose green and be satisfiable; a missing, un-provable, or non-passing check MUST hard-gate (flagged, non-green) in both interactive and autonomous modes. Fail-first is **machine-proven, not caller-attested** (#1279, ADR-550 D5d): before a clean pass greens, the producer independently runs the wired check against a known violation (the descriptor's `violationFixture`) and confirms it goes RED — a lint rule via the violating fixture, a node test via the violating subject injected through the `GSD_PROHIB_SUBJECT` convention; absent a violation source it fails closed, never falling back to attestation. (Enforcement half shipped #1259; deterministic descriptor auto-locate in #1278.)
|
|
|
|
**Reference:** [Prohibition Probe](../gsd-core/references/prohibition-probe.md)
|