@tradejs/app 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -10
- package/src/app/actions/strategies.ts +4 -2
- package/src/app/api/ai/route.ts +21 -50
- package/src/app/api/strategies/runtime/route.ts +1 -1
- package/src/app/components/Strategies/RuntimeStrategyCard.presenter.ts +5 -2
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +1 -2
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +1 -2
- package/src/app/components/Strategies/RuntimeStrategyStatsDrawer.tsx +1 -2
- package/src/app/lib/connectorCreator.ts +1 -10
- package/src/app/routes/derivatives/DerivativesDashboardView.tsx +900 -0
- package/src/app/routes/derivatives/derivativesDashboardConfig.ts +53 -0
- package/src/app/routes/derivatives/derivativesDashboardLoader.ts +74 -0
- package/src/app/routes/derivatives/page.tsx +18 -1087
- package/src/app/routes/derivatives/useDerivativesDashboard.ts +105 -0
- package/src/app/routes/strategies/StrategiesPageClient.tsx +4 -2
- package/src/app/lib/runtimeDashboard.ts +0 -700
- package/src/app/lib/runtimeStrategies.ts +0 -1107
- package/src/app/lib/runtimeStrategyContracts.ts +0 -85
- package/src/app/lib/runtimeStrategyLineage.ts +0 -264
- package/src/app/lib/runtimeTradeReconciliation.ts +0 -113
- package/src/app/lib/runtimeTradeSync.ts +0 -280
- package/src/app/lib/strategyEvidenceTimeline.ts +0 -298
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { TTL_1M } from '@tradejs/core/constants';
|
|
2
|
-
import { getRuntimeStorageDayKey } from '@tradejs/core/time';
|
|
3
|
-
import {
|
|
4
|
-
delKey,
|
|
5
|
-
getData,
|
|
6
|
-
redisKeys,
|
|
7
|
-
setData,
|
|
8
|
-
setHashJsonField,
|
|
9
|
-
} from '@tradejs/infra/redis';
|
|
10
|
-
import type {
|
|
11
|
-
ClosedPnlRecord,
|
|
12
|
-
Connector,
|
|
13
|
-
PositionPnlSnapshot,
|
|
14
|
-
RuntimeTradeRecord,
|
|
15
|
-
} from '@tradejs/types';
|
|
16
|
-
import { takeClosedPnlMatch } from './runtimeTradeReconciliation';
|
|
17
|
-
|
|
18
|
-
export type ClosedPnlRecordWithOrderLinkId = ClosedPnlRecord & {
|
|
19
|
-
orderLinkId?: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const getRuntimeTradeScopeId = (trade: RuntimeTradeRecord) =>
|
|
23
|
-
trade.deploymentId ?? trade.accountId;
|
|
24
|
-
|
|
25
|
-
export const isRuntimeTradeInConnectorScope = (
|
|
26
|
-
trade: RuntimeTradeRecord,
|
|
27
|
-
connector: Connector,
|
|
28
|
-
) => {
|
|
29
|
-
if (trade.deploymentId && trade.deploymentId !== connector.deploymentId) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
if (trade.deploymentId && !connector.deploymentId) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
if (trade.accountId && trade.accountId !== connector.accountId) {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
if (trade.accountId && !connector.accountId) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return (trade.universe ?? 'crypto') === connector.universe;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const buildRiskLevelsAnalysis = (position: PositionPnlSnapshot) => {
|
|
46
|
-
const takeProfitPrice =
|
|
47
|
-
typeof position.takeProfitPrice === 'number' &&
|
|
48
|
-
Number.isFinite(position.takeProfitPrice)
|
|
49
|
-
? position.takeProfitPrice
|
|
50
|
-
: null;
|
|
51
|
-
const stopLossPrice =
|
|
52
|
-
typeof position.stopLossPrice === 'number' &&
|
|
53
|
-
Number.isFinite(position.stopLossPrice)
|
|
54
|
-
? position.stopLossPrice
|
|
55
|
-
: null;
|
|
56
|
-
|
|
57
|
-
if (takeProfitPrice == null && stopLossPrice == null) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
...(takeProfitPrice != null ? { takeProfitPrice } : {}),
|
|
63
|
-
...(stopLossPrice != null ? { stopLossPrice } : {}),
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const hasExchangeCloseDetails = (trade: RuntimeTradeRecord) =>
|
|
68
|
-
trade.status === 'closed' &&
|
|
69
|
-
typeof trade.exitPrice === 'number' &&
|
|
70
|
-
Number.isFinite(trade.exitPrice) &&
|
|
71
|
-
typeof trade.actualExitPrice === 'number' &&
|
|
72
|
-
Number.isFinite(trade.actualExitPrice) &&
|
|
73
|
-
typeof trade.closedPnl === 'number' &&
|
|
74
|
-
Number.isFinite(trade.closedPnl) &&
|
|
75
|
-
typeof trade.openFee === 'number' &&
|
|
76
|
-
Number.isFinite(trade.openFee) &&
|
|
77
|
-
typeof trade.closeFee === 'number' &&
|
|
78
|
-
Number.isFinite(trade.closeFee);
|
|
79
|
-
|
|
80
|
-
export const syncRuntimeTrades = async ({
|
|
81
|
-
userName,
|
|
82
|
-
connector,
|
|
83
|
-
trades,
|
|
84
|
-
endTime,
|
|
85
|
-
openPositions,
|
|
86
|
-
openPositionsReliable,
|
|
87
|
-
closedPnlRows,
|
|
88
|
-
}: {
|
|
89
|
-
userName: string;
|
|
90
|
-
connector: Connector;
|
|
91
|
-
trades: RuntimeTradeRecord[];
|
|
92
|
-
endTime: number;
|
|
93
|
-
openPositions: PositionPnlSnapshot[];
|
|
94
|
-
openPositionsReliable: boolean;
|
|
95
|
-
closedPnlRows: ClosedPnlRecordWithOrderLinkId[];
|
|
96
|
-
}) => {
|
|
97
|
-
const openPositionsBySymbol = new Map(
|
|
98
|
-
openPositions.map((position) => [position.symbol, position]),
|
|
99
|
-
);
|
|
100
|
-
const activeOrderIdByKey = new Map<string, string | null>();
|
|
101
|
-
const activeTradeKeys = [
|
|
102
|
-
...new Set(
|
|
103
|
-
trades
|
|
104
|
-
.filter((trade) => isRuntimeTradeInConnectorScope(trade, connector))
|
|
105
|
-
.map((trade) =>
|
|
106
|
-
redisKeys.runtimeActiveTrade(
|
|
107
|
-
userName,
|
|
108
|
-
trade.symbol,
|
|
109
|
-
getRuntimeTradeScopeId(trade),
|
|
110
|
-
),
|
|
111
|
-
),
|
|
112
|
-
),
|
|
113
|
-
];
|
|
114
|
-
|
|
115
|
-
await Promise.all(
|
|
116
|
-
activeTradeKeys.map(async (key) => {
|
|
117
|
-
const activeRef = (await getData(key, null)) as {
|
|
118
|
-
orderId?: string;
|
|
119
|
-
} | null;
|
|
120
|
-
activeOrderIdByKey.set(
|
|
121
|
-
key,
|
|
122
|
-
typeof activeRef?.orderId === 'string' ? activeRef.orderId : null,
|
|
123
|
-
);
|
|
124
|
-
}),
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const exactByOrderLinkId = new Map(
|
|
128
|
-
closedPnlRows
|
|
129
|
-
.filter(
|
|
130
|
-
(
|
|
131
|
-
row,
|
|
132
|
-
): row is ClosedPnlRecordWithOrderLinkId & { orderLinkId: string } =>
|
|
133
|
-
typeof row.orderLinkId === 'string' && row.orderLinkId.length > 0,
|
|
134
|
-
)
|
|
135
|
-
.map((row) => [row.orderLinkId, row]),
|
|
136
|
-
);
|
|
137
|
-
const exactByOrderId = new Map(
|
|
138
|
-
closedPnlRows
|
|
139
|
-
.filter(
|
|
140
|
-
(row): row is ClosedPnlRecordWithOrderLinkId & { orderId: string } =>
|
|
141
|
-
typeof row.orderId === 'string' && row.orderId.length > 0,
|
|
142
|
-
)
|
|
143
|
-
.map((row) => [row.orderId, row]),
|
|
144
|
-
);
|
|
145
|
-
const symbolBuckets = new Map<string, ClosedPnlRecordWithOrderLinkId[]>();
|
|
146
|
-
|
|
147
|
-
for (const row of closedPnlRows) {
|
|
148
|
-
const bucket = symbolBuckets.get(row.symbol) ?? [];
|
|
149
|
-
bucket.push(row);
|
|
150
|
-
symbolBuckets.set(row.symbol, bucket);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const syncedTrades: RuntimeTradeRecord[] = [];
|
|
154
|
-
|
|
155
|
-
for (const trade of trades) {
|
|
156
|
-
if (!isRuntimeTradeInConnectorScope(trade, connector)) {
|
|
157
|
-
syncedTrades.push(trade);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const activeTradeKey = redisKeys.runtimeActiveTrade(
|
|
162
|
-
userName,
|
|
163
|
-
trade.symbol,
|
|
164
|
-
getRuntimeTradeScopeId(trade),
|
|
165
|
-
);
|
|
166
|
-
const isCurrentActiveTrade =
|
|
167
|
-
activeOrderIdByKey.get(activeTradeKey) === trade.orderId;
|
|
168
|
-
|
|
169
|
-
if (hasExchangeCloseDetails(trade)) {
|
|
170
|
-
if (isCurrentActiveTrade) {
|
|
171
|
-
await delKey(activeTradeKey);
|
|
172
|
-
}
|
|
173
|
-
syncedTrades.push(trade);
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const openPosition = openPositionsBySymbol.get(trade.symbol);
|
|
178
|
-
|
|
179
|
-
if (trade.status === 'active' && !openPositionsReliable) {
|
|
180
|
-
syncedTrades.push(trade);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
trade.status === 'active' &&
|
|
186
|
-
isCurrentActiveTrade &&
|
|
187
|
-
openPosition &&
|
|
188
|
-
openPosition.direction === trade.direction
|
|
189
|
-
) {
|
|
190
|
-
const riskLevelsAnalysis = buildRiskLevelsAnalysis(openPosition);
|
|
191
|
-
const nextTrade: RuntimeTradeRecord = {
|
|
192
|
-
...trade,
|
|
193
|
-
status: 'active',
|
|
194
|
-
currentPrice: openPosition.currentPrice,
|
|
195
|
-
currentPnl: openPosition.unrealizedPnl,
|
|
196
|
-
aiAnalysis: riskLevelsAnalysis
|
|
197
|
-
? { ...(trade.aiAnalysis ?? {}), ...riskLevelsAnalysis }
|
|
198
|
-
: trade.aiAnalysis,
|
|
199
|
-
lastSyncedAt: endTime,
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
await Promise.all([
|
|
203
|
-
setData(redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
204
|
-
expire: 0,
|
|
205
|
-
}),
|
|
206
|
-
setHashJsonField(
|
|
207
|
-
redisKeys.runtimeTradeBucket(
|
|
208
|
-
userName,
|
|
209
|
-
getRuntimeStorageDayKey(trade.entryTimestamp),
|
|
210
|
-
),
|
|
211
|
-
trade.orderId,
|
|
212
|
-
nextTrade,
|
|
213
|
-
{ expire: 0 },
|
|
214
|
-
),
|
|
215
|
-
]);
|
|
216
|
-
syncedTrades.push(nextTrade);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const matchedClosedPnl = takeClosedPnlMatch({
|
|
221
|
-
exactByOrderLinkId,
|
|
222
|
-
exactByOrderId,
|
|
223
|
-
symbolBuckets,
|
|
224
|
-
trade,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
if (!matchedClosedPnl) {
|
|
228
|
-
syncedTrades.push(trade);
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const nextTrade: RuntimeTradeRecord = {
|
|
233
|
-
...trade,
|
|
234
|
-
status: 'closed',
|
|
235
|
-
currentPrice: matchedClosedPnl.exitPrice ?? trade.currentPrice ?? null,
|
|
236
|
-
currentPnl: matchedClosedPnl.closedPnl,
|
|
237
|
-
closedPnl: matchedClosedPnl.closedPnl,
|
|
238
|
-
actualEntryPrice:
|
|
239
|
-
matchedClosedPnl.entryPrice ?? trade.actualEntryPrice ?? null,
|
|
240
|
-
exitPrice: matchedClosedPnl.exitPrice ?? trade.exitPrice ?? null,
|
|
241
|
-
actualExitPrice:
|
|
242
|
-
matchedClosedPnl.exitPrice ?? trade.actualExitPrice ?? null,
|
|
243
|
-
exitTimestamp: matchedClosedPnl.closedAt,
|
|
244
|
-
exitType: trade.exitType ?? null,
|
|
245
|
-
openFee: matchedClosedPnl.openFee ?? trade.openFee ?? null,
|
|
246
|
-
closeFee: matchedClosedPnl.closeFee ?? trade.closeFee ?? null,
|
|
247
|
-
fundingFee: matchedClosedPnl.fundingFee ?? trade.fundingFee ?? null,
|
|
248
|
-
totalFee: matchedClosedPnl.totalFee ?? trade.totalFee ?? null,
|
|
249
|
-
lastSyncedAt: endTime,
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
await Promise.all([
|
|
253
|
-
setData(redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
254
|
-
expire: TTL_1M,
|
|
255
|
-
}),
|
|
256
|
-
setHashJsonField(
|
|
257
|
-
redisKeys.runtimeTradeBucket(
|
|
258
|
-
userName,
|
|
259
|
-
getRuntimeStorageDayKey(trade.entryTimestamp),
|
|
260
|
-
),
|
|
261
|
-
trade.orderId,
|
|
262
|
-
nextTrade,
|
|
263
|
-
{ expire: TTL_1M },
|
|
264
|
-
),
|
|
265
|
-
setHashJsonField(
|
|
266
|
-
redisKeys.runtimeClosedTradeBucket(
|
|
267
|
-
userName,
|
|
268
|
-
getRuntimeStorageDayKey(nextTrade.exitTimestamp!),
|
|
269
|
-
),
|
|
270
|
-
trade.orderId,
|
|
271
|
-
nextTrade,
|
|
272
|
-
{ expire: TTL_1M },
|
|
273
|
-
),
|
|
274
|
-
...(isCurrentActiveTrade ? [delKey(activeTradeKey)] : []),
|
|
275
|
-
]);
|
|
276
|
-
syncedTrades.push(nextTrade);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return syncedTrades;
|
|
280
|
-
};
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import type { Dirent } from 'node:fs';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
canonicalStrategyEvidenceJson,
|
|
6
|
-
safeStrategyEvidenceSegment,
|
|
7
|
-
verifyStrategyEvidenceMarkerEnvelope,
|
|
8
|
-
} from '@tradejs/infra/strategyReleaseEvidence';
|
|
9
|
-
import {
|
|
10
|
-
type StrategyEvidenceMarker,
|
|
11
|
-
type StrategyEvidenceMarkerEnvelope,
|
|
12
|
-
type StrategyEvidenceTimeline,
|
|
13
|
-
type StrategyEvidenceTimelineSelector,
|
|
14
|
-
} from '@tradejs/types';
|
|
15
|
-
|
|
16
|
-
const DEFAULT_MARKER_DIRECTORY = 'data/strategy-release/markers';
|
|
17
|
-
type JsonRecord = Record<string, unknown>;
|
|
18
|
-
|
|
19
|
-
const asRecord = (value: unknown): JsonRecord | null =>
|
|
20
|
-
value && typeof value === 'object' && !Array.isArray(value)
|
|
21
|
-
? (value as JsonRecord)
|
|
22
|
-
: null;
|
|
23
|
-
|
|
24
|
-
const isNonEmptyString = (value: unknown): value is string =>
|
|
25
|
-
typeof value === 'string' && value.trim().length > 0;
|
|
26
|
-
|
|
27
|
-
export { canonicalStrategyEvidenceJson, verifyStrategyEvidenceMarkerEnvelope };
|
|
28
|
-
|
|
29
|
-
export const strategyEvidenceTimelineSelectorKey = (
|
|
30
|
-
selector: StrategyEvidenceTimelineSelector,
|
|
31
|
-
) =>
|
|
32
|
-
[
|
|
33
|
-
selector.strategy,
|
|
34
|
-
selector.compositionId ?? '',
|
|
35
|
-
selector.gitSha ?? '',
|
|
36
|
-
selector.gateFingerprint ?? '',
|
|
37
|
-
selector.configFingerprint ?? '',
|
|
38
|
-
selector.contextFingerprint ?? '',
|
|
39
|
-
selector.requireCompleteLineage ? 'exact' : 'partial',
|
|
40
|
-
].join(':');
|
|
41
|
-
|
|
42
|
-
const discoverJsonFiles = async (rootDir: string): Promise<string[]> => {
|
|
43
|
-
const files: string[] = [];
|
|
44
|
-
|
|
45
|
-
const visit = async (directory: string, depth: number): Promise<void> => {
|
|
46
|
-
if (depth > 12) return;
|
|
47
|
-
|
|
48
|
-
let entries: Dirent[];
|
|
49
|
-
try {
|
|
50
|
-
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
await Promise.all(
|
|
57
|
-
entries.map(async (entry) => {
|
|
58
|
-
if (entry.name.startsWith('.') || entry.name.includes('.tmp-')) return;
|
|
59
|
-
const entryPath = path.join(directory, entry.name);
|
|
60
|
-
if (entry.isDirectory()) {
|
|
61
|
-
await visit(entryPath, depth + 1);
|
|
62
|
-
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
63
|
-
files.push(entryPath);
|
|
64
|
-
}
|
|
65
|
-
}),
|
|
66
|
-
);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
await visit(rootDir, 0);
|
|
70
|
-
return files.sort();
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const inferMatchingStrategies = ({
|
|
74
|
-
filePath,
|
|
75
|
-
rootDir,
|
|
76
|
-
parsed,
|
|
77
|
-
strategies,
|
|
78
|
-
}: {
|
|
79
|
-
filePath: string;
|
|
80
|
-
rootDir: string;
|
|
81
|
-
parsed: unknown;
|
|
82
|
-
strategies: string[];
|
|
83
|
-
}) => {
|
|
84
|
-
const payload = asRecord(asRecord(parsed)?.payload);
|
|
85
|
-
const declaredStrategy = isNonEmptyString(payload?.strategy)
|
|
86
|
-
? payload.strategy
|
|
87
|
-
: null;
|
|
88
|
-
const directoryStrategy = path.relative(rootDir, filePath).split(path.sep)[0];
|
|
89
|
-
return strategies.filter(
|
|
90
|
-
(strategy) =>
|
|
91
|
-
strategy === declaredStrategy ||
|
|
92
|
-
directoryStrategy === safeStrategyEvidenceSegment(strategy),
|
|
93
|
-
);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const missingTimeline = (): StrategyEvidenceTimeline => ({
|
|
97
|
-
status: 'missing',
|
|
98
|
-
observedFrom: null,
|
|
99
|
-
markers: [],
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const invalidTimeline = (): StrategyEvidenceTimeline => ({
|
|
103
|
-
status: 'invalid',
|
|
104
|
-
observedFrom: null,
|
|
105
|
-
markers: [],
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
export const loadStrategyEvidenceTimelines = async ({
|
|
109
|
-
projectRoot,
|
|
110
|
-
markerDir,
|
|
111
|
-
selectors: requestedSelectors,
|
|
112
|
-
startTime,
|
|
113
|
-
endTime,
|
|
114
|
-
}: {
|
|
115
|
-
projectRoot: string;
|
|
116
|
-
markerDir?: string | null;
|
|
117
|
-
selectors: Iterable<StrategyEvidenceTimelineSelector>;
|
|
118
|
-
startTime: number;
|
|
119
|
-
endTime: number;
|
|
120
|
-
}): Promise<Map<string, StrategyEvidenceTimeline>> => {
|
|
121
|
-
const selectors = [...requestedSelectors]
|
|
122
|
-
.filter((selector) => selector.strategy.trim().length > 0)
|
|
123
|
-
.sort((left, right) =>
|
|
124
|
-
strategyEvidenceTimelineSelectorKey(left).localeCompare(
|
|
125
|
-
strategyEvidenceTimelineSelectorKey(right),
|
|
126
|
-
),
|
|
127
|
-
);
|
|
128
|
-
const strategies = [...new Set(selectors.map(({ strategy }) => strategy))];
|
|
129
|
-
const timelines = new Map(
|
|
130
|
-
selectors.map((selector) => [
|
|
131
|
-
strategyEvidenceTimelineSelectorKey(selector),
|
|
132
|
-
missingTimeline(),
|
|
133
|
-
]),
|
|
134
|
-
);
|
|
135
|
-
if (!selectors.length) return timelines;
|
|
136
|
-
|
|
137
|
-
const configuredDir = markerDir?.trim() || DEFAULT_MARKER_DIRECTORY;
|
|
138
|
-
const rootDir = path.isAbsolute(configuredDir)
|
|
139
|
-
? configuredDir
|
|
140
|
-
: path.resolve(projectRoot, configuredDir);
|
|
141
|
-
let files: string[];
|
|
142
|
-
try {
|
|
143
|
-
files = await discoverJsonFiles(rootDir);
|
|
144
|
-
} catch {
|
|
145
|
-
for (const selector of selectors) {
|
|
146
|
-
timelines.set(
|
|
147
|
-
strategyEvidenceTimelineSelectorKey(selector),
|
|
148
|
-
invalidTimeline(),
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
return timelines;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const envelopesByStrategy = new Map<
|
|
155
|
-
string,
|
|
156
|
-
StrategyEvidenceMarkerEnvelope[]
|
|
157
|
-
>();
|
|
158
|
-
const invalidStrategies = new Set<string>();
|
|
159
|
-
|
|
160
|
-
for (const filePath of files) {
|
|
161
|
-
let parsed: unknown = null;
|
|
162
|
-
try {
|
|
163
|
-
parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
|
|
164
|
-
} catch {
|
|
165
|
-
for (const strategy of inferMatchingStrategies({
|
|
166
|
-
filePath,
|
|
167
|
-
rootDir,
|
|
168
|
-
parsed,
|
|
169
|
-
strategies,
|
|
170
|
-
})) {
|
|
171
|
-
invalidStrategies.add(strategy);
|
|
172
|
-
}
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const matchingStrategies = inferMatchingStrategies({
|
|
177
|
-
filePath,
|
|
178
|
-
rootDir,
|
|
179
|
-
parsed,
|
|
180
|
-
strategies,
|
|
181
|
-
});
|
|
182
|
-
if (!matchingStrategies.length) continue;
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
const envelope = verifyStrategyEvidenceMarkerEnvelope(parsed);
|
|
186
|
-
for (const strategy of matchingStrategies) {
|
|
187
|
-
if (strategy !== envelope.payload.strategy) {
|
|
188
|
-
invalidStrategies.add(strategy);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (!strategies.includes(envelope.payload.strategy)) {
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
const envelopes =
|
|
195
|
-
envelopesByStrategy.get(envelope.payload.strategy) ?? [];
|
|
196
|
-
envelopes.push(envelope);
|
|
197
|
-
envelopesByStrategy.set(envelope.payload.strategy, envelopes);
|
|
198
|
-
} catch {
|
|
199
|
-
for (const strategy of matchingStrategies) {
|
|
200
|
-
invalidStrategies.add(strategy);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
for (const selector of selectors) {
|
|
206
|
-
const strategy = selector.strategy;
|
|
207
|
-
const selectorKey = strategyEvidenceTimelineSelectorKey(selector);
|
|
208
|
-
if (invalidStrategies.has(strategy)) {
|
|
209
|
-
timelines.set(selectorKey, invalidTimeline());
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const envelopes = envelopesByStrategy.get(strategy) ?? [];
|
|
214
|
-
if (!envelopes.length) continue;
|
|
215
|
-
const hasCompleteSelector =
|
|
216
|
-
Boolean(selector.compositionId) &&
|
|
217
|
-
Boolean(selector.gitSha) &&
|
|
218
|
-
Boolean(selector.gateFingerprint) &&
|
|
219
|
-
Boolean(selector.configFingerprint) &&
|
|
220
|
-
Boolean(selector.contextFingerprint);
|
|
221
|
-
if (selector.requireCompleteLineage && !hasCompleteSelector) continue;
|
|
222
|
-
|
|
223
|
-
const markersById = new Map<string, StrategyEvidenceMarker>();
|
|
224
|
-
let hasConflict = false;
|
|
225
|
-
for (const envelope of envelopes) {
|
|
226
|
-
for (const marker of envelope.payload.markers) {
|
|
227
|
-
const existing = markersById.get(marker.id);
|
|
228
|
-
if (
|
|
229
|
-
existing &&
|
|
230
|
-
canonicalStrategyEvidenceJson(existing) !==
|
|
231
|
-
canonicalStrategyEvidenceJson(marker)
|
|
232
|
-
) {
|
|
233
|
-
hasConflict = true;
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
markersById.set(marker.id, marker);
|
|
237
|
-
}
|
|
238
|
-
if (hasConflict) break;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (hasConflict) {
|
|
242
|
-
timelines.set(selectorKey, invalidTimeline());
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const matchingMarkers = [...markersById.values()]
|
|
247
|
-
.filter(
|
|
248
|
-
(marker) =>
|
|
249
|
-
(!selector.compositionId ||
|
|
250
|
-
marker.compositionId === selector.compositionId) &&
|
|
251
|
-
(!selector.gitSha || marker.gitSha === selector.gitSha) &&
|
|
252
|
-
(!selector.gateFingerprint ||
|
|
253
|
-
marker.gateFingerprint === selector.gateFingerprint) &&
|
|
254
|
-
(!selector.configFingerprint ||
|
|
255
|
-
marker.configFingerprint === selector.configFingerprint) &&
|
|
256
|
-
(!selector.contextFingerprint ||
|
|
257
|
-
marker.contextFingerprint === selector.contextFingerprint) &&
|
|
258
|
-
marker.timestamp >= startTime &&
|
|
259
|
-
marker.timestamp < endTime,
|
|
260
|
-
)
|
|
261
|
-
.sort(
|
|
262
|
-
(left, right) =>
|
|
263
|
-
left.timestamp - right.timestamp ||
|
|
264
|
-
left.type.localeCompare(right.type) ||
|
|
265
|
-
left.id.localeCompare(right.id),
|
|
266
|
-
);
|
|
267
|
-
let lastLossValue: number | null | undefined;
|
|
268
|
-
let hasLastLossValue = false;
|
|
269
|
-
const markers = matchingMarkers.filter((marker) => {
|
|
270
|
-
if (marker.type !== 'L') return true;
|
|
271
|
-
if (hasLastLossValue && marker.maxLossValue === lastLossValue) {
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
hasLastLossValue = true;
|
|
275
|
-
lastLossValue = marker.maxLossValue;
|
|
276
|
-
return true;
|
|
277
|
-
});
|
|
278
|
-
if (
|
|
279
|
-
!markers.length &&
|
|
280
|
-
(selector.compositionId ||
|
|
281
|
-
selector.gitSha ||
|
|
282
|
-
selector.gateFingerprint ||
|
|
283
|
-
selector.configFingerprint ||
|
|
284
|
-
selector.contextFingerprint)
|
|
285
|
-
) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
timelines.set(selectorKey, {
|
|
289
|
-
status: 'verified',
|
|
290
|
-
observedFrom: markers.length
|
|
291
|
-
? Math.min(...markers.map((marker) => marker.timestamp))
|
|
292
|
-
: Math.min(...envelopes.map((envelope) => envelope.payload.createdAt)),
|
|
293
|
-
markers,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return timelines;
|
|
298
|
-
};
|