* test(#1956): failing-first contract for cross-artifact fact-drift pass * feat(#1956): flag cross-artifact fact drift in the plan drift guard * fix(#1956): correct config-key assertion and bidirectional lifecycle-lag exemption * docs(#1956): document the cross-artifact axis in the architecture reference * feat(#1956): decide the phase-status drift axis deterministically * fix(#1956): scope the progress-table lookup, abstain without a position section, rank deferred * docs(#1956): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -254,6 +254,51 @@ Run this pass unless `plan_review.source_grounding` is `false`. It verifies ever
|
||||
- Signature mismatches cannot be asserted under `grep`/`intel`; report the signature as UNCHECKABLE.
|
||||
5. **Coverage block.** Append a "Verification coverage" section to `REVIEWS.md` listing every UNCHECKABLE/skipped symbol and why — a clean review must never silently mean "nothing was checked."
|
||||
|
||||
### Cross-artifact fact-drift pass (same gate: `plan_review.source_grounding`)
|
||||
|
||||
Run this pass whenever the source-grounding pass ran — it is the second axis of the same drift guard, gated by the same `plan_review.source_grounding` key and adding no config surface of its own. Where source-grounding asks *"does this symbol exist in the source?"*, this asks *"does the project state the same fact in two planning artifacts, and do the two disagree?"* Because each phase runs in a fresh context, an agent typically reads only one artifact and trusts it, so a stale duplicate silently steers it wrong.
|
||||
|
||||
**Key on knowledge, not on similar text.** DRY is about a single authoritative representation of a piece of *knowledge*. Two passages that merely read alike, or that restate one fact at different levels of detail, are NOT drift. Only a contradiction is.
|
||||
|
||||
1. **Phase status — decided by the seam, not by judgment.** Do not eyeball this axis:
|
||||
|
||||
```bash
|
||||
DRIFT=$(gsd_run drift-guard phase-status --phase "${PHASE}")
|
||||
# $DRIFT is JSON: {"verdict":"consistent|lag|drifted|uncheckable","stateStatus":…,"roadmapStatus":…}
|
||||
```
|
||||
|
||||
- `drifted` — STATE.md and ROADMAP.md contradict each other. Report it; the authority is STATE.md.
|
||||
- `lag` — one lifecycle step apart between non-terminal statuses. NOT a finding.
|
||||
- `consistent` — nothing to report.
|
||||
- `uncheckable` — a document was absent or carried a status outside both vocabularies. Record it in the coverage block; never read it as consistent.
|
||||
|
||||
Completeness is terminal: when exactly one side says the phase is complete, the verdict is `drifted` and never `lag`, however few steps apart the two words look.
|
||||
|
||||
2. **Pair up the remaining facts by judgment.** The authority column names the source of truth, so a finding can say which side to keep:
|
||||
|
||||
| Fact class | Artifact pair | Authority | Decided by |
|
||||
|---|---|---|---|
|
||||
| Success criteria / must-have truths | ROADMAP.md Success Criteria ↔ PLAN.md `must_haves.truths` | ROADMAP.md | judgment |
|
||||
| Requirement IDs | ROADMAP.md `**Requirements:**` ↔ PLAN.md task requirement refs | ROADMAP.md | judgment |
|
||||
| Phase status | STATE.md status ↔ ROADMAP.md phase state | STATE.md | step 1 (deterministic) |
|
||||
| Glossary / domain term | CONTEXT.md `Decisions` ↔ PLAN.md usage of the term | CONTEXT.md | judgment |
|
||||
|
||||
3. **Judge each judgment pair.** FLAG only when ALL THREE hold:
|
||||
|
||||
1. both sides name the *same* fact — same requirement ID, same success criterion, or the same defined term; and
|
||||
2. the two representations *contradict*, one asserting what the other denies, rather than differing in wording or in level of detail; and
|
||||
3. the pair is one of the judgment pairs above.
|
||||
|
||||
4. **Record.** Emit each finding into `REVIEWS.md` beside the source-grounding coverage block, quoting both locations and naming the divergence and the authority, so the author can collapse the two copies to a single source of truth.
|
||||
|
||||
**Do NOT flag:** a wording-only difference that asserts the same thing; a fact that appears in one artifact only — single-source is the target state, not a finding; a PLAN that ADDS a truth beyond the roadmap Success Criteria, which is sanctioned (plans may add, never subtract); a `lag` verdict from step 1 — two non-terminal statuses a single lifecycle step apart, in either direction, since STATE.md is written at planning time independently of ROADMAP.md and can lead as readily as trail (a disagreement about *completion* is never lag, and step 1 already reports it as `drifted`); anything under CONTEXT.md's `Claude's Discretion` or `Deferred Ideas`, which are non-authoritative by design.
|
||||
|
||||
**Report once, not twice — these belong to `gsd-plan-checker`:** a PLAN that omits a roadmap Success Criterion is scope reduction (Dimension 7b); a requirement ID the ROADMAP never defines is requirement coverage (Dimension 1); two PLAN.md files in one phase disagreeing is cross-plan data contracts (Dimension 9).
|
||||
|
||||
**Severity: advisory, never a blocker.** This pass never sets `hardBlock`, and its findings contribute to neither `HIGH_COUNT` nor `ACTIONABLE_COUNT` — a project carrying pre-existing drift must still be able to converge, or an advisory check becomes an endless replan loop.
|
||||
|
||||
**Coverage, never silence.** If STATE.md or CONTEXT.md is absent, that axis is skipped and the skip is recorded in the same "Verification coverage" block. A clean pass must never mean "nothing was compared."
|
||||
|
||||
After agent returns, verify REVIEWS.md exists:
|
||||
```bash
|
||||
REVIEWS_FILE=$(ls ${phase_dir}/${padded_phase}-REVIEWS.md 2>/dev/null)
|
||||
|
||||
Reference in New Issue
Block a user