Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/validate-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 89 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
20 changes: 16 additions & 4 deletions src/adapters/exchanges/blofinPriceFeedAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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
Expand Down Expand Up @@ -93,8 +99,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],
Expand Down
20 changes: 16 additions & 4 deletions src/adapters/exchanges/coinbasePriceFeedAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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
Expand Down Expand Up @@ -84,8 +90,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],
Expand Down
39 changes: 35 additions & 4 deletions src/pipeline/nodes/mergeEnrichedView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -53,6 +54,36 @@ export interface MergeNodeInput {
parents: Record<string, unknown>;
}

/**
* 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<string, unknown>` 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;
Expand Down Expand Up @@ -204,7 +235,7 @@ export function createMergeEnrichedViewNode(): AnalysisNodePlugin {
enrichmentSummary += `. Pattern: ${pattern.patternName}`;
}

const enriched: FroggyEnrichedView = {
const enriched: ReactorEnrichedView = {
signalId,
symbol,
market: normalizedMarketType,
Expand All @@ -222,15 +253,15 @@ export function createMergeEnrichedViewNode(): AnalysisNodePlugin {
},
};

(enriched as Record<string, unknown>).lenses = lenses;
(enriched as Record<string, unknown>)._priceFeedMetadata = {
enriched.lenses = lenses;
enriched._priceFeedMetadata = {
priceSource: actualPriceSource,
venueType,
marketType: normalizedMarketType,
technicalIndicators: technicalLensPayload || undefined,
patternSignals: patternLensPayload || undefined,
};
(enriched as Record<string, unknown>)._enrichmentSummary = enrichmentSummary;
enriched._enrichmentSummary = enrichmentSummary;

ctx.logger.info("enriched view assembled", { categories: enrichedCategories });
return ok(enriched);
Expand Down
11 changes: 9 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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).
Expand Down
4 changes: 2 additions & 2 deletions src/uss/cpjMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Loading
Loading