* test(#3604): pin parity-dependent ref visibility in the glossary gate * fix(#3604): make glossary ref visibility independent of backtick parity * chore(#3604): regenerate CONTEXT-INDEX for corrected predicates * chore(#3604): regenerate examples CONTEXT-INDEX for corrected predicates * fix(#3604): complete retired-family exemptions and pin the guard rails * chore(#3604): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/nimble-quails-fly.md
Normal file
5
.changeset/nimble-quails-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3680
|
||||
---
|
||||
**`check-glossary-refs` no longer reports a false clean** — backtick-pairing parity let stale file references in CONTEXT.md hide behind RULESET predicate lines, so renamed files stayed invisible to the drift gate. Visibility is now structural (per-line pairing + predicate-value harvesting), the renamed test reference is corrected, and retired-file mentions are exempted by name. (#3604)
|
||||
@@ -407,7 +407,7 @@ Module owning the code-intelligence store: tri-state capability gate (`isCapabil
|
||||
The GSD-RESEARCH capability behind an L2-hybrid seam: code owns cache + provider policy + package legitimacy; MCP owns the actual fetch. Reachable via `gsd-tools query research-plan|research-store|package-legitimacy`. Source: `src/research-{store,provider}.cts` + `src/package-legitimacy.cts` (generated to `gsd-core/bin/lib/*.cjs` per ADR-457). Replaces the prose provider-waterfall duplicated across the researcher agents and the pip-install `slopcheck` bolt-on.
|
||||
|
||||
- `GSD-RESEARCH.MODULE.research-store=content-addressed cache; key=sha256(ecosystem+library+version+query+kind); getResearch->{hit,stale} never throws (mirrors graphify staleness); ttlForSource curated HIGH 30d|MED 7d|web LOW 1d; tiers: curated-doc kinds -> ~/.gsd/research-cache (cross-project), web/synthesis -> project .planning/research/.cache`
|
||||
- `GSD-RESEARCH.MODULE.research-provider=single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in docs/web discovery)`
|
||||
- `GSD-RESEARCH.MODULE.research-provider=single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in the docs or web legs)`
|
||||
- `GSD-RESEARCH.MODULE.package-legitimacy=registry-API verdicts (npm/PyPI/crates.io injectable adapters) computed from thresholds {minAgeDays:30,minWeeklyDownloads:1000,requireRepo:true}; verdict OK|SUS|SLOP per package; slopcheck=optional adapter that can only escalate, never the install-or-degrade gate`
|
||||
- `GSD-RESEARCH.INTEGRATION.L2-hybrid=code owns cache+legitimacy+confidence+provider-pick (gsd-tools query research-plan/research-store/package-legitimacy); MCP owns the fetch; agent returns RESEARCH.md path, never raw fetches`
|
||||
- `GSD-RESEARCH.PROVIDER.availability=config flags brave_search/exa_search/firecrawl/tavily_search/ref_search/perplexity/jina (env <X>_API_KEY or ~/.gsd/<x>_api_key); context7/jina/websearch always available; planResearch falls through waterfall to websearch terminal`
|
||||
@@ -547,7 +547,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr
|
||||
`PROHIB.descriptor.shape=5 FLAT scalars (check_kind,check_target,check_rule,check_violation_fixture,check_clean_fixture) — NEVER a nested check:{} (parseMustHavesBlock is a flat parser, src/frontmatter.cts)`
|
||||
`PROHIB.rail=core verify rail, non-toggleable (ADR-857 verification-substrate boundary / decision #6); the verifier<->predicate contract is NOT an off-by-default capability`
|
||||
`PROHIB.judgment-tier=never-silent / never-hard-halt soft gate; autonomous emits "unverified-prohibition — human review recommended" (exogenous grading, ADR-550 D4)`
|
||||
`PROHIB.enforce.adr=docs/adr/1606 (verify-time enforcement seam) + docs/adr/550 (spec-phase contract)`
|
||||
`PROHIB.enforce.adr=docs/adr/1606-prohibition-enforcement-verify-seam.md (verify-time enforcement seam) + docs/adr/550-spec-phase-probe-contract.md (spec-phase contract)`
|
||||
|
||||
---
|
||||
|
||||
@@ -644,7 +644,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr
|
||||
`WORKTREE.SEAM.invariant=parser failure must degrade to metadata_prune_only and never escalate to destructive removal`
|
||||
`WORKTREE.SEAM.inventory-interface=[listLinkedWorktreePaths, inspectWorktreeHealth]`
|
||||
`WORKTREE.SEAM.caller-rule=verify.cjs must consume inspectWorktreeHealth for W017 classification; no ad-hoc porcelain parsing in callers`
|
||||
`WORKTREE.SEAM.test-anchor-w017=tests/orphan-worktree-detection.test.cjs + tests/worktree-safety-policy.test.cjs`
|
||||
`WORKTREE.SEAM.test-anchor-w017=tests/orphan-worktree-detection.test.cjs + tests/worktree-safety.test.cjs`
|
||||
`WORKTREE.SEAM.inventory-snapshot=snapshotWorktreeInventory(repoRoot,{staleAfterMs,nowMs}) is canonical linked-worktree health snapshot for callers`
|
||||
`PLANNING.PATH.PARITY.project-scope=.planning/<project> (never .planning/projects/<project>); mirror planning-workspace.cjs planningDir()`
|
||||
`PLANNING.PATH.SEAM.helpers=helpers.planningPaths delegates to workspacePlanningPaths + resolveWorkspaceContext; precedence explicit-ws > env-ws > env-project > root`
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
{
|
||||
"id": "GSD-RESEARCH.MODULE.research-provider",
|
||||
"klass": "GSD-RESEARCH",
|
||||
"value": "single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in docs/web discovery)"
|
||||
"value": "single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in the docs or web legs)"
|
||||
},
|
||||
{
|
||||
"id": "GSD-RESEARCH.MODULE.research-store",
|
||||
@@ -692,7 +692,7 @@
|
||||
{
|
||||
"id": "PROHIB.enforce.adr",
|
||||
"klass": "PROHIB",
|
||||
"value": "docs/adr/1606 (verify-time enforcement seam) + docs/adr/550 (spec-phase contract)"
|
||||
"value": "docs/adr/1606-prohibition-enforcement-verify-seam.md (verify-time enforcement seam) + docs/adr/550-spec-phase-probe-contract.md (spec-phase contract)"
|
||||
},
|
||||
{
|
||||
"id": "PROHIB.enforce.causation",
|
||||
@@ -1317,7 +1317,7 @@
|
||||
{
|
||||
"id": "WORKTREE.SEAM.test-anchor-w017",
|
||||
"klass": "WORKTREE",
|
||||
"value": "tests/orphan-worktree-detection.test.cjs + tests/worktree-safety-policy.test.cjs"
|
||||
"value": "tests/orphan-worktree-detection.test.cjs + tests/worktree-safety.test.cjs"
|
||||
},
|
||||
{
|
||||
"id": "WORKTREE.SEAM.test-anchors",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -65,7 +65,7 @@ const TRACKED_PREFIXES = [
|
||||
const TRACKED_EXACT = new Set(['bin/install.js', 'package.json']);
|
||||
|
||||
/**
|
||||
* Paths CONTEXT.md documents whose ABSENCE is the healthy steady state (#2778).
|
||||
* Paths CONTEXT.md documents whose ABSENCE is the healthy steady state.
|
||||
*
|
||||
* `TRACKED_PREFIXES` skips claims this gate *cannot* check. This is the narrower
|
||||
* third case: a claim it must not check, because "does not exist" is the correct
|
||||
@@ -78,14 +78,43 @@ const TRACKED_EXACT = new Set(['bin/install.js', 'package.json']);
|
||||
* shipping an empty stub. So the file is absent on a healthy `next` and present
|
||||
* only inside a PR that needs it, and asserting either way is wrong.
|
||||
*
|
||||
* Until #2778 this passed only by accident: CONTEXT.md's `RULESET.` entries are
|
||||
* themselves backtick-wrapped and contain backticks, so the sequential pairing in
|
||||
* `extractTrackedRefs` happened to leave this token outside a code span. Any edit
|
||||
* that shifted the parity — such as #2778's own — exposed it. A gate that passes
|
||||
* by luck is not passing; naming the exemption makes the intent explicit and
|
||||
* survives the next edit.
|
||||
* The emitted-attribution family (`tests/fixtures/golden-install-parity`,
|
||||
* `tests/golden-install-parity.test.cjs`, `scripts/gen-golden-install-parity-zcode.cjs`,
|
||||
* `tests/agent-size-baseline.json`, `tests/workflow-size-baseline.json`,
|
||||
* `scripts/git-merge-regen-driver.cjs`, `scripts/update-size-baseline.cjs`) was RETIRED
|
||||
* by the #2724 cutover — the differential attribution check replaced the committed
|
||||
* baselines and their generator/bridge tooling. CONTEXT.md's RULESET.EMITTED_ATTRIBUTION
|
||||
* predicate documents that retirement ("Historically …"), so the mentions are history,
|
||||
* not live claims, and their absence is exactly the healthy state the retirement
|
||||
* produced. The whole documented family is listed, not just the members today's tick
|
||||
* parity happens to hide — the two tooling paths were invisible only because of where
|
||||
* line 585's inner backticks sat, which is the #2778 luck this gate must not rely on.
|
||||
*
|
||||
* `scripts/eslint-rules` appears only inside a CONTRASTIVE mention ("the local
|
||||
* plugin lives at `eslint-rules/` (repo root, NOT `scripts/eslint-rules/`)") — the
|
||||
* predicate asserts where the directory is NOT, so non-existence is the claim
|
||||
* being made, not drift away from one.
|
||||
*
|
||||
* Until #2778 this set's first entry passed only by accident: CONTEXT.md's
|
||||
* `RULESET.` entries are themselves backtick-wrapped and contain backticks, so the
|
||||
* sequential pairing in `extractTrackedRefs` happened to leave this token outside a
|
||||
* code span. Any edit that shifted the parity — such as #2778's own — exposed it.
|
||||
* A gate that passes by luck is not passing; naming the exemption makes the intent
|
||||
* explicit and survives the next edit. #3604 removed the luck itself (pairing is
|
||||
* per line), which is what surfaced the entries above: each names a path whose
|
||||
* absence is deliberate, so each is exempted by name for the same reason.
|
||||
*/
|
||||
const INTENTIONALLY_ABSENT = new Set(['tests/emitted-drift-ack.json']);
|
||||
const INTENTIONALLY_ABSENT = new Set([
|
||||
'tests/emitted-drift-ack.json',
|
||||
'tests/fixtures/golden-install-parity',
|
||||
'tests/golden-install-parity.test.cjs',
|
||||
'scripts/gen-golden-install-parity-zcode.cjs',
|
||||
'tests/agent-size-baseline.json',
|
||||
'tests/workflow-size-baseline.json',
|
||||
'scripts/git-merge-regen-driver.cjs',
|
||||
'scripts/update-size-baseline.cjs',
|
||||
'scripts/eslint-rules',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Shape a backticked token must have to even be considered a path candidate:
|
||||
@@ -119,18 +148,51 @@ function isWithinRoot(token) {
|
||||
/**
|
||||
* Every distinct, trackable file-path token referenced in `text`, with any
|
||||
* trailing `:<line>` suffix stripped.
|
||||
*
|
||||
* #3604: pairing is per LINE, not over the whole text. A single whole-text pass
|
||||
* `[^`]+` crosses newlines, so one odd-backtick line (the `RULESET.*` predicate
|
||||
* format is backtick-wrapped and its values sometimes contain backticks) shifted
|
||||
* the pairing of every later line — a tracked token's visibility depended on
|
||||
* where it sat, which is how a renamed test file stayed invisible for weeks.
|
||||
*
|
||||
* The fact-store predicate lines (one `CLASS.subkey=value` fact per line,
|
||||
* backtick-wrapped as a whole) carry REAL paths in their values; the span itself
|
||||
* is not path-shaped (spaces, `=`), so a second pass harvests path-shaped tracked
|
||||
* tokens from inside any predicate-shaped span rather than letting the outer
|
||||
* wrapper hide them.
|
||||
*
|
||||
* Fragment guards: a real path in this repo never ends in `-` or `.` — those are
|
||||
* remnants of glob/template mentions (`scripts/gen-*.cjs`, `tests/foo.*.test.cjs`)
|
||||
* split at the `*` — and `NNNN` is the ADR filename template token
|
||||
* (CONTRIBUTING's "Do not compute a next number locally"), never a real path.
|
||||
*/
|
||||
function extractTrackedRefs(text) {
|
||||
const tokens = new Set();
|
||||
const re = /`([^`]+)`/g;
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
const raw = m[1];
|
||||
if (!PATH_TOKEN_RE.test(raw)) continue;
|
||||
const add = (raw) => {
|
||||
if (!PATH_TOKEN_RE.test(raw)) return;
|
||||
const token = raw.replace(/:\d+$/, '');
|
||||
if (!isTracked(token)) continue;
|
||||
if (!isWithinRoot(token)) continue;
|
||||
// Fragment guard, on the EMITTED token (after the :line strip, so
|
||||
// `src/foo-:12` is judged on `src/foo-`): glob/template remnants end in `-`
|
||||
// or `.`, a real path in this repo never does.
|
||||
if (!/[A-Za-z0-9_]$/.test(token)) return;
|
||||
if (token.includes('NNNN')) return;
|
||||
if (!isTracked(token)) return;
|
||||
if (!isWithinRoot(token)) return;
|
||||
tokens.add(token);
|
||||
};
|
||||
const subTokenRe = /[\w.-]+(?:\/[\w.-]+)*/g;
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const re = /`([^`]+)`/g;
|
||||
let m;
|
||||
while ((m = re.exec(line)) !== null) {
|
||||
const span = m[1];
|
||||
if (PATH_TOKEN_RE.test(span)) {
|
||||
add(span);
|
||||
continue;
|
||||
}
|
||||
if (!/^[A-Z][A-Za-z0-9_.-]*=/.test(span)) continue;
|
||||
for (const sub of span.matchAll(subTokenRe)) add(sub[0]);
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@@ -233,3 +233,118 @@ test('the real script runs cleanly against the real repo without crashing', () =
|
||||
assert.equal(res.error, undefined, `spawn must not error: ${res.error}`);
|
||||
assert.ok(res.status === 0 || res.status === 1, `expected exit 0 or 1, got ${res.status} (stderr: ${res.stderr})`);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #3604 — backtick-pairing parity. extractTrackedRefs paired backticks in one
|
||||
// whole-text pass, so (a) an odd-backtick line (the RULESET.* predicate format
|
||||
// is backtick-wrapped and its values sometimes contain backticks) shifted the
|
||||
// pairing of every later line, and (b) the fact-store predicate lines wrap the
|
||||
// whole `CLASS.subkey=value` in ONE pair, hiding the real tests/… paths inside
|
||||
// the span's value. A broken ref the gate cannot see is a gate that reports a
|
||||
// false clean — the exact luck-based pass #2778's docstring warned about.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('a broken tracked reference after a RULESET predicate with inner backticks is caught (#3604)', (t) => {
|
||||
// The predicate line carries an ODD backtick count (outer pair + one inner
|
||||
// unpaired tick), which desynchronized the old whole-text pairing. The broken
|
||||
// ref on a LATER line must still be found.
|
||||
const context = [
|
||||
'# Context',
|
||||
'',
|
||||
"`RULESET.TESTS.inner=this value breaks ` pairing for everything after it`",
|
||||
'',
|
||||
'Coverage lives in `src/does-not-exist.cts`.',
|
||||
'',
|
||||
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
|
||||
'',
|
||||
].join('\n');
|
||||
const root = makeRepo(t, { contextBody: context });
|
||||
|
||||
const lf = run(root, ['--check']);
|
||||
assert.equal(lf.status, 1, 'a broken ref after a desynchronizing RULESET predicate must fail --check');
|
||||
|
||||
// CRLF replay: the same verdict under \r\n (recurring class #1658/#1668/#2206).
|
||||
const rootCrlf = makeRepo(t, { contextBody: context.replace(/\n/g, '\r\n') });
|
||||
const crlf = run(rootCrlf, ['--check']);
|
||||
assert.equal(crlf.status, 1, 'CRLF input must yield the same verdict as LF');
|
||||
});
|
||||
|
||||
test('tracked paths inside an outer-wrapped predicate span are extracted (#3604)', (t) => {
|
||||
// The line-647 shape: one backtick pair wraps the whole predicate, and the
|
||||
// VALUE carries two real paths. The resolving one must pass; the missing one
|
||||
// must be named. The old extractor read the whole span as one token (spaces
|
||||
// → rejected) and saw neither.
|
||||
const context = [
|
||||
'# Context',
|
||||
'',
|
||||
'`WORKTREE.SEAM.demo-anchor=src/real-module.cts + tests/missing-anchor.test.cjs`',
|
||||
'',
|
||||
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
|
||||
'',
|
||||
].join('\n');
|
||||
const root = makeRepo(t, { contextBody: context });
|
||||
|
||||
const res = run(root, ['--check']);
|
||||
assert.equal(res.status, 1, 'a missing path inside a predicate value must fail --check');
|
||||
assert.match(`${res.stdout}${res.stderr}`, /tests\/missing-anchor\.test\.cjs/,
|
||||
'the finding must name the missing token, not the enclosing span');
|
||||
assert.doesNotMatch(`${res.stdout}${res.stderr}`, /real-module/,
|
||||
'the resolving sibling in the same span must not be reported');
|
||||
});
|
||||
|
||||
test('glob and NNNN template mentions are not drift (#3604)', (t) => {
|
||||
// `scripts/gen-*.cjs`-style globs and the documented ADR filename template
|
||||
// (`docs/adr/NNNN-*.md`, CONTRIBUTING's "Do not compute a next number
|
||||
// locally") are legitimate mentions. A fragment ending in `-` or `.` is a
|
||||
// glob/template remnant, never a real path. The bare template shape
|
||||
// (`docs/adr/NNNN.md`, no dash-star) ends in a word char and ends the
|
||||
// final-char guard's reach — only the NNNN skip covers it.
|
||||
const context = [
|
||||
'# Context',
|
||||
'',
|
||||
'`RULESET.AUDIT.demo=search src/*.cts OR the scripts/gen-*.cjs generator`',
|
||||
'`RULESET.ADR-HEADER=every docs/adr/NNNN-*.md must open with Status and Date`',
|
||||
'A fresh ADR starts from `docs/adr/NNNN.md`.',
|
||||
'Retired scanners were `scripts/lint-*.cjs`.',
|
||||
'',
|
||||
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
|
||||
'',
|
||||
].join('\n');
|
||||
const root = makeRepo(t, { contextBody: context });
|
||||
|
||||
const res = run(root, ['--check']);
|
||||
assert.equal(res.status, 0, `glob and NNNN template fragments must be skipped: ${res.stderr}`);
|
||||
});
|
||||
|
||||
test('retired-path exemptions are exact, not a blanket hole (#3604)', (t) => {
|
||||
// The emitted-attribution family retired by the #2724 cutover is documented
|
||||
// in CONTEXT.md as HISTORY ("Historically tests/fixtures/...") — absence is
|
||||
// the healthy steady state, the same semantics as the #2778 entry. But the
|
||||
// exemption must stay exact: a sibling missing tests/ path still fails.
|
||||
//
|
||||
// The NOT-mention must ride a PREDICATE span, mirroring CONTEXT.md's real
|
||||
// shape (RULESET.TESTS.eslint-harness): inside a predicate value the
|
||||
// sub-scan harvests `scripts/eslint-rules` WITHOUT the trailing slash, so
|
||||
// it is tracked-prefix-shaped and only the exemption saves it. Standalone
|
||||
// spans with trailing slashes never reach the exemption (PATH_TOKEN_RE
|
||||
// rejects them), which is exactly the vacuity this test must not have.
|
||||
const context = [
|
||||
'# Context',
|
||||
'',
|
||||
'Historically `tests/golden-install-parity.test.cjs` and `tests/workflow-size-baseline.json`.',
|
||||
'`RULESET.TESTS.harness=local plugin at eslint-rules/ (repo root, NOT scripts/eslint-rules/)`',
|
||||
'A typo sibling `tests/golden-install-parity-typo.test.cjs` is real drift.',
|
||||
'',
|
||||
`${allRuntimesSentence(17, REAL_RUNTIMES)}.`,
|
||||
'',
|
||||
].join('\n');
|
||||
const root = makeRepo(t, { contextBody: context });
|
||||
|
||||
const res = run(root, ['--check']);
|
||||
assert.equal(res.status, 1, 'the sibling typo must still be a finding');
|
||||
assert.match(`${res.stdout}${res.stderr}`, /golden-install-parity-typo/);
|
||||
assert.doesNotMatch(`${res.stdout}${res.stderr}`, /scripts\/eslint-rules/,
|
||||
'the contrastive NOT-mention inside a predicate value must be exempt');
|
||||
assert.doesNotMatch(`${res.stdout}${res.stderr}`, /workflow-size-baseline/,
|
||||
'the retired-path mention must be exempt');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user