From 441c1d61da281ca77d78b759e9419670e27cbe3a Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 4 Aug 2026 04:50:59 -0400 Subject: [PATCH 1/2] analysts: declare indicators projected-but-unread and widen it to match reality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two type-only corrections to FroggyEnrichedView.technical.indicators, both surfaced by afi-reactor's D8 pass (the reactor↔afi-core boundary is now genuinely typechecked instead of ambient `any`). 1. Documentation gap. indicators' siblings atr14 and trendBias carry explicit "context only — NOT read by this adapter or any scorer input" annotations, and atrRegime documents that it IS read (AR-GOV D-AR-3). indicators carried no annotation at all, so a reader could not tell which it was. It is read by nothing: the only mentions of the token anywhere in afi-core are this declaration and an unrelated `indicators` array on the universal signal schema. Now annotated to match its siblings. 2. The declared type did not describe what is produced. The reactor's projecting node writes volume_ratio from TechnicalLensV1.volumeRatio, which is optional, so the key is written with an `undefined` value. The Record value type was `number | null`, which a Record position cannot widen away — the only field of the four to error. The distinction is load-bearing and is why the fix lands here rather than in the reactor: canonical JSON DROPS an undefined-valued key but RETAINS a null-valued one. Coercing with `?? null` in the reactor would have moved hashes; building the key conditionally would have changed Object.keys(). The only zero-behaviour-change option is to make the declared type honest. Type-only, proven: all 22 emitted dist/*.js files are byte-identical before and after. 231/231 tests green. Pairs with afi-reactor's D8 PR and must merge with it — the reactor now binds to this package's published dist/*.d.ts rather than to an `any` shim. Co-Authored-By: Claude Opus 5 (1M context) --- analysts/froggy.enrichment_adapter.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/analysts/froggy.enrichment_adapter.ts b/analysts/froggy.enrichment_adapter.ts index 5d7bdbe..2fcd2b5 100644 --- a/analysts/froggy.enrichment_adapter.ts +++ b/analysts/froggy.enrichment_adapter.ts @@ -48,7 +48,22 @@ export interface FroggyEnrichedView { emaDistancePct?: number | null; isInValueSweetSpot?: boolean | null; brokeEmaWithBody?: boolean | null; - indicators?: Record | null; + /** + * Raw indicator readings projected from the technical lane + * (rsi / ema_20 / ema_50 / volume_ratio). Context only — NOT read by this + * adapter or any scorer input; wiring one into an axis is a separately + * governed change. Documented here to match its siblings below, which + * already carry this annotation. + * + * Values are `| undefined` because the producing lane payload marks some + * readings optional (e.g. TechnicalLensV1.volumeRatio): the projecting node + * writes the key with an `undefined` value rather than omitting it or + * substituting null. That distinction is load-bearing — canonical JSON drops + * an `undefined`-valued key but retains a `null`-valued one — so this type + * describes what is actually produced instead of forcing a coercion that + * would move hashes. + */ + indicators?: Record | null; /** * ATR(14) projected from the technical lane. Context only — NOT read by * this adapter or any scorer input; wiring it into an axis is a separately From 927ec3e8dab70c109d8ba65d35d39555057833d1 Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 4 Aug 2026 05:02:07 -0400 Subject: [PATCH 2/2] analysts: make the indicators JSDoc claim precise about hash relevance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review correction. The comment justified widening with "a coercion that would move hashes", which overstates the present situation: technical.indicators reaches no live hash preimage today — enrichmentHash is computed over the raw lane payloads and sorted categories, not over this renamed {rsi, ema_20, ema_50, volume_ratio} projection. The accurate justification, now recorded: widening is the only option that provably changes nothing (coercing with `?? null` alters the value, omitting the key alters Object.keys()), AND strategyLocalViewHash is a declared-but-unproduced pin over exactly this view — so a coercion here would silently become hash-relevant the moment that producer is wired up. Still type-only: 19/19 emitted .js byte-identical (a clean build emits 19; the previously-reported 22 counted three stale artifacts from sources deleted in Mission R — dist/runtime/*.js and dist/src/dag/SignalEnvelope.js — which `npm run build:clean` has now dropped). This matters beyond tidiness: afi-reactor now typechecks against this dist/, so stale declarations here could mask or invent errors across the D8 seam. 231/231 tests green. Co-Authored-By: Claude Opus 5 (1M context) --- analysts/froggy.enrichment_adapter.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/analysts/froggy.enrichment_adapter.ts b/analysts/froggy.enrichment_adapter.ts index 2fcd2b5..f9a58f0 100644 --- a/analysts/froggy.enrichment_adapter.ts +++ b/analysts/froggy.enrichment_adapter.ts @@ -58,10 +58,17 @@ export interface FroggyEnrichedView { * Values are `| undefined` because the producing lane payload marks some * readings optional (e.g. TechnicalLensV1.volumeRatio): the projecting node * writes the key with an `undefined` value rather than omitting it or - * substituting null. That distinction is load-bearing — canonical JSON drops - * an `undefined`-valued key but retains a `null`-valued one — so this type - * describes what is actually produced instead of forcing a coercion that - * would move hashes. + * substituting null. + * + * The type is widened to match that rather than coercing at the producer + * because widening is the only option that provably changes nothing. The + * alternatives do not: canonical JSON drops an `undefined`-valued key but + * retains a `null`-valued one, and omitting the key changes `Object.keys()`. + * This view reaches no live hash preimage today (`enrichmentHash` is taken + * over the raw lane payloads, not this renamed projection), but + * `strategyLocalViewHash` is a declared-but-unproduced pin over exactly this + * view — so a coercion here would become hash-relevant the moment that + * producer is wired up. */ indicators?: Record | null; /**