From cf650f55de18fba57aea112a46fd17cf78dd8c2e Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 4 Aug 2026 04:50:45 -0400 Subject: [PATCH 1/2] =?UTF-8?q?D8:=20restore=20real=20type=20checking=20ac?= =?UTF-8?q?ross=20the=20reactor=E2=86=94afi-core=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` 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) --- .github/workflows/validate-all.yml | 14 +++ package-lock.json | 100 +++++++++++++++- package.json | 3 +- .../exchanges/blofinPriceFeedAdapter.ts | 12 +- .../exchanges/coinbasePriceFeedAdapter.ts | 12 +- src/pipeline/nodes/mergeEnrichedView.ts | 39 ++++++- src/server.ts | 11 +- src/uss/cpjMapper.ts | 4 +- tsconfig.build.json | 24 ++++ tsconfig.json | 37 +++++- typings.d.ts | 108 +++--------------- 11 files changed, 254 insertions(+), 110 deletions(-) create mode 100644 tsconfig.build.json diff --git a/.github/workflows/validate-all.yml b/.github/workflows/validate-all.yml index 3b4d8a7b..c1d756aa 100644 --- a/.github/workflows/validate-all.yml +++ b/.github/workflows/validate-all.yml @@ -91,6 +91,20 @@ jobs: working-directory: afi-reactor run: npm ci + # D8: the reactor↔afi-core boundary is genuinely typechecked now (strict, + # real afi-core dist/*.d.ts, no ambient `any` shims). `npm run build` only + # compiles src/, and ts-jest only checks the suites jest actually runs, so + # this step is what keeps the FULL typecheck config honest — including the + # test tree. If it fails, do not re-add a shim: fix resolution, or install + # the package's @types. See reports/afi-d8-type-boundary-v0.1.md. + - name: Typecheck (src + tests, strict) + id: typecheck + working-directory: afi-reactor + run: | + echo "🔎 Typechecking AFI Reactor (strict, afi-core boundary included)..." + npm run typecheck + echo "✅ Typecheck clean" + - name: Build id: build working-directory: afi-reactor diff --git a/package-lock.json b/package-lock.json index d2c28f7e..1a3482b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "zod": "^4.0.14" }, "devDependencies": { + "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/supertest": "^6.0.3", "jest": "^29.0.0", @@ -44,10 +45,19 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { - "@types/node": "^20.11.0", + "@eslint/js": "^10.0.1", + "@types/node": "^26.1.1", + "@vitest/coverage-v8": "^4.1.10", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", + "eslint": "^10.7.0", + "husky": "^9.1.7", + "jscpd": "^5.0.12", + "knip": "^6.27.0", + "lint-staged": "^17.1.0", + "prettier": "^3.9.5", "typescript": "^5.4.0", + "typescript-eslint": "^8.64.0", "vitest": "^4.0.14" }, "engines": { @@ -2544,6 +2554,17 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -2555,6 +2576,16 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -2576,6 +2607,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2586,6 +2642,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2650,6 +2713,41 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", diff --git a/package.json b/package.json index 410dcd70..7edce9d1 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "elizaos": "./src" }, "scripts": { - "build": "tsc", + "build": "tsc -p tsconfig.build.json", "typecheck": "tsc --noEmit", "start:demo": "node dist/src/server.js", "test": "jest && node test/operational/captureOutcomesDerivation.test.mjs", @@ -24,6 +24,7 @@ "esm:check": "bash scripts/esm-check.sh" }, "devDependencies": { + "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/supertest": "^6.0.3", "jest": "^29.0.0", diff --git a/src/adapters/exchanges/blofinPriceFeedAdapter.ts b/src/adapters/exchanges/blofinPriceFeedAdapter.ts index e172c6e0..67eb3dfa 100644 --- a/src/adapters/exchanges/blofinPriceFeedAdapter.ts +++ b/src/adapters/exchanges/blofinPriceFeedAdapter.ts @@ -12,7 +12,7 @@ * @see https://docs.ccxt.com/ */ -import ccxt from "ccxt"; +import ccxt, { type OHLCV } from "ccxt"; import type { PriceFeedAdapter, OHLCVCandle, TickerSnapshot } from "./types.js"; import { toVenueSymbol } from "../symbolRegistry.js"; @@ -93,8 +93,14 @@ class BloFinPriceFeedAdapter implements PriceFeedAdapter { limit ); - // Transform ccxt format to our OHLCVCandle type - const candles: OHLCVCandle[] = ohlcvData.map((candle) => ({ + // Transform ccxt format to our OHLCVCandle type. + // ccxt types every OHLCV element as `Num` (number | undefined) — a venue + // MAY return a partial candle — while OHLCVCandle requires `number`. The + // assignment still compiles only because `exchange` below is `any`, so + // presence is asserted, not checked. That is the pre-existing runtime + // contract and is deliberately left unchanged: rejecting or defaulting a + // partial candle would alter scoring behaviour. Recorded as D8-R2. + const candles: OHLCVCandle[] = ohlcvData.map((candle: OHLCV) => ({ timestamp: candle[0], open: candle[1], high: candle[2], diff --git a/src/adapters/exchanges/coinbasePriceFeedAdapter.ts b/src/adapters/exchanges/coinbasePriceFeedAdapter.ts index a4b6b325..4f8479f2 100644 --- a/src/adapters/exchanges/coinbasePriceFeedAdapter.ts +++ b/src/adapters/exchanges/coinbasePriceFeedAdapter.ts @@ -15,7 +15,7 @@ * @see https://docs.ccxt.com/ */ -import ccxt from "ccxt"; +import ccxt, { type OHLCV } from "ccxt"; import type { PriceFeedAdapter, OHLCVCandle, TickerSnapshot } from "./types.js"; import { toVenueSymbol } from "../symbolRegistry.js"; @@ -84,8 +84,14 @@ class CoinbasePriceFeedAdapter implements PriceFeedAdapter { limit ); - // Transform ccxt format to our OHLCVCandle type - const candles: OHLCVCandle[] = ohlcvData.map((candle) => ({ + // Transform ccxt format to our OHLCVCandle type. + // ccxt types every OHLCV element as `Num` (number | undefined) — a venue + // MAY return a partial candle — while OHLCVCandle requires `number`. The + // assignment still compiles only because `exchange` below is `any`, so + // presence is asserted, not checked. That is the pre-existing runtime + // contract and is deliberately left unchanged: rejecting or defaulting a + // partial candle would alter scoring behaviour. Recorded as D8-R2. + const candles: OHLCVCandle[] = ohlcvData.map((candle: OHLCV) => ({ timestamp: candle[0], open: candle[1], high: candle[2], diff --git a/src/pipeline/nodes/mergeEnrichedView.ts b/src/pipeline/nodes/mergeEnrichedView.ts index 9660868e..58226f5d 100644 --- a/src/pipeline/nodes/mergeEnrichedView.ts +++ b/src/pipeline/nodes/mergeEnrichedView.ts @@ -32,6 +32,7 @@ import type { SupportedLens, } from "../../types/UssLenses.js"; import type { NewsFeatures } from "../../news/newsFeatures.js"; +import type { AfiMarketType, VenueType } from "../../utils/marketUtils.js"; import { ok, type AnalysisNodePlugin, @@ -53,6 +54,36 @@ export interface MergeNodeInput { parents: Record; } +/** + * The enriched view as it LEAVES this node: afi-core's analyst-facing + * `FroggyEnrichedView` plus three reactor-only runtime attachments. + * + * These three are deliberately NOT part of afi-core's contract — the analyst + * adapter and scorer never read them — and the Evidence V3 record structurally + * rejects them as runtime/storage baggage (`additionalProperties: false`; see + * pipeline/governed-schema/scored-signal-evidence.v3.schema.json). They exist + * for the scored-response projection and the analytics plane: + * - `lenses` → ReactorScoredSignalV1.lenses, analytics scoring_context + * - `_priceFeedMetadata` → graphScoringService, analytics scoring_context + * - `_enrichmentSummary` → NO READER in either repo (dormant; see D8-D3 in + * reports/afi-dormant-surface-inventory-v0.1.md) + * + * Declaring the extension keeps the attachment sites honest: they were + * previously three `as Record` casts, which suppressed the + * fact that this node widens a cross-repo type. + */ +interface ReactorEnrichedView extends FroggyEnrichedView { + lenses?: SupportedLens[]; + _priceFeedMetadata?: { + priceSource: string; + venueType: VenueType; + marketType: AfiMarketType; + technicalIndicators?: TechnicalLensV1["payload"]; + patternSignals?: PatternLensV1["payload"]; + }; + _enrichmentSummary?: string; +} + interface CategorizedResult { category: string; [field: string]: unknown; @@ -204,7 +235,7 @@ export function createMergeEnrichedViewNode(): AnalysisNodePlugin { enrichmentSummary += `. Pattern: ${pattern.patternName}`; } - const enriched: FroggyEnrichedView = { + const enriched: ReactorEnrichedView = { signalId, symbol, market: normalizedMarketType, @@ -222,15 +253,15 @@ export function createMergeEnrichedViewNode(): AnalysisNodePlugin { }, }; - (enriched as Record).lenses = lenses; - (enriched as Record)._priceFeedMetadata = { + enriched.lenses = lenses; + enriched._priceFeedMetadata = { priceSource: actualPriceSource, venueType, marketType: normalizedMarketType, technicalIndicators: technicalLensPayload || undefined, patternSignals: patternLensPayload || undefined, }; - (enriched as Record)._enrichmentSummary = enrichmentSummary; + enriched._enrichmentSummary = enrichmentSummary; ctx.logger.info("enriched view assembled", { categories: enrichedCategories }); return ok(enriched); diff --git a/src/server.ts b/src/server.ts index cfe51352..ffaab664 100644 --- a/src/server.ts +++ b/src/server.ts @@ -697,7 +697,14 @@ app.post("/api/ingest/cpj", async (req: Request, res: Response) => { const ingestHash = canonicalUss.provenance.ingestHash; const signalId = canonicalUss.provenance.signalId; - const duplicate = checkDuplicate(ingestHash); + // The governed USS v1.1 schema requires only source/providerId/signalId, so + // provenance.ingestHash is legitimately optional. mapCpjToUssV11 always + // populates it, so these guards are no-ops on every payload this route can + // produce today. They exist because an absent hash would otherwise be used + // as a literal cache key: with dedupe enabled (AFI_INGEST_DEDUPE=1) the + // first hashless signal would be recorded under `undefined` and every + // subsequent hashless signal rejected 409 as a duplicate. See D6. + const duplicate = ingestHash ? checkDuplicate(ingestHash) : undefined; if (duplicate) { console.warn(`⚠️ Duplicate CPJ ingestion detected:`, { ingestHash, @@ -715,7 +722,7 @@ app.post("/api/ingest/cpj", async (req: Request, res: Response) => { } // Record this ingest for future dedupe checks - recordIngest(ingestHash, signalId); + if (ingestHash) recordIngest(ingestHash, signalId); // ✅ STEP 4: Execute the RESOLVED registered composition through the // manifest-driven GraphExecutor (boot-validated registry composition). diff --git a/src/uss/cpjMapper.ts b/src/uss/cpjMapper.ts index 9da3162f..ea0772ad 100644 --- a/src/uss/cpjMapper.ts +++ b/src/uss/cpjMapper.ts @@ -177,7 +177,7 @@ function canonicalizeCpjForHashing(cpj: CpjV01Payload): any { if (Array.isArray(clone.extracted?.takeProfits)) { clone.extracted.takeProfits = clone.extracted.takeProfits .slice() // Copy array - .sort((a, b) => { + .sort((a: { price: number } | number, b: { price: number } | number) => { const priceA = typeof a === "object" ? a.price : a; const priceB = typeof b === "object" ? b.price : b; return priceA - priceB; @@ -188,7 +188,7 @@ function canonicalizeCpjForHashing(cpj: CpjV01Payload): any { if (Array.isArray(clone.extracted?.stopLoss)) { clone.extracted.stopLoss = clone.extracted.stopLoss .slice() - .sort((a, b) => a - b); + .sort((a: number, b: number) => a - b); } return clone; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..5925559d --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,24 @@ +{ + // BUILD config — what actually ships. + // + // tsconfig.json is the TYPECHECK config: it includes test/**/* so the compiler + // checks the suite (D8). That is correct for `tsc --noEmit` and for ts-jest, + // but `tsc` with an outDir would then EMIT the compiled tests into dist/ and + // ship test code in the production image. This config inherits every + // compilerOption (strict, bundler resolution, …) and narrows the input set + // back to the shipping sources. + // + // `npm run build` → this file (emit src only) + // `npm run typecheck` → tsconfig.json (check src + test, no emit) + "extends": "./tsconfig.json", + // Matches main's emitted output exactly: dist/src only, 87 .js files. + "include": [ + "src/**/*", + "typings.d.ts" + ], + "exclude": [ + "node_modules", + "dist", + "test" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 65d3d384..46fa2cf6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,24 @@ { "compilerOptions": { "target": "ES2022", - "module": "ESNext", - "moduleResolution": "node", + // D8: was module ESNext + moduleResolution "node". "node" (node10) cannot + // read afi-core's package `exports` map, and that failure is why the + // afi-core boundary was papered over with ambient `declare module` shims + // typing it as `any`. + // + // NodeNext is chosen over "bundler" deliberately: this package runs as real + // Node ESM ("type": "module", entrypoint dist/src/server.js), so NodeNext is + // the semantics that actually apply at runtime. It resolves the exports map + // AND enforces `.js`-suffixed relative specifiers at compile time — which + // "bundler" does not, leaving that invariant to scripts/esm-check.sh (which + // is not run by CI). Measured: both settings typecheck clean; NodeNext is + // the one that cannot drift. + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./", - "strict": false, + "strict": true, + // Third-party declaration files only; does NOT relax the afi-core seam. "skipLibCheck": true, "allowJs": true, "resolveJsonModule": true, @@ -18,13 +31,27 @@ }, "include": [ "src/**/*", + "test/**/*", "typings.d.ts" ], + // The excluded test paths mirror jest.config.js `testPathIgnorePatterns` + // exactly: they are the files jest never executes, so typechecking them would + // gate CI on code that no longer runs. Both currently-rotten ones are recorded + // as dormant surfaces (D8-D1/D8-D2 in + // reports/afi-dormant-surface-inventory-v0.1.md) with deletion proposed: + // - test/receiptProvenanceService.test.ts imports src/types/TssdSignalDocument.js, + // which does not exist (retired TSSD framing) — it can never run. + // - test/uss/ussMapper.test.ts calls mapTradingViewToUssV11 with 1 arg; the + // function has required 2 since resolution-precedes-mapping landed, and + // both live callers pass 2. + // Keep this list in sync with jest.config.js. "exclude": [ "node_modules", "dist", - "**/*.test.ts", - "**/*.spec.ts" + "test/receiptProvenanceService.test.ts", + "test/vaultReplayService.test.ts", + "test/uss", + "test/cpj" ], "ts-node": { "esm": true diff --git a/typings.d.ts b/typings.d.ts index 11346b32..87c71388 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -1,93 +1,23 @@ -// Legacy ambient typings shim for older tests/missing module declarations. -// Prefer real @types packages; this is a fallback only. - -// Optional: global placeholders for test frameworks -declare var describe: any; -declare var it: any; -declare var test: any; - -// Minimal ambient modules for build-time compatibility (scored-only public surface) -declare module "afi-core/analyst" { - export type AnalystScoreTemplate = any; -} -declare module "afi-core/analysts/froggy.enrichment_adapter.js" { - export type FroggyEnrichedView = any; - export function buildFroggyTrendPullbackInputFromEnriched(enriched: any): any; -} -declare module "afi-core/analysts/froggy.trend_pullback_v1.js" { - export type FroggyTrendPullbackScore = any; - export function scoreFroggyTrendPullback(input: any, config?: any, enriched?: any): any; - export function scoreFroggyTrendPullbackFromEnriched(enriched: any): any; -} -declare module "afi-core/validators/ValidatorDecision.js" { - export type ValidatorDecisionBase = any; -} -declare module "afi-core/validators/NoveltyScorer.js" { - export type NoveltySignalInput = any; -} -declare module "afi-core/validators/NoveltyTypes.js" { - export type NoveltyResult = any; - export type NoveltyClass = any; -} -declare module "afi-core/decay" { - export type DecayParams = any; - export const DEFAULT_DECAY_TEMPLATES_BY_HORIZON: Record< - string, - { templateId: string; [key: string]: any } - >; - export function pickDecayParamsForAnalystScore(...args: any[]): any; -} -declare module "ccxt" { - const ccxt: any; - export = ccxt; -} -declare module "node-telegram-bot-api" { - export default class TelegramBot { - constructor(token: string, options?: any); - on(event: string, callback: (...args: any[]) => void): void; - sendMessage(chatId: any, text: string, opts?: any): void; - } -} -declare module "telegram" { - export class TelegramClient { - constructor(session: any, apiId: any, apiHash: any, options?: any); - connect(): Promise; - start(options: any): Promise; - } - export class StringSession { - constructor(session?: string); - save(): string; - } - export type Message = any; -} -declare module "telegram/events/index.js" { - export class NewMessage { - constructor(options?: any); - } - export type NewMessageEvent = any; -} -declare module "telegram/sessions/index.js" { - export const StringSession: any; -} +// Ambient module declarations — the JUSTIFIED MINIMUM. +// +// An ambient `declare module` BEATS real module resolution: if a package is +// declared here, TypeScript binds every import of it to this declaration even +// when the real types resolve successfully. This file previously declared 16 +// modules, 7 of them typing the whole afi-core boundary as `any`, which is why +// no compiler had ever checked that seam (D8). +// +// Everything else was removed because the package ships real types that this +// file was shadowing: afi-core (via its `exports` map, reached by +// moduleResolution "bundler"), ajv, ajv-formats, ccxt, trading-signals, +// telegram, node-telegram-bot-api, plus express via @types/express and the +// jest globals via @types/jest. +// +// DO NOT add a module here to silence a type error. If the package ships types, +// fix resolution; if it does not, install its @types package. Only a package +// with no types from any source belongs below. +// +// `input` (v1.0.1) ships no type declarations and has no @types package. declare module "input" { const input: any; export = input; } -declare module "ajv" { - export type ValidateFunction = any; - export type ErrorObject = any; - export class Ajv { - constructor(options?: any); - addSchema(schema: any, key?: string): void; - getSchema(key: string): ValidateFunction | undefined; - compile(schema: any): ValidateFunction; - addVocabulary(keywords: string[]): void; - } - export default Ajv; -} -declare module "ajv-formats" { - export default function addFormats(ajv: any): void; -} -declare module "trading-signals" { - export const StochasticRSI: any; -} From 51fc929cb7acbddfb80ddcdcdd955d065839f89d Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 4 Aug 2026 05:01:55 -0400 Subject: [PATCH 2/2] D8 review fixes: NodeNext, honest tsconfig comment, lockfile hygiene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- package-lock.json | 11 +---------- src/adapters/exchanges/blofinPriceFeedAdapter.ts | 8 +++++++- .../exchanges/coinbasePriceFeedAdapter.ts | 8 +++++++- tsconfig.json | 15 ++++++++++++--- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a3482b5..44c2d358 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,19 +45,10 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/node": "^26.1.1", - "@vitest/coverage-v8": "^4.1.10", + "@types/node": "^20.11.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", - "eslint": "^10.7.0", - "husky": "^9.1.7", - "jscpd": "^5.0.12", - "knip": "^6.27.0", - "lint-staged": "^17.1.0", - "prettier": "^3.9.5", "typescript": "^5.4.0", - "typescript-eslint": "^8.64.0", "vitest": "^4.0.14" }, "engines": { diff --git a/src/adapters/exchanges/blofinPriceFeedAdapter.ts b/src/adapters/exchanges/blofinPriceFeedAdapter.ts index 67eb3dfa..b45df048 100644 --- a/src/adapters/exchanges/blofinPriceFeedAdapter.ts +++ b/src/adapters/exchanges/blofinPriceFeedAdapter.ts @@ -28,7 +28,13 @@ class BloFinPriceFeedAdapter implements PriceFeedAdapter { public readonly supportsPerps = true; public readonly supportsSpot = true; - private exchange: any; // ccxt.Exchange type + // D8-R2: this `any` is why the OHLCV mapping below type-checks at all. + // Typing it `ccxt.Exchange` surfaces that ccxt declares every OHLCV + // element as `Num` (number | undefined) and `ticker.symbol` as `Str`, + // neither of which satisfies our OHLCVCandle/TickerSnapshot contracts. + // Left as-is deliberately: tightening it forces a reject-or-default + // decision that changes scoring behaviour. See the D8 report. + private exchange: any; // ccxt.Exchange constructor() { // Initialize ccxt BloFin exchange diff --git a/src/adapters/exchanges/coinbasePriceFeedAdapter.ts b/src/adapters/exchanges/coinbasePriceFeedAdapter.ts index 4f8479f2..00dcc712 100644 --- a/src/adapters/exchanges/coinbasePriceFeedAdapter.ts +++ b/src/adapters/exchanges/coinbasePriceFeedAdapter.ts @@ -31,7 +31,13 @@ class CoinbasePriceFeedAdapter implements PriceFeedAdapter { public readonly supportsPerps = false; // Coinbase spot only public readonly supportsSpot = true; - private exchange: any; // ccxt.Exchange type + // D8-R2: this `any` is why the OHLCV mapping below type-checks at all. + // Typing it `ccxt.Exchange` surfaces that ccxt declares every OHLCV + // element as `Num` (number | undefined) and `ticker.symbol` as `Str`, + // neither of which satisfies our OHLCVCandle/TickerSnapshot contracts. + // Left as-is deliberately: tightening it forces a reject-or-default + // decision that changes scoring behaviour. See the D8 report. + private exchange: any; // ccxt.Exchange constructor() { // Initialize ccxt Coinbase exchange diff --git a/tsconfig.json b/tsconfig.json index 46fa2cf6..fd04609e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,9 +35,18 @@ "typings.d.ts" ], // The excluded test paths mirror jest.config.js `testPathIgnorePatterns` - // exactly: they are the files jest never executes, so typechecking them would - // gate CI on code that no longer runs. Both currently-rotten ones are recorded - // as dormant surfaces (D8-D1/D8-D2 in + // exactly — the files jest is explicitly configured to skip. They are excluded + // because both currently-rotten ones would otherwise gate CI on code that no + // longer runs. + // + // NOTE: this is NOT the same as "everything jest never executes". jest's + // `testMatch` is an ALLOWLIST, so three further test files match no pattern + // and never run either, yet are still typechecked here (deliberately — they + // compile clean, and typechecking them is what would catch them rotting). + // They are recorded as D8-D3. Keep this list in sync with + // `testPathIgnorePatterns`, not with `testMatch`. + // + // The two rotten exclusions are recorded as dormant surfaces (D8-D1/D8-D2 in // reports/afi-dormant-surface-inventory-v0.1.md) with deletion proposed: // - test/receiptProvenanceService.test.ts imports src/types/TssdSignalDocument.js, // which does not exist (retired TSSD framing) — it can never run.