* refactor(#1190): extract ADR-22 drift-guard decision logic into a testable seam ADR-22's severity mapping, authority auto-upgrade, and rung>=3 hard-block lived only as prose in plan-review-convergence.md — untestable. Extracted into src/plan-drift-guard.cts (pure: AUTHORITY_RUNGS, getEffectiveAuthority, classifyDriftSeverity) + a gsd-tools drift-guard CLI seam (authority/severity), and rewired the workflow to call the seam deterministically instead of reasoning the decision in prose. 47 unit/e2e/structural tests cover the full severity table, the grep->intel auto-upgrade, and the rung>=3 HIGH hard-block. Closes #1190 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1190): add changeset for ADR-22 drift-guard seam (#1242) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1190): register ADR-22 module in eslint-ignore + inventory manifest + docs-exempt changeset Full-matrix CI surfaced new-module/command governance ripples beyond the lint-tests chain: (1) tsc-generated plan-drift-guard.cjs must be in the eslint ignore list (551-eslint-bin-lib-coverage); (2) docs/INVENTORY-MANIFEST.json must include the new module/command (regen via gen-inventory-manifest.cjs --write); (3) a type:Added changeset triggers docs-required — added a docs-exempt marker (internal seam, no user-facing surface). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,13 @@
|
||||
* Returns JSON { valid, errors[], slots: {role,capability,outcome} | null }
|
||||
* --pick valid Emit bare boolean (for workflow boolean checks)
|
||||
*
|
||||
* Drift Guard (ADR-22):
|
||||
* drift-guard authority Resolve effective source-grounding authority
|
||||
* (reads plan_review.source_grounding_authority + intel.enabled from config)
|
||||
* drift-guard severity --status <S> Classify a symbol verdict into { severity, hardBlock }
|
||||
* [--authority <A>] Status: VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE
|
||||
* Authority: grep|intel|treesitter|lsp|scip (default: config-resolved)
|
||||
*
|
||||
* Validation:
|
||||
* validate consistency Check phase numbering, disk/roadmap sync
|
||||
* validate health [--repair] Check .planning/ integrity, optionally repair
|
||||
@@ -227,6 +234,7 @@ const { routeCheckCommand } = require('./lib/check-command-router.cjs');
|
||||
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
|
||||
const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
|
||||
const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
|
||||
const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-drift-guard.cjs');
|
||||
|
||||
// ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
|
||||
// Non-family commands now run through their CJS handlers directly. Keep the
|
||||
@@ -510,7 +518,7 @@ async function main() {
|
||||
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
||||
'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
|
||||
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
|
||||
'current-timestamp, detect-custom-files, docs-init, effort, extract-messages, find-phase, ' +
|
||||
'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
||||
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
||||
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
||||
'capability, classify-confidence, git, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
||||
@@ -2097,6 +2105,67 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'drift-guard': {
|
||||
// ADR-22: deterministic authority resolution + severity classification.
|
||||
// Subcommands:
|
||||
// drift-guard authority → effective authority string
|
||||
// drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
|
||||
const subcommand = args[1];
|
||||
|
||||
// Read config.json directly for both plan_review.source_grounding_authority
|
||||
// and intel.enabled. Neither key is in the config-loader.cjs whitelist that
|
||||
// core.loadConfig() returns; plan_review is only in config.cjs's private
|
||||
// buildConfig(), and intel is a federated capability config key.
|
||||
let configuredAuthority = 'grep';
|
||||
let intelEnabled = false;
|
||||
try {
|
||||
const { planningDir } = require('./lib/planning-workspace.cjs');
|
||||
const cfgPath = require('path').join(planningDir(cwd), 'config.json');
|
||||
if (require('fs').existsSync(cfgPath)) {
|
||||
const rawCfg = JSON.parse(require('fs').readFileSync(cfgPath, 'utf-8'));
|
||||
if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
|
||||
configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
|
||||
}
|
||||
if (rawCfg && rawCfg.intel && rawCfg.intel.enabled === true) {
|
||||
intelEnabled = true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// not fatal — defaults apply
|
||||
}
|
||||
|
||||
const effectiveAuthority = getEffectiveAuthority(configuredAuthority, intelEnabled);
|
||||
|
||||
if (subcommand === 'authority') {
|
||||
// Pass rawValue as 3rd arg so --raw returns unquoted string (not JSON)
|
||||
core.output(effectiveAuthority, raw, effectiveAuthority);
|
||||
break;
|
||||
}
|
||||
|
||||
if (subcommand === 'severity') {
|
||||
const statusIdx = args.indexOf('--status');
|
||||
const statusVal = statusIdx !== -1 ? args[statusIdx + 1] : undefined;
|
||||
if (!statusVal || statusVal.startsWith('--')) {
|
||||
error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
||||
break;
|
||||
}
|
||||
const authIdx = args.indexOf('--authority');
|
||||
const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
|
||||
const authorityForClassify = (authVal && !authVal.startsWith('--'))
|
||||
? authVal
|
||||
: effectiveAuthority;
|
||||
const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
|
||||
core.output(result, raw);
|
||||
break;
|
||||
}
|
||||
|
||||
error(
|
||||
`Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
|
||||
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// ADR-959: try capability-registry dispatch before emitting the unknown-command error.
|
||||
// An unmigrated command still hits its hardcoded `case` above — untouched.
|
||||
|
||||
@@ -185,13 +185,23 @@ Run this pass unless `plan_review.source_grounding` is `false`. It verifies ever
|
||||
|
||||
1. **Enumerate cited symbols.** List every referenced symbol by kind, quoting the plan line for each (coverage must be auditable): decorators (`@name`), classes/methods (`Class.method`), functions (`module.function`), CLI flags (`--name`), file paths, dataclass/struct fields.
|
||||
2. **Exclude new artifacts.** Do NOT verify symbols the plan declares under its "Artifacts this phase produces" section — those are created by this phase, not references to existing code.
|
||||
3. **Resolve each remaining symbol** using the adapter named by `plan_review.source_grounding_authority` (default `grep`):
|
||||
3. **Resolve each remaining symbol** using the effective authority adapter (resolved deterministically — see step 4a):
|
||||
- `grep` — ripgrep / Read the source; confirm the name appears as a real declaration.
|
||||
- `intel` — consult `.planning/intel/API-SURFACE.md` / `api-map.json` (only when `intel.enabled`).
|
||||
Record one verdict per symbol: **VERIFIED** (quote `file:line`), **MISSING** (adapter can check this language/kind and the symbol is absent), **AMBIGUOUS** (multiple candidates), or **UNCHECKABLE** (adapter cannot analyze this language/kind — e.g. non-JS under `intel`, or any signature under `grep`). Never treat UNCHECKABLE as verified or missing.
|
||||
4. **Severity & gating:**
|
||||
- **MISSING** at authority `grep`/`intel` → `needs-acknowledgement`: the plan proceeds only if the author confirms the symbol is genuinely new or dynamically resolved, and that acknowledgement is recorded. A hard block is reserved for higher-authority adapters (LSP/SCIP) that can prove absence.
|
||||
- **AMBIGUOUS** → MEDIUM. **UNCHECKABLE** → INFO.
|
||||
4a. **Resolve effective authority** (deterministic — replaces manual `intel.enabled` reasoning):
|
||||
```bash
|
||||
EFFECTIVE_AUTHORITY=$(gsd_run drift-guard authority --raw)
|
||||
```
|
||||
4. **Severity & gating** — classify each symbol's verdict using the seam (do not apply the table manually):
|
||||
```bash
|
||||
# For each symbol, e.g.:
|
||||
RESULT=$(gsd_run drift-guard severity --status <verdict> --authority "$EFFECTIVE_AUTHORITY")
|
||||
# $RESULT is JSON: {"severity":"…","hardBlock":true|false}
|
||||
```
|
||||
- `hardBlock: true` (HIGH at authority `lsp`/`scip`) — stops the review cycle immediately; do not proceed until the plan author resolves the missing symbol.
|
||||
- `hardBlock: false`, severity `needs-acknowledgement` — plan proceeds only if the author confirms the symbol is genuinely new or dynamically resolved, and that acknowledgement is recorded.
|
||||
- `AMBIGUOUS` → MEDIUM. `UNCHECKABLE` → INFO.
|
||||
- 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."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user