* feat(spec-phase): spec-completeness edge-probe (#550) — relocated to gsd-core/
Rebased onto current next and relocated the whole feature from get-shit-done/ to
gsd-core/ per #615 (trek-e re-review #4, option 1). The artifact now builds to
gsd-core/bin/lib/edge-probe.cjs; all hard-coded path strings (tests, workflow
@-refs, run-tests.cjs sentinel, eslint ADR-457 ignore, .gitignore) updated.
Content conflicts in .gitignore / eslint.config.mjs / run-tests.cjs resolved.
Feature: Step 5.5 edge-completeness probe walks each SPEC requirement against a
closed 8-category edge taxonomy, proposes applicable candidate edges, and resolves
each (covered/dismissed/backstop/unresolved). covered/backstop criteria are lifted
by plan-phase into must_haves.truths, extending the goal-backward verifier's reach
to boundary edges no requirement was written for. Engine authored as strict TS
(src/edge-probe.cts, ADR-457), compiled to the gitignored gsd-core/bin/lib/edge-probe.cjs.
Folds in every prior review round on PR #584:
- RR-01..03: plan-phase resolves the phase *-SPEC.md and injects {SPEC_PATH} into the
planner; must_haves<->Edge-Coverage quality_gate; held-out planner-contract test.
- RR-04/11: Step 5.5 invokes the compiled engine at runtime (npm --prefix-pinned,
source-checkout-gated build fallback) instead of LLM re-derivation; the engine
capture is exit-checked and the report JSON-validated before use (fail closed).
- RR-05..10: all six fixtures embedded + count-equality; backstop/covered require a
resolution; Array.isArray(shapes); duplicate-resolution rejection; CLI JSON exit(2);
per-artifact build sentinel.
- Authored-shape validation: invalid (non-empty) shapes fail closed (VALID_SHAPES).
- Adversarial-review hardening: orphan/typo resolution rejection, requirement input
validation (id/text/shapes, duplicate id, non-array), zero-applicable guard.
Full suite 0 failures; npm run lint 0 errors; edge-probe suite 72/72.
* test(#550): RED — status×verification re-cut + probe-core engine specs
Re-cut the edge-probe resolution model onto two orthogonal axes per
ADR-550 Decision 7 (trek-e #644 comments 2026-06-03 14:36 + 14:44):
status: resolved | dismissed | unresolved (lifecycle, shared)
verification: explicit | backstop | null (only when resolved)
- tests/probe-core.test.cjs (new): behavioral specs for the generic engine
to be extracted — validateResolution(r, validators), validateRequirement,
analyzeCoverage(items, resolutions?, validators), byVerification rollup,
runProbeCli I/O scaffold (injected-io unit tests).
- tests/edge-probe.test.cjs: covered→{resolved,explicit}, backstop→
{resolved,backstop}; coverage gains byVerification.{explicit,backstop};
proposeEdges items gain verification:null.
- 6 fixtures re-genned + re-embedded in edge-probe.md; coverage.resolved
COUNT preserved on every fixture (closed set = resolved+dismissed; doc
line: 'adjacency=covered + ordering=dismissed' -> 2). edge-probe.md prose
rewritten to the two-axis model.
Fails as expected: probe-core.cjs has no source yet; edge-probe still
emits the old covered/backstop enum (27/61 edge specs red).
* feat(#550): extract probe-core seam + refactor edge-probe onto it (ADR-550 D7)
Extract the generic resolution model into src/probe-core.cts (the shared
seam the prohibition probe #644 is born on) and refactor edge-probe.cts
into its first adapter.
probe-core owns (probe-agnostic):
- the status×verification re-cut: status: resolved|dismissed|unresolved ×
verification: <probe-defined>|null
- validateResolution(r, validators) / validateRequirement (generic id+text)
- analyzeCoverage(items, resolutions?, validators) over ALREADY-PROPOSED
items[] (core never assumes propose is deterministic — edge resolves via
LLM, #644 proposes via LLM), with merge / dup-reject / orphan-reject
- byVerification rollup; coverage.resolved = closed set (resolved+dismissed),
count-preserved from the pre-re-cut engine
- runProbeCli I/O scaffold (injected io; one bin per probe)
- hybrid typing: generic params + injected runtime validators
{categories, verification, requiredFieldsByVerification} (ADR-550 #5)
edge-probe keeps ONLY the edge cluster: Shape/SHAPE_CUES/VALID_SHAPES/
classifyShape/TAXONOMY/applicableCategories/proposeEdges + EDGE_VALIDATORS
{explicit,backstop}; delegates merge/rollup/CLI to probe-core. Every shipped
#584 guarantee preserved (fail-closed shapes, orphan/dup rejection, input
validation, CLI exit 2). 104/104 edge+probe-core+docs+contract specs green.
* chore(#550): register probe-core.cjs artifact in ledgers + inventory
New gitignored build artifact gsd-core/bin/lib/probe-core.cjs (compiled
from src/probe-core.cts) needs registering in every artifact ledger:
- .gitignore + eslint.config.mjs ADR-457 ignore: lint the .cts source,
never the emitted .cjs.
- scripts/run-tests.cjs per-artifact build sentinel: build if probe-core.cjs
is missing on a clean checkout.
- docs/INVENTORY.md: CLI Modules 83 -> 84, new probe-core.cjs row, and the
edge-probe.cjs row updated to reflect it is now the first probe-core adapter.
- docs/INVENTORY-MANIFEST.json: regenerated (gen-inventory-manifest.cjs --write).
probe-core.test.cjs is a single test file (under the 2-file cap), so no
lint-test-file-count allowlist entry is needed.
* docs(adr-550): spec-phase probe pattern + prohibition contract [Accepted]
trek-e's final ADR-550 body, verbatim (open-gsd/gsd-core#644 comment
2026-06-03T15:23Z), Accepted by both maintainer and #550 author. Lands on
PR #584 alongside the probe-core extraction (Decision 7) it governs, so the
contract and its first implementation arrive together.
Decisions: probe packaging (3 layers); recall->precision protocol;
prohibition home = SPEC acceptance criteria + optional must_haves.prohibitions:
(truths untouched, no polarity); tiered verification test|judgment
(judgment = mode-dependent soft-gate-with-flags, never silent pass / never
hard-halt); CI tests the contract not the classifier; secure-phase ownership
seam; and Decision 7 — probe-core seam + status×verification re-cut (7a-7e),
which this PR implements.
* feat(#550): fail-closed probe-core across full status×verification + runProbeCli structural guard
Re-review #5 (trek-e) seam-hardening on the generic probe-core contract #644 inherits:
- validateResolution now enforces the 'verification is null unless resolved'
invariant for EVERY status (not just resolved): a dismissed/unresolved
resolution carrying a verification tier is rejected instead of merging verbatim.
- An unresolved resolution carrying a resolution/reason payload is rejected
(was silently dropped into the unresolved count).
- runProbeCli structurally validates the report an adapter returns before writing
it (was: any malformed object stringified as green output) — fails closed → exit 2.
- coverage.resolved kept count-preserved (closed set) per the blessed migration
contract; a new test locks that an all-dismissed run is NOT affirmatively covered
(byVerification is the honest gate).
Tests: probe-core 37/37; full edge-probe suite 113/113; full suite 1816/1816; lint 0.
* docs(adr-550): annotate Decision 5 #584/#644 scope + correct 7a coverage.resolved semantics
Re-review #5 (trek-e) clarity edits:
- Decision 5: annotate that only contract item (a) ships on #584 (the edge
adapter's parse+validate test); (b)–(d) are #644 scope, matching Consequences.
- Decision 7a: correct the 'coverage.resolved is preserved (status === resolved)'
parenthetical — the blessed/implemented semantics are count-preserved = the
CLOSED set (resolved + dismissed = applicable − unresolved), with byVerification
carrying the per-tier resolved-status breakdown. The old parenthetical
contradicted the shipped count.
* test(#550): cover runProbeCli structural-guard numeric-count branch
Second-pass coverage audit found the 'coverage object present but counts
non-numeric' branch of isValidReport (built probe-core.cjs:60-61) unexercised —
the {nope:true} malformed test fails earlier at the items[] check. Add a report
with well-formed items[] + a coverage object carrying non-numeric counts so the
numeric branch is hit. No source change; closes the line gap.
* fix(#550): reject edge requirement with missing/empty text when no shapes override (M2)
The edge adapter's `text` is the classification signal and a required field, but
core `validateRequirement` left it optional, so a `{ id }` requirement classified to
zero shapes -> zero edges -> was silently DROPPED from coverage with no signal -- the
exact fail-open this feature exists to eliminate. Reject missing/empty text unless an
authored `shapes` override (incl. `[]`) opts out of prose classification.
* fix(#550): validate verbatim items in analyzeCoverage shared seam (m1)
A proposed item with no matching author resolution is rolled up VERBATIM, but its own
status/fields were never validated -- an item carrying an out-of-enum status (the dropped
"covered") or `dismissed` without a reason would be counted closed. The edge adapter only
proposes `unresolved` items, but the prohibition adapter (#644) proposes LLM-generated
items that arrive populated. An Item is structurally a superset of a Resolution, so reuse
validateResolution to fail closed. ADR-550 Decision 5 hardens this shared seam.
* fix(#550): move edge-coverage lift instruction to runtime planner surface (M1)
templates/planner-subagent-prompt.md is loaded by nothing at runtime (no @-import in
agents/gsd-planner.md; plan-phase.md spawns the planner from its own inline
<planning_context>), so the precise covered/backstop -> must_haves.truths lift instruction
this PR added there never reached the planner -- and the RR-02 contract test asserted it in
that dead file, giving false green. Move the instruction into plan-phase.md's runtime
<downstream_consumer> block (where the rest of the wire already lives), revert the dead-template
edit, and retarget RR-02 to the loaded surface with a guard against re-orphaning.
* test(#550): lock machine<->SPEC vocabulary mapping against drift (m2)
The machine contract uses orthogonal status x verification; the SPEC table renders a flat
covered/dismissed/backstop/unresolved. The migration map (ADR-550 Decision 7a) was prose-only
with no test, so the layers could silently drift. The SPEC table is LLM-rendered (no JS
renderer to round-trip), so pin the canonical bijection as code AND ground it in every doc
surface that renders the vocabulary (ADR migration clause, spec.md legend, reference mapping
table) -- a rename or remap now fails the suite.
* docs(#550): add how-to for resolving edge-coverage findings (B1)
Feature shipped reference coverage (FEATURES.md, COMMANDS.md, references/edge-probe.md) but
no how-to -- reference-only does not satisfy the Diataxis docs standard for a user-facing
capability. Add a single-mode how-to (imperative, goal-directed) walking each resolution
state (specify/dismiss/backstop/defer), the soft gate, and --auto, with taxonomy/concepts
linked out to the reference. Register it in the docs/how-to index.
* docs(#550): add Probe Core + Edge Probe glossary entries to CONTEXT.md (N1)
trek-e re-review #7 N1 (Major): adding probe-core/edge-probe as src/*.cts-derived
seam modules (ADR-550 Decision 7) requires CONTEXT.md Domain-terms glossary entries
per the maintainer-enforced new-seam gate. Adds '### Probe Core Module' and
'### Edge Probe Module' with exports, generated source paths, and the ADR-550 seam
contract, placed beside the Research Module feature-seam entries.
* test(#550): add fast-check property suite for probe-core analyzeCoverage (N2)
trek-e re-review #7 N2 (RULESET.TESTS.property-based-testing): analyzeCoverage is a
transformation/rollup module, the class the property-testing predicate covers, and
fast-check is already a dependency with an established *.property.test.cjs pattern.
Adds 5 properties over the algebraic invariants: closed-set identity
(applicable === resolved + unresolved), byVerification sums ≤ resolved, per-tier
recount + resolved-status-only counting, rollup determinism, and stable orphan
rejection. 200 runs/seed 42 via helpers/fast-check-setup.cjs.
* test(#550): align allow-test-rule tokens to canonical runtime-contract-is-the-product (N3)
trek-e re-review #7 N3 (Nit): the // allow-test-rule: tokens (source-text-is-the-product,
docs-parity) differed from CONTEXT.md's canonical exemption category
'runtime-contract-is-the-product' (RULESET.TESTS.no-source-grep.exemption, CONTEXT.md:240).
All three tests assert deployed runtime-contract surfaces (spec-phase.md Step 5.5, the
plan-phase.md planner prompt, the rendered reference/SPEC/ADR vocabulary), so the canonical
category fits; each now carries a one-line justification per the ruleset format. Free-text
reason — lint behavior unchanged.
* test(#550): re-baseline plan-phase + spec-phase byte sizes for edge-probe
Rebased onto next (e4f0910d), which replaced the line-based tier-max
size ratchet (#597) with the byte-based per-file baseline guard (#1074).
The edge-probe feature legitimately grows two workflows:
- spec-phase.md 15131 -> 23094 (+7963): Step 5.5 Edge-Completeness Probe
- plan-phase.md 93135 -> 94253 (+1118): covered/backstop edge lift into
must_haves.truths (the live <downstream_consumer> block)
Both remain under their tier hard caps (plan-phase XL 98304, ~4KB
headroom; spec-phase DEFAULT 40960). Growth is real inline workflow
content the feature requires at that step — not eager @-import proxy
gaming. Drops the obsolete line-based XL_BUDGET 93000->94000 bump
(superseded by the byte baseline) via rebase.
* chore(#550): reconcile INVENTORY headline counts after rebase onto next
Rebase onto current next dropped the prior reconcile commit (stale counts
refs 68 / CLI 107). Current next + the edge-probe additions yield:
- References (68 -> 69 shipped): + gsd-core/references/edge-probe.md
- CLI Modules (107 -> 109 shipped): + edge-probe.cjs + probe-core.cjs
Rows for all three already present; only the headline counts were stale.
Caught by tests/inventory-counts.test.cjs (CI ubuntu-24 leg).
16 KiB
Edge-Probe — Spec-Completeness Reference
Shared reference for the spec/requirements phase. Companion to
@~/.claude/gsd-core/references/domain-probes.md: domain-probes covers the
technology axis (auth, search, caching, deployment); this covers the
data/behavior-shape axis (boundaries, adjacency, encoding, ordering…). Walk each
requirement against the closed taxonomy below, propose a concrete candidate edge for each
applicable category, and resolve each to exactly one state. Adopt the established QA names
verbatim — this is decades-old black-box test technique, moved upstream to the spec layer.
This doc is written in generic requirements → checks → verifier terms with no
tool-specific vocabulary, so it is portable: copy it into any spec/requirements process.
A short mapping table at the end binds it to common host structures.
Why front-of-pipeline
A goal-backward verifier only checks assertions that exist; an assertion only exists for a requirement that was written down. A domain-boundary edge the author never surfaced is invisible to the verifier — and worse, the verifier is confidently wrong about it (measured: ~0.93 confidence while catching 0/12 omitted-edge defects, an expected calibration error of 0.81 — worse than a coin flip — versus 100% / 0.03 on edges the spec did state). The fix is not a better verifier or a confidence gate; confidence is uninformative across these regimes. The fix is spec completeness: surface the omitted edge into an explicit, checkable assertion before any code exists, after which the verifier reliably catches it.
The underlying techniques are classic and should be named as such — Boundary Value Analysis, Equivalence Partitioning, the Category-Partition method, and Metamorphic Relations; property-based testing (PBT) is the academic name for the held-out backstop. The literature applies these at the test layer (back of pipeline, generating checks). The differentiated move here is placement, not technique: apply the same edge taxonomy at the spec layer (front of pipeline, generating requirements), with the explicit goal of extending a verifier's reach. Recent LLM spec work corroborates the core finding that the spec layer is the measured weak point:
- SLD-Spec — program slicing + logical deletion (code→spec): arXiv 2509.09917
- Specine / AutoReSpec / SpecMind — spec alignment & postcondition inference
- CodeSpecBench (2604.12268), OSVBench (2504.20964), VERINA (2505.23135) — benchmarks showing models solve tasks far better than they generate precise behavioral specs
- LLM property-based tests for edge cases: arXiv 2510.25297 (PBT+EBT ≈ 81% edge detection)
Inputs
A list of requirements, each a { id, text, shapes? } record where text is a testable
statement and shapes is an optional author-supplied override of the data/behavior shape.
The five shapes are: numeric-range, collection, text, stateful, io. When
shapes is absent, a heuristic classifier proposes them from the requirement prose
(propose-then-confirm) — the author may correct the shape.
Taxonomy (8 categories)
Closed and small by design: a fixed eight the author must explicitly clear beats thirty nobody finishes. The failure mode being eliminated was never "too few categories" — it was that no taxonomy was systematically applied at all. Categories 1, 2, and 4 alone cover the three canonical corpus defects (banker's-rounding ties, touching intervals, grapheme truncation). Growth happens via optional domain packs, not by bloating the core.
| id | name (QA term) | applies to shapes | probe question |
|---|---|---|---|
| boundary | Boundary values | numeric-range | What happens exactly at each min/max/threshold — and one step either side? |
| adjacency | Adjacency / touching | collection | When two things are exactly equal or just touch, do they merge, collide, or separate? |
| empty | Empty / degenerate | collection, text | What is the result for empty, single-element, or null input? |
| encoding | Encoding / representation | text | Whose definition of length/equality applies — bytes, code points, grapheme clusters, or normalized form? |
| ordering | Ordering / stability | collection | When elements compare equal, is output order specified and stable? |
| precision | Precision / overflow | numeric-range | Where can precision loss or overflow occur, and what is the contract? |
| idempotency | Idempotency / repetition | stateful | What happens if this runs twice on the same input? |
| concurrency | Concurrency / effect ordering | stateful, io | If interrupted or run in parallel, what is guaranteed? |
Relevance filter + resolution states
Two rules keep the probe honest and prevent an "everything is N/A" failure mode:
- Relevance filter first. Classify each requirement's shape, then raise only the
categories whose
applies to shapesintersect that requirement's shapes. A pure-text requirement is never asked about overflow. This is what makes an unresolved edge meaningful: it is an edge that applies and was not addressed. - Dismissal requires a reason string. "N/A — input is a bounded enum, no boundary exists" is valid; silence is not. The reason string is the audit trail.
Each raised edge carries two orthogonal axes — a resolution lifecycle and, when resolved, a verification tier (ADR-550 Decision 7, the shared probe-core model):
- status —
resolved | dismissed | unresolved:- resolved — the edge is addressed; how it is addressed is the verification tier.
- dismissed — not applicable, accompanied by a required, non-empty reason string.
- unresolved — carried forward and flagged; the author chose not to resolve it yet.
- verification (only when
statusisresolved;nullotherwise) —explicit | backstop:- explicit — a checkable assertion for the edge is written (a SPEC acceptance criterion).
- backstop — a held-out / property-based test stands in for an edge the author knows but cannot fully articulate in prose (records intent; the test body is authored later).
Splitting these axes keeps the lifecycle enum free of a verification fact (the old single
enum smuggled covered/backstop — both resolved — into one flat list) and lets sibling
probes add their own verification tiers (e.g. test | judgment) without a parallel enum.
coverage.resolved is the count of closed edges — resolved + dismissed (the
pre-re-cut "covered + dismissed + backstop" set, count-preserved). An unresolved
applicable edge is the precise signal a soft completeness gate raises.
Output schema
The probe emits, per edge, an item of the form:
{ requirement_id, category, status, verification, resolution, reason, probe }
plus a coverage summary:
coverage: { applicable, resolved, unresolved, byVerification: { explicit, backstop } }
applicable is the number of raised edges, resolved = closed (resolved + dismissed)
status edges, unresolved is the remainder, and byVerification breaks the
resolved-status edges down by tier (probe-agnostic in core; the edge adapter declares
{ explicit, backstop }). This JSON is the stable contract both the reference
implementation and any third-party port emit.
Generic mapping (requirements → checks → verifier)
| Host structure | "requirement" | a resolved/explicit edge becomes |
a resolved/backstop edge becomes |
|---|---|---|---|
| GSD SPEC | a SPEC Requirement | an Acceptance Criterion that plan-phase lifts into must_haves.truths |
a non-inferable check in must_haves.truths (needs a held-out/PBT test) |
| Gherkin feature | a Scenario | an additional Then assertion / Scenario Outline row |
a tagged scenario routed to a property test |
| OpenAPI operation | an operation | a response/constraint example + schema rule | a contract/property test on the operation |
| Docstring contract | a documented behavior | an assertion in the contract test | a property-based test for the function |
The portable invariant: a resolved/explicit edge produces the unit your verifier
iterates over (GSD: a must_haves.truth); a resolved/backstop edge produces a test
added to that same set as a non-inferable check. An unresolved edge is an explicit
assumption the downstream planner must surface, not silently drop.
Worked example (merge-intervals)
Given a single requirement with no resolutions yet:
[{ "id": "R1", "text": "Merge a list of overlapping intervals into the minimal set" }]
the requirement classifies as a collection, which raises adjacency, empty, and
ordering (but not boundary/precision/encoding/idempotency/concurrency). With no
resolutions supplied, every applicable edge is unresolved:
{
"items": [
{ "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
{ "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
],
"coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
}
The adjacency row is the one that catches the canonical defect: [[1,2],[2,3]] intervals
that only touch — does the spec say they merge? Resolving it resolved/explicit writes
that assertion, and the verifier can then enforce it. This worked-example block is kept
byte-for-byte (parsed-JSON) identical to its fixture by edge-probe-docs-fixtures.test.cjs,
so the doc and the reference implementation cannot silently drift.
Worked example (round-half-even)
Given a requirement to round floating-point values using the banker's-rounding (half-even)
rule, the requirement classifies as numeric-range, which raises boundary and precision
(but not adjacency/empty/ordering/encoding/idempotency/concurrency):
{
"items": [
{ "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
{ "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss or overflow occur, and what is the contract?" }
],
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
}
The precision row surfaces the canonical defect for this requirement: IEEE 754 floating-point
arithmetic rounds 2.5 to 2 (not 3) under half-even — the probe asks whether the spec
states that contract explicitly, so the verifier can enforce it.
Worked example (truncate-graphemes)
Given a requirement to truncate a string to N grapheme clusters (not bytes or code points),
the requirement classifies as text, which raises empty and encoding:
{
"items": [
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
{ "requirement_id": "R1", "category": "encoding", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Whose definition of length/equality applies — bytes, code points, grapheme clusters, or normalized form?" }
],
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
}
The encoding row is the load-bearing one: a requirement that says "truncate to 10
characters" is ambiguous — the spec must state whether "character" means bytes, UTF-16
code units, Unicode code points, or grapheme clusters (emoji sequences are 1 grapheme,
multiple code points).
Worked example (money-rounding)
Given a requirement to round monetary amounts to two decimal places, the requirement
classifies as numeric-range, which raises boundary and precision:
{
"items": [
{ "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
{ "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss or overflow occur, and what is the contract?" }
],
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
}
The boundary row asks what the minimum and maximum representable values are (negative
amounts? fractional cents?). The precision row asks whether IEEE 754 binary rounding
can produce $0.30000000000000004 — the spec must commit to a representation.
Worked example (list-dedupe)
Given a requirement to deduplicate a list of items, the requirement classifies as
collection, which raises adjacency, empty, and ordering:
{
"items": [
{ "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
{ "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
],
"coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
}
The ordering row is the non-obvious one: dedupe removes duplicates, but which copy is
kept — first occurrence, last, or implementation-defined? The spec must commit.
Worked example (resolved-mixed)
The same merge-intervals requirement after a resolution session where adjacency was
resolved with an explicit acceptance criterion, ordering was dismissed, and empty was
left unresolved:
{
"items": [
{ "requirement_id": "R1", "category": "adjacency", "status": "resolved", "verification": "explicit", "resolution": "AC#6: touching intervals merge", "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
{ "requirement_id": "R1", "category": "ordering", "status": "dismissed", "verification": null, "resolution": null, "reason": "output is canonically sorted; no tie possible", "probe": "When elements compare equal, is output order specified and stable?" }
],
"coverage": { "applicable": 3, "resolved": 2, "unresolved": 1, "byVerification": { "explicit": 1, "backstop": 0 } }
}
coverage.resolved is 2 — the closed set (adjacency=resolved/explicit + ordering=dismissed);
unresolved is 1 (empty); byVerification.explicit is 1 (the single explicitly-verified
edge), backstop 0. The soft gate raises on this example because one applicable edge remains
unresolved — the author must either specify, dismiss, or backstop it before writing the SPEC.