Files
msd-core/docs/how-to/resolve-a-skipped-capability-probe.md
Tom Boucher 03b7125293 enhance(#3909): a probe that could not run no longer asserts a verdict (#3944)
* test(#3909): failing-first suite for the fabricated probe fallbacks

Binds the four fabrication sites found by executing the surfaces (ADR-3889
failure class (c)), each with a positive control so an over-firing fix goes red:

- the blocking api-coverage.verify-pre gate certifying "no external-API
  integration" from a zero-byte phase scope
- the assumption-delta query route scanning an unresolvable phase section as
  the empty string and reporting it as an examined negative
- both capability fragments' probe fallbacks, which append a fabricated
  verdict rather than replacing, and fire on the legitimate exit-1 negative

Verification runs on the remote runner.

Refs #3909

* enhance(#3909): a probe that could not run no longer asserts a verdict

ADR-3889 Phase 5. Four sites turned a failed or unexamined probe into a
confident negative; each now reports what it could not establish.

- check api-coverage.verify-pre: a phase with no plan body and no roadmap
  section ran detection over zero bytes and PASSED the blocking seal gate,
  certifying "no external-API integration" from input it never read. It now
  holds with scope_unavailable. The discriminator is bytes examined, never
  signals found, so a phase whose plans are real and simply carry no API
  vocabulary passes exactly as before.
- query assumption-delta scan: an unresolvable phase section was scanned as
  the empty string and reported as an examined negative. It now returns
  {skipped, reason: phase_unresolved}, still at exit 0 — an ADR-2980 degraded
  result in the payload, leaving the gsd-tools exit projection to P8.
- both capability fragments: `|| echo '{"detected":false}'` appended rather
  than replaced, and fired on the legitimate exit-1 negative, so a correct
  answer and an honest skip both arrived as two concatenated objects. They now
  keep the probe's own payload and manufacture only an explicit
  probe_unavailable skip when the probe produced nothing at all.

Every registered outcome is more restrictive on a blocking gate, so this can
turn a false green red and never a red green.

Docs: FEATURES 156, CONFIGURATION (both keys), references/api-coverage.md
seal-time outcome table, and a new how-to for the reason-code vocabulary.

Verification runs on the remote runner.

Closes #3909

* test(#3909): correct the stale unknown-phase assertion

`unknown phase → detected:false, no throw (graceful)` scanned phase 999
against a two-phase roadmap and asserted `detected === false`. That pinned
the fabrication as intended behavior: the phase does not exist, so the
detector was handed the empty string and its "no core assumption changed"
answer described nothing that was ever read.

It now asserts the skipped-with-reason shape. The graceful-degradation
contract the test was actually protecting — the query succeeds and does not
throw on an unknown phase — is unchanged.

Found by code review, not by the author.

Refs #3909

* docs(#3909): author the FEATURES entry in its generator source

`docs/FEATURES.md` is generated by `scripts/gen-features.cjs` from the
per-feature fragments in `docs/features/`. The API-coverage entry was edited
in the generated file, so the next regeneration silently dropped it.

The text now lives in `docs/features/api-coverage-gate.md` and
`docs/FEATURES.md` is regenerated from it, leaving the shipped file
byte-identical and its content actually derivable.

Caught by `lint:generated-sync`.

Refs #3909

* test(#3909): bind the skip to "not found", and pin the discriminator

The first verification run went red on one case, and the case was wrong
rather than the code.

`getRoadmapPhaseWithFallback` returns `null` for an unknown phase and for a
missing ROADMAP.md, but for a section whose body is whitespace-only it returns
the heading line alone — which is not empty. So a body-less section WAS found,
and reporting `detected:false` over its heading is a real negative, not a
fabrication. The test had assumed the resolver yielded `''` there.

Correcting the test rather than the resolver keeps `skipped` bound to the
distinction the issue asks for — found versus not found — and avoids diverging
`assumption-delta scan` from `roadmap.get-phase`, which the fragment documents
as sharing one resolver.

Also adds the seeded property the test matrix had promised: for any plan body,
the scope read back is whitespace-only exactly when the body was. That pins the
gate's discriminator to bytes examined, so it cannot quietly become "no signals
found", across unicode whitespace and CRLF.

`docs/INVENTORY.md` picks up the reference doc's new seal-time outcome table —
surfaced by the co-change gate, not by a lint failure.

Refs #3909

* chore(#3909): backfill the changeset PR number

Refs #3909

---------

Co-authored-by: sim <sim@local>
2026-08-27 15:50:12 -04:00

6.2 KiB

Resolve a skipped capability probe

A phase-scope probe — the API-coverage detector or the assumption-delta detector — needs real text to examine before it can assert a verdict. When it gets none, it says so instead of guessing. This page covers reading that signal and clearing it.

What you saw

One of two things, depending on which surface you hit:

  • The seal gate held your phase. verify:pre reported a block with scope_unavailable: true instead of the usual "no external-API integration detected" pass.
  • A planning checkpoint reported skipped instead of a verdict. The assumption-delta or api-coverage plan-time checkpoint printed {"skipped":true,"reason":"..."} and produced no decision either way.

Both are the same underlying fix (#3909): a probe that never examined real input used to fabricate detected:false, which reads as "nothing here" when the true answer is "nobody looked." Now it says which one happened.

The reason-code table

reason What it means What actually happened Remedy
scope_unavailable The seal-time gate found no phase scope at all No plan body (.planning/phases/<N>/*-PLAN.md) and no ROADMAP section for the phase Add the plan or roadmap section, or write a reasoned No external API integration: <reason> declaration to COVERAGE.md
phase_unresolved The assumption-delta scan query could not resolve a phase section to scan No ROADMAP.md, an unknown phase number, or a phase section with no body If you expected a real scan, fix the phase reference or roadmap section; otherwise no action — the checkpoint correctly stays silent
probe_unavailable The detector process itself produced no output The probe crashed, was not found, or its stdout was empty for a reason unrelated to input content Check that gsd-core/bin/lib/api-coverage.cjs or gsd-core/bin/lib/assumption-delta.cjs runs standalone; re-run the checkpoint once the probe itself is healthy
no_input The detector ran but stdin was empty or whitespace-only The phase scope resolved to nothing (empty plan body and empty roadmap section) Same as scope_unavailable — give the detector something to read
stdin_error The detector could not read stdin at all A pipe/read failure upstream of the detector, not an empty-input case Re-run; if it recurs, the caller constructing $SCOPE is the thing to fix, not the detector

The seal gate held my phase

  1. Confirm the reason directly:

    gsd_run check api-coverage.verify-pre <phase> --raw
    

    Look for "scope_unavailable": true in the output. That confirms this is the fail-closed arm, not a real "integration detected" block.

  2. Check whether the phase actually has scope to read:

    ls .planning/phases/<N>/*-PLAN.md 2>/dev/null
    gsd_run query roadmap.get-phase <phase>
    

    If both come back empty, the gate is correct — there is genuinely nothing for the detector to examine.

  3. Resolve it one of two ways:

    • Add the missing scope. Write the phase plan, or add the phase's section to ROADMAP.md, then re-run detection.

    • Record the reasoned declaration anyway. If the phase truly has no plan body worth writing (rare), put the human decision directly in COVERAGE.md:

      No external API integration: <one-line reason>.
      

      This is the same reasoned overrule the gate already accepts when a detector does find (or falsely flags) a signal — see gsd-core/references/api-coverage.md.

  4. Re-run the gate to confirm it clears:

    gsd_run check api-coverage.verify-pre <phase> --raw
    

Turning the gate off does not answer the question. Setting workflow.api_coverage_gate: false in .planning/config.json silences the hold, but the phase's API surface is still undecided — you have just stopped being told. Prefer resolving the scope or writing the declaration.

A checkpoint reported skipped

The assumption-delta and api-coverage plan-time checkpoints are advisory: when they cannot resolve a phase section, they skip rather than fire, and that is correct, non-blocking behavior. You do not need to do anything except notice that the phase was not actually cleared by a real scan — a skipped payload carries no detected key and is not the same as "no signal found." If you expected a real scan and got a skip instead, treat it like the phase_unresolved row above: check that the phase reference and roadmap section actually exist.

Why this is not just a stricter gate

A probe reporting detected:false from an input it never read is a false negative on a blocking gate — the one direction a gate must never fail silently. A false positive here costs one line in COVERAGE.md; a false negative lets a real external-API integration seal with an undecided surface, discovered later by a user who reasonably expected it to work. This change can turn a false green red. It can never turn a red green — every phase that passed because a signal was genuinely absent from scope the detector actually read still passes, unchanged.

Telling "nothing to report" apart from "could not look"

This is the recurring distinction across this doc set (see also Resolve unreachable-guard findings and Consume the planning snapshot). A verdict of detected: false or passed: true means the detector examined real text and found nothing. A skipped payload or a scope_unavailable block means the detector examined nothing at all. Only the first is good news; the second is a request for more scope, not a clean bill of health.