D8: restore real type checking across the reactor↔afi-core boundary - #77
Conversation
afi-reactor/typings.d.ts declared 16 ambient modules, 7 of which typed the
entire afi-core boundary as `any`, with strict:false and tests excluded from
tsconfig. No compiler had ever checked that seam.
Root cause was subtler than "resolution is broken". An ambient `declare module`
BEATS successful module resolution: 4 of the 6 afi-core specifiers already
resolved to real afi-core source and were still bound to `any`. Switching
resolution alone changed nothing (0 errors); the shims had to be deleted. A
side effect was that afi-core's source was being compiled inside the reactor's
program under strict:false — laxer than afi-core's own strict:true build.
Config: module/moduleResolution NodeNext (reads afi-core's exports map AND
enforces .js relative specifiers at compile time, which "bundler" does not),
strict:true, test/**/* typechecked. tsconfig exclude now mirrors jest's
testPathIgnorePatterns. New tsconfig.build.json keeps the compiled test suite
out of dist/ (build emits 87 files, same as main). CI gains a typecheck step.
Shims: 16 -> 1. Only `input` genuinely ships no types. The other 15 shadowed
real types — afi-core (via its exports map), ajv, ajv-formats, ccxt,
trading-signals, telegram x3, node-telegram-bot-api, plus the describe/it/test
globals shadowing @types/jest and a missing @types/express. Removing them
REDUCED the error count 45 -> 21 while increasing type coverage.
45 errors triaged: 2 REAL BUG, 13 DORMANT SURFACE, 30 NOISE.
Fixed here:
- server.ts: provenance.ingestHash is optional by governance (the USS v1.1
schema requires only source/providerId/signalId) but was passed to
checkDuplicate/recordIngest which require string. With AFI_INGEST_DEDUPE=1
the first hashless signal would be recorded under the key `undefined` and
every subsequent hashless signal rejected 409 as a duplicate. Guards added;
they are no-ops on every payload this route can produce today. This makes D6
("turn dedupe on?") not a one-line change.
- mergeEnrichedView.ts: three `as Record<string, unknown>` casts replaced by a
declared ReactorEnrichedView interface. Compiles to byte-identical JS.
- cpjMapper.ts: comparator parameters annotated, semantics untouched (this
feeds generateIngestHash).
- adapters: real ccxt OHLCV type at the mapping site.
Deliberately NOT fixed (needs a decision — would change scoring behaviour):
ccxt declares OHLCV elements as `Num` = number | undefined, i.e. a venue may
return a partial candle, while OHLCVCandle requires number. The code defends
ticker.last/timestamp but not ticker.symbol and no candle field. Recorded as
D8-R2 in the dormant-surface inventory.
Zero behaviour change, verified by diffing emitted JavaScript rather than
trusting the suite: of 87 emitted files only server.js differs semantically,
by exactly the 2 intentional guard statements. mergeEnrichedView.js and
cpjMapper.js are byte-identical.
670/670 tests green (47/47 suites), provenance golden 312da118... and all 140
test fixtures byte-identical, compiled honest-unavailable smoke passes.
Report: reports/afi-d8-type-boundary-v0.1.md
Requires afi-core PR (indicators type widening) to be merged together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Applied from the pre-merge adversarial review (4 lenses + refutation): - module/moduleResolution -> NodeNext (was bundler). Both typecheck clean and emit byte-identical JS, but only NodeNext enforces .js relative specifiers at compile time. "bundler" delegated that invariant to scripts/esm-check.sh, which no CI workflow runs and which already exits 1 on main (D8-D8). - tsconfig exclude comment corrected: it claimed the excluded paths "are the files jest never executes". They are the files jest is CONFIGURED to skip (testPathIgnorePatterns). Because testMatch is an ALLOWLIST, three further test files never run yet are still typechecked (deliberately) — D8-D3. - package-lock.json: reverted unrelated ../afi-config dev-dep drift that `npm install` re-snapshotted. The lock diff is now @types/express plus its transitive @types only, all dev:true. `npm ci --dry-run` clean. - Annotated `private exchange: any` in both price adapters. The reviewer was right that the `(candle: OHLCV)` annotation buys no safety while the receiver is `any` — the seam is now flagged where the `any` actually lives (D8-R2). Verified after the changes, against a `main` worktree build: of 87 emitted files, exactly ONE differs semantically (dist/src/server.js, by the two intentional ingestHash guards). Everything else is comment-only. 670/670 tests, golden 312da118... and all 140 fixtures byte-identical, compiled honest-unavailable smoke passes.⚠️ MERGE ORDER: afi-core PR must land FIRST. Reactor CI checks out afi-core at its default branch with no `ref:`, so the new strict typecheck step fails against afi-core@main until the `indicators` widening is on main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Kilo Code Review could not run — your account is out of credits. Add credits or switch to a free model to enable reviews on this change. |
|
D8 (PR #77) deleted the ambient afi-core shims, including the froggy.enrichment_adapter block this branch had patched to declare BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB. Conflict resolved by taking main's typings.d.ts. The shim declaration is now unnecessary and must NOT be reinstated: the constant resolves from afi-core's real published dist/*.d.ts (afi-core PR #29, on main). Keeping both sides here would have silently restored the `any` boundary D8 removed.
Executes D8 from
reports/WORKLIST.md— the highest-leverage open item.The problem was subtler than "resolution is broken"
typings.d.tsdeclared 16 ambient modules, 7 of which typed the entire afi-core boundary asany, alongsidestrict: falseand tests excluded fromtsconfig.The hypothesis was that
moduleResolution: "node"couldn't read afi-core'sexportsmap. That was half right. Fromtsc --traceResolution:"node"?analysts/froggy.enrichment_adapter.jsanalysts/froggy.trend_pullback_v1.jsvalidators/UwrProfileLoader.jsvalidators/UniversalWeightingRule.jsafi-core/analystafi-core/decayOnly two shims were load-bearing. An ambient
declare modulebeats successful module resolution — four specifiers already resolved to real afi-core source and were still bound toany. Switching resolution alone changed nothing (0 errors); the shims had to be deleted.Two consequences nobody could see: the reactor was compiling afi-core's source inside its own program under
strict: false(laxer than afi-core's ownstrict: truebuild), and three shims declared modules the reactor never imports at all.Results — 45 errors: 2 REAL BUG · 13 DORMANT · 30 NOISE
Removing shims reduced the count 45 → 21 while increasing type coverage — the clearest proof they were never load-bearing. 6 of 7 third-party shims shadowed types the package already ships; only
inputgenuinely has none.strict:false, src only)noImplicitAny→strict→ tests includedajv/ajv-formatsshims (realErrorObject)describe/it/testglobals (real@types/jest)@types/expressFixed here
server.ts— the one real bug.provenance.ingestHashis optional by governance (the USS v1.1 schema requires only source/providerId/signalId) but was passed tocheckDuplicate/recordIngest, which requirestring. WithAFI_INGEST_DEDUPE=1, the first hashless signal would be recorded under the literal keyundefinedand every subsequent hashless signal rejected 409 as a duplicate — silently dropping real signals. Guards added; they are no-ops on every payload this route can produce today. This makes D6 ("turn dedupe on?") not a one-line change.mergeEnrichedView.ts— threeas Record<string, unknown>casts replaced by a declaredReactorEnrichedViewinterface. Compiles to byte-identical JS.cpjMapper.ts— comparator parameters annotated, semantics untouched (feedsgenerateIngestHash).tsconfig.build.json(new) — keeps the compiled test suite out ofdist/. CI gains a typecheck step.Deliberately NOT fixed — needs a decision
ccxt declares
OHLCV = [Num × 6]withNum = number | undefined(a venue may return a partial candle) whileOHLCVCandlerequiresnumber. The code defendsticker.last/timestampbut notticker.symboland no candle field — an inconsistently applied assumption on the live Blofin feed. Rejecting or defaulting a partial candle changes scoring behaviour, so it is filed as D8-R2, not fixed.Zero behaviour change — proven by diffing emitted JavaScript
Tests passing only proves the tests didn't notice. Both branches were compiled and the emitted
.jscompared directly against amainworktree:Of 87 emitted files, exactly ONE differs semantically —
dist/src/server.js, by exactly the two intentional guard statements.mergeEnrichedView.jsandcpjMapper.jsare byte-identical, confirming those changes were purely type-level.312da118…and all 140 fixtures byte-identicalnpm ci --dry-runclean; lock diff is@types/express+ transitive@typesonlyWas 324 a floor?
Yes, but only modestly: +10 (→ 334). Three findings were already in the inventory, so the compiler mostly corroborated the manual audit. The real yield was a different class: 2 latent defects on live paths, 9 shims shadowing real third-party types (incl.
ajv— schema validation was unchecked), and 9 of 56 test files that never execute (testMatchis an allowlist; 3 have no marker at all, including a 630-line integration suite for the live CPJ route).Review
4 adversarial lenses + refutation agents. The review changed the diff in six ways — including switching
bundler→NodeNext(only NodeNext enforces.jsspecifiers at compile time;bundlerdelegated that to a guard CI doesn't run), catching a factual error in the report (afi-core emits 19 files, not 22 — 3 were stale residue), and reverting unrelated lockfile drift. Two "REAL_BUG" calls onmergeEnrichedViewwere refuted and corrected to NOISE.Full report:
reports/afi-d8-type-boundary-v0.1.md· Register: inventory §3.2