Skip to content

D8: restore real type checking across the reactor↔afi-core boundary - #77

Merged
Gio2050 merged 2 commits into
mainfrom
mission/d8-type-boundary-v0.1
Aug 4, 2026
Merged

D8: restore real type checking across the reactor↔afi-core boundary#77
Gio2050 merged 2 commits into
mainfrom
mission/d8-type-boundary-v0.1

Conversation

@Gio2050

@Gio2050 Gio2050 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Executes D8 from reports/WORKLIST.md — the highest-leverage open item.

⚠️ Merge AFI-Protocol/afi-core#28 FIRST. This PR's CI checks out afi-core at its default branch with no ref:, and the new strict typecheck step fails against afi-core@main until the indicators widening lands. A red CI here before that is expected, not a defect.

The problem was subtler than "resolution is broken"

typings.d.ts declared 16 ambient modules, 7 of which typed the entire afi-core boundary as any, alongside strict: false and tests excluded from tsconfig.

The hypothesis was that moduleResolution: "node" couldn't read afi-core's exports map. That was half right. From tsc --traceResolution:

Specifier Resolved under "node"? Shim?
analysts/froggy.enrichment_adapter.js ✅ → real afi-core source yes
analysts/froggy.trend_pullback_v1.js ✅ → real afi-core source yes
validators/UwrProfileLoader.js ✅ → real afi-core source yes
validators/UniversalWeightingRule.js ✅ → real afi-core source no
afi-core/analyst yes
afi-core/decay yes

Only two shims were load-bearing. An ambient declare module beats successful module resolution — four 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.

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 own strict: true build), 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 input genuinely has none.

Change Errors
baseline (16 shims, strict:false, src only) 0 ← the problem
bundler resolution, shims still present 0
+ 7 afi-core shims deleted 3
+ noImplicitAnystrict → tests included 45
ajv/ajv-formats shims (real ErrorObject) 42
describe/it/test globals (real @types/jest) 30
+ @types/express 21
all fixes 0

Fixed here

  • server.ts — the one real bug. 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 literal key undefined and 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 — three as Record<string, unknown> casts replaced by a declared ReactorEnrichedView interface. Compiles to byte-identical JS.
  • cpjMapper.ts — comparator parameters annotated, semantics untouched (feeds generateIngestHash).
  • tsconfig.build.json (new) — keeps the compiled test suite out of dist/. CI gains a typecheck step.

Deliberately NOT fixed — needs a decision

ccxt declares OHLCV = [Num × 6] with Num = number | undefined (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 — 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 .js compared directly against a main worktree:

Of 87 emitted files, exactly ONE differs semanticallydist/src/server.js, by exactly the two intentional guard statements. mergeEnrichedView.js and cpjMapper.js are byte-identical, confirming those changes were purely type-level.

  • ✅ 670/670 tests, 47/47 suites (baseline is 670, not the 664 in the worklist)
  • ✅ provenance golden 312da118… and all 140 fixtures byte-identical
  • ✅ compiled honest-unavailable smoke passes (exercises the modified CPJ route)
  • npm ci --dry-run clean; lock diff is @types/express + transitive @types only

Was 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 (testMatch is 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 bundlerNodeNext (only NodeNext enforces .js specifiers at compile time; bundler delegated 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 on mergeEnrichedView were refuted and corrected to NOISE.

Full report: reports/afi-d8-type-boundary-v0.1.md · Register: inventory §3.2

Gio2050 and others added 2 commits August 4, 2026 04:50
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>
@cursor

cursor Bot commented Aug 4, 2026

Copy link
Copy Markdown

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-bot

kilo-code-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

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.

@Gio2050

Gio2050 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ The red CI here is expected — it is the merge-order coupling, and it confirms the new step works

Validate AFI Reactor fails at the new Typecheck (src + tests, strict) step with exactly one error and nothing else:

src/pipeline/nodes/laneView.ts(77,7): error TS2322: Type 'number | undefined' is not assignable to type 'number | null'.
  Type 'undefined' is not assignable to type 'number | null'.

Cause: this workflow checks out afi-core with no ref: (validate-all.yml:22-26), so CI always builds afi-core's default branch. AFI-Protocol/afi-core#28 — which widens FroggyEnrichedView.technical.indicators to match what this projection actually produces — is not on afi-core main yet.

Fix: merge AFI-Protocol/afi-core#28 first, then re-run this job. Locally, against afi-core with #28 applied, npm run typecheck exits 0.

This is arguably the best possible demonstration that D8 did what it claimed. Before this PR, the reactor was structurally immune to anything afi-core did to its types — the ambient declare module shims meant a cross-repo type change could not fail a build. That immunity is now gone, deliberately, and the very first cross-repo skew it could catch is the one it caught.

Follow-up worth doing separately

afi-core is the only cross-repo build input that records no revision — an unpinned file:../ edge, while afi-infra is pinned by SHA (git+https://…#<sha>). Now that it is a compile-time dependency, recommend either pinning ref: on the sibling checkouts in validate-all.yml, or moving afi-core to the same pinned-git form. Tracked in reports/afi-d8-type-boundary-v0.1.md §7.5.

(Kilo Code Review fails app-side on every AFI PR and is not a signal — judge by repo CI. Scan for Secrets passes on both runs.)

@Gio2050
Gio2050 merged commit 614bca4 into main Aug 4, 2026
3 of 5 checks passed
Gio2050 added a commit that referenced this pull request Aug 4, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant