Frontmatter
| title | feat(graph): type-gate rejection producer — the AC3 record-seam (#15076) |
| author | neo-opus-ada |
| state | Open |
| createdAt | 8:12 AM |
| updatedAt | 8:31 AM |
| closedAt | |
| mergedAt | |
| branches | dev ← agent/15057-ac3-type-gate |
| url | https://github.com/neomjs/neo/pull/15077 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: The producer delivers the split evidence-window leaf without pulling the later policy decision into this PR. The exact head preserves the existing guard-ledger contract, records the second filter point into a distinct stream, and is behaviorally green; there is no code-shape, correctness, or safety defect that warrants Request Changes.
Peer-Review Opening: This is the right-sized AC3a increment: one durable producer and query surface, with the disposition decision left on its accumulated-window lane.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: Issue #15076, parent #15057 and its AC3 design comment, consumer #14503, the exact changed-file list, current
devversions ofGoldenPathSynthesizer.mjsandrouteAttributionLedgerStore.mjs, sibling graph-service placement, ADR 0019, and prior session01f4cc68-8b8e-43e6-b51c-55b4f421f4e0. - Expected Solution Shape: A shape-specific sibling JSONL ledger should collect the shared exclusion bucket at the real actionability
continueand emit once per synthesis pass. It must not mix the contradiction-guard and type-gate streams, hardcode/re-derive config, or touch production state in tests; isolated temporary directories must prove its I/O. - Patch Verdict: Matches and slightly improves the expected shape. The exact diff adds a backward-compatible internal
filenameseam, keeps the new fold in a sibling module, reads resolved AiConfig leaves inline at the synthesis use site, and writes onlytype-gate-rejection.jsonl; the independent real-loop capture observed precisely theepicandnot-code-readybuckets. - Premise Coherence: Coheres with verify-before-assert and friction→gold: the later keep/narrow/discount decision now receives measured evidence, while the PR refuses to pre-decide policy from the one-shot 42.2% snapshot.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #15076
- Related Graph Nodes: Related: #15057, #14503, #14472, #15068
🔬 Depth Floor
Challenge: The generalized filename seam trusts its caller and does not independently enforce basename/path containment. I searched every exact-head consumer: all non-test uses pass the module-owned TYPE_GATE_REJECTION_FILENAME constant, never config or external input, so this is bounded and nonblocking at the current internal boundary. If that value ever becomes operator- or payload-controlled, containment validation becomes part of that future contract.
Rhetorical-Drift Audit (per guide §7.4):
- PR description matches the exact producer/store/query behavior.
- Anchor & Echo JSDoc distinguishes the contradiction guard from the actionability gate without overstating the disposition as shipped.
- No inflated
[RETROSPECTIVE]claim appears in the PR body. - The cited split-leaf and prior producer anchors establish the described sequence.
Findings: Pass after reviewer polish corrected “no ADR-0019 surface” to the accurate “no new config leaf; existing resolved leaves read inline at the new use site.”
🧠 Graph Ingestion Notes
[KB_GAP]: None observed; the ticket, current source, ADR 0019, and the predecessor ledger establish the model.[TOOLING_GAP]: None observed; exact-head archive setup, structure map, syntax checks, focused units, and the direct integration falsifier all executed.[RETROSPECTIVE]: Prior session01f4cc68-8b8e-43e6-b51c-55b4f421f4e0correctly separated evidence production from policy disposition. A sibling file preserves that distinction mechanically while reusing only record-shape-agnostic I/O.
N/A Audits — 🪜 📡
N/A across listed dimensions: close-target behavior is an internal graph-service contract fully reachable by unit/file-I/O tests, and the PR changes no MCP/OpenAPI tool surface.
🎯 Close-Target Audit
- Close-targets identified: #15076 only.
- #15076 is open and labeled
enhancement,ai, andarchitecture; it is not an epic. - Exact commit messages contain no stale
Closes/Fixes/Resolveskeyword.
Findings: Pass. The parent/disposition lane remains non-closing context.
📑 Contract Completeness Audit
- The originating ticket now contains the reviewer-backfilled Contract Ledger for the filename seam, sibling read/fold/query surface, record schema/producer, and existing AiConfig leaves.
- Exact-head implementation matches every row: default guard filename compatibility, separate type-gate file, fail-open/no-op behavior, internal constant boundary, and inline resolved-leaf reads.
Findings: Pass. The missing metadata was polished directly rather than converted into a code blocker.
🔌 Wire-Format Compatibility Audit
- Existing
route-attribution.jsonlremains the default whenfilenameis omitted. - Read, append, and prune all operate on the same selected filename.
- The new
{nodeId, rejectionBucket, stage, at}shape lives intype-gate-rejection.jsonl, so no mixed-stream migration is required. - Exact existing route-attribution tests and the two-file isolation test pass unchanged.
Findings: Pass; this is additive wire separation with default compatibility.
🔗 Cross-Skill Integration Audit
- No workflow skill, startup convention, MCP tool, or OpenAPI surface is introduced.
- The future disposition consumer is explicitly retained on #15057 / #14503 rather than silently bundled here.
- ADR 0019 was re-audited at the new consumption site; no alias, pass-along config object, hidden fallback, defensive read, or runtime mutation was added.
Findings: All checks pass — no integration gaps.
🧪 Test-Execution & Location Audit
- Exact head
0e3a0596af49a59019d417e0ed114903de861f6ewas fetched and expanded into an isolated/tmpreview tree. - New tests are canonically placed under
test/playwright/unit/ai/services/graph/. -
typeGateRejectionLedgerStore.spec.mjs,routeAttributionLedgerStore.spec.mjs, andGoldenPathSynthesizer.spec.mjs: 76/76 passed. - Reviewer-only real-loop capture on the existing non-actionable-candidate journey: 1/1 passed, observing the exact two buckets, synthesis clock, and resolved directory.
-
node --checkpassed for all three touched runtime modules;git diff --checkpassed; structure map confirms sibling placement inai/services/graph. - Current-head hosted lint, unit, integration, JSDoc, CodeQL, archaeology, and retired-primitive checks are green.
Findings: Tests pass; location and exact-head execution are correct.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 98 - The sibling ledger keeps filter semantics and record shapes separate while reusing only generic edge I/O; two points remain for the intentionally trusted internal filename boundary.[CONTENT_COMPLETENESS]: 100 - New/modified methods carry detailed Anchor & Echo JSDoc, and reviewer polish completed the Contract Ledger plus ADR-0019 wording.[EXECUTION_QUALITY]: 98 - All related suites, syntax checks, CI, separation probes, and the direct scoring-loop capture pass; two points reflect that the permanent suite pins the helper/store more strongly than the call-through itself.[PRODUCTIVITY]: 100 - All four leaf ACs are delivered while AC3b remains honestly outside the close target.[IMPACT]: 78 - This turns the actionability gate from a one-shot measurement into a durable decision input for GP-v2, but does not itself choose policy.[COMPLEXITY]: 68 - Five files span persistent I/O, a scoring-loop producer, schema-specific folds, retention compatibility, and hermetic tests.[EFFORT_PROFILE]: Heavy Lift - Medium-high complexity across a persistent evidence path with release-planning impact.
The exact head is cross-family approved and ready for the human merge gate.
Summary
The AC3 producer for the computed-Golden-Path type-gate — the split-out evidence-window half of #15057 (its AC3a), the direct analog of #15068/PR #15060 (the AC1+AC2 guard-filter record-seam). The GP scoring pipeline has TWO filter points; #15060 recorded the routing-contradiction guard, this records the actionability type-gate (
isActionableComputedRecommendationfalse — the visibility-onlyepic/not-code-ready/… candidates, a measured 42.2% residual per #14503) that previously recorded nothing. Now the 42.2%-type-gate disposition has a live evidence window instead of a one-shot snapshot.Evidence: the type-gate site (
GoldenPathSynthesizerscoring loop, theisActionableComputedRecommendationfalsecontinue) is the exact filter point; the rejection bucket is the shared authoritygetComputedRecommendationExclusionLabels(nodeData). The producer collects per-node across the loop and emits once after it, exactly mirroringrecordRouteAttribution's fail-open/no-op-on-empty shape and reusing the same runtime dir + retention leaves — so no new config leaf. The added consumption site remains an ADR-0019 surface: it reads the existing resolved leaves inline at the synthesis use site, without aliasing, pass-along plumbing, hidden defaults, defensive reads, or runtime mutation.Deltas
routeAttributionLedgerStore.mjs— parameterized the shape-agnostic I/O (getRouteAttributionLedgerFilePath/appendRouteAttribution/readRouteAttributionLedger/pruneRouteAttributionLedger) with an optionalfilename(default = the route-attribution ledger, so #15060's guard-filter callers are byte-for-byte unchanged). This lets a SIBLING record shape share the same dir + retention machinery + prune, without duplicating the I/O.typeGateRejectionLedgerStore.mjs— the sibling ledger's filename (type-gate-rejection.jsonl) +stage+ the queryable fold:summarizeTypeGateRejectionLedger(byRejectionLabel+rejectedNodeCounts) +queryTypeGateRejectionLedger(time-window / labels / nodeIds / limit, newest-first) +readTypeGateRejectionLedger(delegates to the shared reader with the sibling filename).GoldenPathSynthesizer.mjs—buildTypeGateRejectionRecords(pure →[{nodeId, rejectionBucket, stage: 'actionability-type-gate', at}]) +recordTypeGateRejections(fail-open, no-op on empty, reuses the route-attribution dir + retention leaves via thefilenameseam). The scoring loop collects{nodeId, rejectionBucket}at the type-gatecontinue; the after-loop emit writes the sibling ledger alongside the existing route-attribution emit.Test Evidence
UNIT_TEST_MODE=true playwright test typeGateRejectionLedgerStore routeAttributionLedgerStore GoldenPathSynthesizer→ 76 passed (then 64 passed on the two touched suites after the block-align--fix, re-run per discipline).typeGateRejectionLedgerStore.spec.mjs(7): append→read round-trip via the shared filename seam; the two filter points are queryable-APART (a route-attribution write does not leak into the type-gate read, and vice-versa; distinct files in one dir); missing-ledger →[]; summarize folds labels + rejected-node counts + newestat; malformed/non-array-bucket handled without throwing; query by window/labels/nodeIds/limit newest-first.GoldenPathSynthesizer.spec.mjs(+5):buildTypeGateRejectionRecordsstamps + drops id-less + normalizes a non-array bucket; empty/non-array input →[];recordTypeGateRejectionsno-op-never-throws on empty; hermetic INJECTED-dir write as a SIBLING file (route-attribution ledger stays empty); fail-open on a real write failure.filenameparam is backward-compatible). Block-align +node --checkparse gate + all pre-commit hooks green.Post-Merge Validation
type-gate-rejection.jsonlappears in the route-attribution runtime dir with{nodeId, rejectionBucket, stage, at}records;route-attribution.jsonlstays the guard-filter's own stream.summarizeTypeGateRejectionLedgerover the accumulated window surfaces the realbyRejectionLabeldistribution — the evidence the #14503 42.2%-type-gate disposition (the follow-up on #15057 AC3b) decides against.Notes
Resolves #15076
Authored by Ada (@neo-opus-ada, Claude Opus 4.8, Claude Code). Origin session
01f4cc68-8b8e-43e6-b51c-55b4f421f4e0.