Resolves #14926
Root cause, found and named: GoldenPathSynthesizer.spec.mjs writes DISCUSSION/ISSUE fixture nodes through GraphService.upsertNode, and its isolation relied on aiConfig.storagePaths.graph = testDbPath — but storagePaths.graph is a read-only computed leaf (derived from the useTestDatabase toggle), so that assignment does not reliably re-point the database. The spec's own comment documents this exact trap two lines later for handoffFilePath, then steps into it for the graph path. Any invocation outside the unit-test runner (no UNIT_TEST_MODE → useTestDatabase false) resolves the production graph and the fixtures land in the live advisory's source set. The observed pollution id discussion-open-1783347784287 decodes to 2026-07-06T02:23Z — a bare run that night. Fixture rows then scored 10.00 and served as the advisory's rank-1 "release lane" for every stop-hook fire since (~8 consecutive payloads observed on the ticket; three more confirmed live by @neo-opus-vega).
The fix — three layers, each at its own boundary:
- Synthesizer-boundary guard (computedGoldenPathRouting.mjs):
isActionableComputedRecommendation — the choke-point every computed recommendation passes — now excludes nodes stamped properties.isTestFixture: true. The guard stays PURE (no config import) and deliberately does NOT pattern-match id shapes: this suite's scoring-subject fixtures must flow through the pipeline in test runs (they are what the tests exercise), so blinding the guard to them by idiom would blind the tests themselves.
- Fail-loud isolation gate (the spec's
beforeAll): the suite now REFUSES to run when storagePaths.useTestDatabase !== true, with an actionable message — the recurrence vector (bare invocation resolving prod) dies at its source, mechanically, for every test in the file.
- Deterministic realistic fixture ids for the state-gating test (
discussion-91001 / discussion-91002 / issue-91003 replace ${Date.now()}-suffixed ids): the test keeps its exact semantics (OPEN included, CLOSED excluded) while no longer manufacturing the timestamp-shaped ids that made the leak look like live data.
Deliberately NOT in this PR: committed backfill/cleanup code (a live-graph mutation belongs to the operator, one time — see Post-Merge Validation), and any reworking of the spec's broader tmp-db plumbing (the fail-loud gate makes its correctness non-load-bearing for pollution; test-infra polish is separable).
Evidence: L1 (both covering suites green at this head; the regression describe pins the guard's three behaviors: stamped-excluded for both steerable prefixes — including the exact leaked shape — and realistic-unstamped still flows) → L1 required (advisory correctness class). Residual: the one-time feed cleanup below.
Deltas from ticket
- The ticket's "provenance flag or naming-convention guard" choice: provenance flag (stamp), with the naming-convention (timestamp-id) variant tried and rejected in-branch — it collided with this suite's legitimate intra-file-unique fixture ids, i.e. the convention cannot distinguish "fixture in a test db" from "fixture leaked to prod" because they are the same idiom by construction. The fail-loud gate is what actually separates the two worlds.
- The ticket's "backfill-clean" AC is discharged as an operator-run one-time command (Post-Merge Validation), not committed code.
Test Evidence
npm run test-unit -- test/playwright/unit/ai/services/graph/computedGoldenPathRouting.spec.mjs test/playwright/unit/ai/services/graph/GoldenPathSynthesizer.spec.mjs --workers=1 → 54 passed at this head (incl. the new fixture-provenance-guard describe and the full synthesizer suite against the reshaped fixture ids).
node --check green on all three touched files; pre-commit gates green (whitespace, shorthand, jsdoc-types, ticket-archaeology, block-alignment, aiconfig-test-mutation).
Post-Merge Validation
Commits
- 774c018ac — guard + fail-loud gate + deterministic fixture ids + regression describe.
Related: #14588 (adjacent, same routing files, distinct defect — the contradiction guard) · #14920 evidence trail (@neo-opus-vega) · filed by @neo-opus-grace, disposed to me as GP-lane owner.
Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.
Resolves #14926
Root cause, found and named:
GoldenPathSynthesizer.spec.mjswrites DISCUSSION/ISSUE fixture nodes throughGraphService.upsertNode, and its isolation relied onaiConfig.storagePaths.graph = testDbPath— butstoragePaths.graphis a read-only computed leaf (derived from theuseTestDatabasetoggle), so that assignment does not reliably re-point the database. The spec's own comment documents this exact trap two lines later forhandoffFilePath, then steps into it for the graph path. Any invocation outside the unit-test runner (noUNIT_TEST_MODE→useTestDatabasefalse) resolves the production graph and the fixtures land in the live advisory's source set. The observed pollution iddiscussion-open-1783347784287decodes to 2026-07-06T02:23Z — a bare run that night. Fixture rows then scored 10.00 and served as the advisory's rank-1 "release lane" for every stop-hook fire since (~8 consecutive payloads observed on the ticket; three more confirmed live by @neo-opus-vega).The fix — three layers, each at its own boundary:
isActionableComputedRecommendation— the choke-point every computed recommendation passes — now excludes nodes stampedproperties.isTestFixture: true. The guard stays PURE (no config import) and deliberately does NOT pattern-match id shapes: this suite's scoring-subject fixtures must flow through the pipeline in test runs (they are what the tests exercise), so blinding the guard to them by idiom would blind the tests themselves.beforeAll): the suite now REFUSES to run whenstoragePaths.useTestDatabase !== true, with an actionable message — the recurrence vector (bare invocation resolving prod) dies at its source, mechanically, for every test in the file.discussion-91001/discussion-91002/issue-91003replace${Date.now()}-suffixed ids): the test keeps its exact semantics (OPEN included, CLOSED excluded) while no longer manufacturing the timestamp-shaped ids that made the leak look like live data.Deliberately NOT in this PR: committed backfill/cleanup code (a live-graph mutation belongs to the operator, one time — see Post-Merge Validation), and any reworking of the spec's broader tmp-db plumbing (the fail-loud gate makes its correctness non-load-bearing for pollution; test-infra polish is separable).
Evidence: L1 (both covering suites green at this head; the regression describe pins the guard's three behaviors: stamped-excluded for both steerable prefixes — including the exact leaked shape — and realistic-unstamped still flows) → L1 required (advisory correctness class). Residual: the one-time feed cleanup below.
Deltas from ticket
Test Evidence
npm run test-unit -- test/playwright/unit/ai/services/graph/computedGoldenPathRouting.spec.mjs test/playwright/unit/ai/services/graph/GoldenPathSynthesizer.spec.mjs --workers=1→ 54 passed at this head (incl. the new fixture-provenance-guard describe and the full synthesizer suite against the reshaped fixture ids).node --checkgreen on all three touched files; pre-commit gates green (whitespace, shorthand, jsdoc-types, ticket-archaeology, block-alignment, aiconfig-test-mutation).Post-Merge Validation
sqlite3 <graph.sqlite> "SELECT id FROM Nodes WHERE (id LIKE 'discussion-open-1%' OR id LIKE 'discussion-closed-1%' OR id LIKE 'issue-actionable-1%' OR id LIKE 'issue-ready-%') AND id GLOB '*[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'"then the same predicate asDELETE FROM Nodes WHERE ...(Edges cascade via FK).Commits
Related: #14588 (adjacent, same routing files, distinct defect — the contradiction guard) · #14920 evidence trail (@neo-opus-vega) · filed by @neo-opus-grace, disposed to me as GP-lane owner.
Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.