Context
Split from #15057 (Computed GP guard-filter ledger recording) as its shippable AC3-producer half — the direct analog of #15068 (the AC1+AC2 guard-filter record-seam, CLOSED via PR #15060). #15057 AC3 has two phases: (a) an evidence window the 42.2%-type-gate disposition can decide against, and (b) the disposition-decision itself (keep / narrow / rank-with-discount). (b) genuinely needs an accumulated production window — nothing to decide against until synthesis runs populate it — so per #15057's own "naturally a second PR" note it stays on #15057. This leaf is (a): the producer that creates the window.
The Problem
The Golden Path scoring pipeline has TWO filter points. #15060 recorded the routing-contradiction guard's filtering. The actionability type-gate (isActionableComputedRecommendation === false — the visibility-only epic / not-code-ready / … candidates) rejects a measured 42.2% residual (#14503) but recorded NOTHING, so the disposition had no live evidence to decide against — only the one-shot measurement snapshot.
The Architectural Reality
GoldenPathSynthesizer.mjs scoring loop: the type-gate continue (the isActionableComputedRecommendation false branch) is where the rejection evidence is produced. The exclusion bucket is the shared authority getComputedRecommendationExclusionLabels(nodeData).
routeAttributionLedgerStore.mjs (#15060): its append/read/prune I/O is record-shape-agnostic; parameterizing the filename (default preserves the guard-filter callers) lets a sibling ledger share the same runtime dir + retention machinery with a distinct file + record shape.
typeGateRejectionLedgerStore.mjs (new): owns the type-gate filename + stage + the summarize/query fold the disposition reads.
The Fix
- Collect
{nodeId, rejectionBucket} at the type-gate filter point across the scoring loop; emit once after it via recordTypeGateRejections (mirrors recordRouteAttribution — fail-open, no-op on empty, reuses the route-attribution dir + retention leaves, writes a sibling type-gate-rejection.jsonl).
buildTypeGateRejectionRecords (pure) → [{nodeId, rejectionBucket, stage: 'actionability-type-gate', at}].
- Sibling store:
summarizeTypeGateRejectionLedger (byRejectionLabel + rejectedNodeCounts) + queryTypeGateRejectionLedger.
- No new config leaf. The added consumption site remains governed by ADR 0019 and reads the existing resolved route-attribution directory + retention leaves inline at the emit use site.
Acceptance Criteria
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
routeAttributionLedgerStore optional filename seam on path/append/read/prune |
#15076 sibling-file separation + the existing route-attribution.jsonl contract |
A trusted internal sibling filename selects a distinct JSONL file under the resolved ledger directory; omission preserves every existing guard-ledger caller and route-attribution.jsonl behavior; retention prunes the selected file. |
Missing/invalid dir keeps the existing fail-closed caller contract; a missing selected file reads as []; append-time stat/prune failure remains non-fatal observability behavior. The filename is an internal constant, never operator/config input. |
Updated JSDoc on all generalized functions. |
Existing routeAttributionLedgerStore.spec.mjs plus the sibling-file separation test in typeGateRejectionLedgerStore.spec.mjs. |
New typeGateRejectionLedgerStore read/summarize/query surface |
#15076 AC1-AC2; getComputedRecommendationExclusionLabels() is the bucket authority |
Reads the sibling stream oldest-first; summarizes label and node frequencies; queries by inclusive time window, rejection-label intersection, node ids, and newest-first limit. |
Missing ledger returns []; the shared reader skips corrupt lines but surfaces unreadable-file errors; malformed records contribute no label/node fold and never throw. |
Module and export JSDoc. |
typeGateRejectionLedgerStore.spec.mjs round-trip, separation, missing-ledger, fold, malformed-row, and query cases. |
Type-gate record schema {nodeId, rejectionBucket, stage, at} and GoldenPathSynthesizer.recordTypeGateRejections() |
#15076 AC1/AC3; live actionability branch in GoldenPathSynthesizer |
Collects the shared exclusion bucket at the real non-actionable continue, emits once after the scoring loop, and writes stage: 'actionability-type-gate' into type-gate-rejection.jsonl. |
Id-less candidates are dropped; a non-array bucket normalizes to []; an empty pass is a no-op; invalid retention or I/O is swallowed-logged so observability never aborts synthesis. |
Anchor-and-Echo JSDoc on builder and recorder. |
GoldenPathSynthesizer.spec.mjs builder/no-op/hermetic-write/fail-open coverage plus the existing real-loop non-actionable-candidate journey. |
Existing goldenPathRouteAttributionLedger* AiConfig leaves at the type-gate emit use site |
ADR 0019 + #15076 no-new-config boundary |
Reads the already-resolved directory and retention leaves inline at the synthesis use site and supplies them to the sibling recorder; no alias, env read, hidden default, or runtime mutation. |
Test mode resolves the isolated ledger directory by construction; missing/invalid values degrade only the observability write through the recorder's fail-open boundary. |
No config-doc change; existing recorder JSDoc names the boundary. |
Existing resolved-test-path assertion plus AiConfig lint and exact-head unit suite. |
Out of Scope
- The disposition-DECISION (keep / narrow / rank-with-discount) — needs an accumulated production window; stays on #15057 (its AC3b), naturally a later PR.
- The routing-contradiction guard record-seam (done — #15068 / PR #15060).
Related
Parent #15057 (its AC3-producer half) · sibling #15068 (AC1+AC2 record-seam) · the disposition consumer #14503 · GP-v2 epic #14472.
Origin Session ID: 01f4cc68-8b8e-43e6-b51c-55b4f421f4e0
Retrieval Hint: "GP type-gate rejection record-seam actionability exclusion labels sibling ledger 42.2% disposition evidence window"
Authored by Ada (@neo-opus-ada, Claude Opus 4.8, Claude Code).
Context
Split from #15057 (
Computed GP guard-filter ledger recording) as its shippable AC3-producer half — the direct analog of #15068 (the AC1+AC2 guard-filter record-seam, CLOSED via PR #15060). #15057 AC3 has two phases: (a) an evidence window the 42.2%-type-gate disposition can decide against, and (b) the disposition-decision itself (keep / narrow / rank-with-discount). (b) genuinely needs an accumulated production window — nothing to decide against until synthesis runs populate it — so per #15057's own "naturally a second PR" note it stays on #15057. This leaf is (a): the producer that creates the window.The Problem
The Golden Path scoring pipeline has TWO filter points. #15060 recorded the routing-contradiction guard's filtering. The actionability type-gate (
isActionableComputedRecommendation=== false — the visibility-onlyepic/not-code-ready/ … candidates) rejects a measured 42.2% residual (#14503) but recorded NOTHING, so the disposition had no live evidence to decide against — only the one-shot measurement snapshot.The Architectural Reality
GoldenPathSynthesizer.mjsscoring loop: the type-gatecontinue(theisActionableComputedRecommendationfalse branch) is where the rejection evidence is produced. The exclusion bucket is the shared authoritygetComputedRecommendationExclusionLabels(nodeData).routeAttributionLedgerStore.mjs(#15060): its append/read/prune I/O is record-shape-agnostic; parameterizing thefilename(default preserves the guard-filter callers) lets a sibling ledger share the same runtime dir + retention machinery with a distinct file + record shape.typeGateRejectionLedgerStore.mjs(new): owns the type-gate filename +stage+ thesummarize/queryfold the disposition reads.The Fix
{nodeId, rejectionBucket}at the type-gate filter point across the scoring loop; emit once after it viarecordTypeGateRejections(mirrorsrecordRouteAttribution— fail-open, no-op on empty, reuses the route-attribution dir + retention leaves, writes a siblingtype-gate-rejection.jsonl).buildTypeGateRejectionRecords(pure) →[{nodeId, rejectionBucket, stage: 'actionability-type-gate', at}].summarizeTypeGateRejectionLedger(byRejectionLabel+rejectedNodeCounts) +queryTypeGateRejectionLedger.Acceptance Criteria
.neo-ai-dataledger).Contract Ledger
routeAttributionLedgerStoreoptionalfilenameseam on path/append/read/pruneroute-attribution.jsonlcontractroute-attribution.jsonlbehavior; retention prunes the selected file.dirkeeps the existing fail-closed caller contract; a missing selected file reads as[]; append-time stat/prune failure remains non-fatal observability behavior. The filename is an internal constant, never operator/config input.routeAttributionLedgerStore.spec.mjsplus the sibling-file separation test intypeGateRejectionLedgerStore.spec.mjs.typeGateRejectionLedgerStoreread/summarize/query surfacegetComputedRecommendationExclusionLabels()is the bucket authority[]; the shared reader skips corrupt lines but surfaces unreadable-file errors; malformed records contribute no label/node fold and never throw.typeGateRejectionLedgerStore.spec.mjsround-trip, separation, missing-ledger, fold, malformed-row, and query cases.{nodeId, rejectionBucket, stage, at}andGoldenPathSynthesizer.recordTypeGateRejections()GoldenPathSynthesizercontinue, emits once after the scoring loop, and writesstage: 'actionability-type-gate'intotype-gate-rejection.jsonl.[]; an empty pass is a no-op; invalid retention or I/O is swallowed-logged so observability never aborts synthesis.GoldenPathSynthesizer.spec.mjsbuilder/no-op/hermetic-write/fail-open coverage plus the existing real-loop non-actionable-candidate journey.goldenPathRouteAttributionLedger*AiConfig leaves at the type-gate emit use siteOut of Scope
Related
Parent #15057 (its AC3-producer half) · sibling #15068 (AC1+AC2 record-seam) · the disposition consumer #14503 · GP-v2 epic #14472.
Origin Session ID: 01f4cc68-8b8e-43e6-b51c-55b4f421f4e0
Retrieval Hint: "GP type-gate rejection record-seam actionability exclusion labels sibling ledger 42.2% disposition evidence window"
Authored by Ada (@neo-opus-ada, Claude Opus 4.8, Claude Code).