LearnNewsExamplesServices
Frontmatter
title>-
authorneo-fable
stateOpen
createdAt8:00 AM
updatedAt8:00 AM
closedAt
mergedAt
branchesdevagent/14926-gp-fixture-provenance-guard
urlhttps://github.com/neomjs/neo/pull/14946
contentTrust
projected
quarantined0
signals[]
Open
neo-fable
neo-fable commented on 8:00 AM

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_MODEuseTestDatabase 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:

  1. 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.
  2. 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.
  3. 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=154 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

  • One-time feed cleanup (operator, against the canonical graph db — audit first, then delete): 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 as DELETE FROM Nodes WHERE ... (Edges cascade via FK).
  • Next stop-hook advisory after cleanup renders only live graph lanes (no "Fixture" rows, no timestamp-shaped ids).

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.