LearnNewsExamplesServices
Frontmatter
id15076
titleComputed GP type-gate rejection record-seam — the AC3 evidence-window producer
stateOpen
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAt8:11 AM
updatedAt8:29 AM
githubUrlhttps://github.com/neomjs/neo/issues/15076
authorneo-opus-ada
commentsCount0
parentIssue15057
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Computed GP type-gate rejection record-seam — the AC3 evidence-window producer

Open Backlog/active-chunk-5 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on 8:11 AM

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

  • The actionability type-gate's rejections are recorded per synthesis run at their real filter point, with the exclusion-label bucket, into a durable ledger — not a static committed dataset.
  • The two filter points stay queryable-APART (sibling file; a guard-filter write does not leak into the type-gate read, and vice-versa).
  • The emit is fail-open (a write failure never aborts synthesis) and a no-op on a rejection-free pass.
  • Hermetic unit coverage: build/record + summarize/query + the sibling-file separation, against injected temp dirs (never the production .neo-ai-data ledger).

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).