@dzhechkov/harness-core 0.8.5 → 0.8.10
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/.dz-manifest.json +286 -82
- package/README.md +54 -2
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +11 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +38 -3
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +58 -23
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +194 -69
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +34 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +113 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +18 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/loop-blobs.generated.js +8 -8
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan.d.ts +13 -1
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +15 -1
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +51 -6
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-trace.d.ts +20 -1
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +83 -1
- package/dist/loop-trace.js.map +1 -1
- package/dist/model-recommender.d.ts +8 -0
- package/dist/model-recommender.d.ts.map +1 -1
- package/dist/model-recommender.js +31 -4
- package/dist/model-recommender.js.map +1 -1
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +178 -2
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +18 -12
- package/dist/workflow-run.js.map +1 -1
- package/package.json +19 -15
- package/sbom.json +591 -81
- package/src/amendment-trace.ts +12 -1
- package/src/compounding.ts +300 -1
- package/src/eta.ts +590 -0
- package/src/feature-adr-checkpoints.ts +39 -3
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +224 -65
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +148 -4
- package/src/index.ts +65 -3
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/loop-blobs.generated.ts +8 -8
- package/src/loop-plan.ts +36 -3
- package/src/loop-render.ts +50 -6
- package/src/loop-trace.ts +91 -2
- package/src/model-recommender.ts +35 -4
- package/src/operations.ts +199 -2
- package/src/patterns.ts +252 -43
- package/src/setup.ts +20 -17
- package/src/target-integrations.ts +225 -0
- package/src/vector-tier.ts +44 -14
- package/src/workflow-run.ts +18 -11
package/src/eta.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure ETA calibration for the live feature-adr statusline.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately performs no filesystem access and reads no ambient clock. Callers pass
|
|
5
|
+
* parsed JSONL text and `nowMs`; malformed or unstamped observations remain unknown, never zero.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CHECKPOINT_STAGES,
|
|
10
|
+
DESIGN_SUBSTAGES,
|
|
11
|
+
type CheckpointStage,
|
|
12
|
+
} from './feature-adr-checkpoints.js';
|
|
13
|
+
|
|
14
|
+
export const ETA_MAX_STAGE_MS = 6 * 3_600_000;
|
|
15
|
+
|
|
16
|
+
export interface CheckpointObservation {
|
|
17
|
+
readonly runId: string;
|
|
18
|
+
readonly stage: string;
|
|
19
|
+
readonly result: unknown;
|
|
20
|
+
readonly tsMs?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RunSegment {
|
|
24
|
+
readonly runId: string;
|
|
25
|
+
readonly observations: readonly CheckpointObservation[];
|
|
26
|
+
/** The most recent router in this invocation, retained across resume slices. */
|
|
27
|
+
readonly router?: CheckpointObservation;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StageDurationSample {
|
|
31
|
+
readonly runId: string;
|
|
32
|
+
readonly tier: string;
|
|
33
|
+
readonly stage: CheckpointStage;
|
|
34
|
+
readonly ms: number;
|
|
35
|
+
readonly fromTsMs: number;
|
|
36
|
+
readonly toTsMs: number;
|
|
37
|
+
readonly codexCoded?: boolean;
|
|
38
|
+
/** Macro stages whose wall time is already included in this sample. */
|
|
39
|
+
readonly coveredStages?: readonly CheckpointStage[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IncompleteCoverageSample {
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly tier: string;
|
|
45
|
+
readonly stage: 'code';
|
|
46
|
+
readonly codexCoded: true;
|
|
47
|
+
readonly incompleteCoverage: true;
|
|
48
|
+
readonly fromTsMs: number;
|
|
49
|
+
readonly toTsMs: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type StageSample = StageDurationSample | IncompleteCoverageSample;
|
|
53
|
+
|
|
54
|
+
export type EtaEstimate =
|
|
55
|
+
| {
|
|
56
|
+
readonly kind: 'eta';
|
|
57
|
+
readonly presentation: 'point' | 'range';
|
|
58
|
+
readonly p25Ms: number;
|
|
59
|
+
readonly medianMs: number;
|
|
60
|
+
readonly p75Ms: number;
|
|
61
|
+
readonly runsUsed: number;
|
|
62
|
+
readonly tier: string;
|
|
63
|
+
readonly paceFactor: number;
|
|
64
|
+
readonly stagesCovered: readonly CheckpointStage[];
|
|
65
|
+
readonly windowFrom: string;
|
|
66
|
+
readonly windowTo: string;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
readonly kind: 'insufficient-history';
|
|
70
|
+
readonly tier: string;
|
|
71
|
+
readonly runsFound: number;
|
|
72
|
+
readonly thinStage: CheckpointStage;
|
|
73
|
+
readonly windowFrom?: string;
|
|
74
|
+
readonly windowTo?: string;
|
|
75
|
+
}
|
|
76
|
+
| { readonly kind: 'no-tier' }
|
|
77
|
+
| { readonly kind: 'no-checkpoints' };
|
|
78
|
+
|
|
79
|
+
export interface EtaInput {
|
|
80
|
+
readonly samples: readonly StageSample[];
|
|
81
|
+
readonly currentTier: string | undefined;
|
|
82
|
+
readonly currentStage: CheckpointStage;
|
|
83
|
+
readonly remainingStages: readonly CheckpointStage[];
|
|
84
|
+
readonly currentRunSamples: readonly StageSample[];
|
|
85
|
+
readonly nowMs: number;
|
|
86
|
+
readonly lastCheckpointTsMs?: number;
|
|
87
|
+
readonly hasCurrentCheckpoints?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DESIGN_STAGE_NAMES = new Set<string>([
|
|
91
|
+
'design',
|
|
92
|
+
...DESIGN_SUBSTAGES.map((stage) => `design:${stage}`),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
function macroStage(stage: string): CheckpointStage | undefined {
|
|
96
|
+
if (DESIGN_STAGE_NAMES.has(stage)) return 'design';
|
|
97
|
+
return (CHECKPOINT_STAGES as readonly string[]).includes(stage) ? stage as CheckpointStage : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Parse JSONL independently per line. A malformed line can never poison its valid neighbours. */
|
|
101
|
+
export function parseCheckpointLines(text: string, runId: string): CheckpointObservation[] {
|
|
102
|
+
const observations: CheckpointObservation[] = [];
|
|
103
|
+
for (const raw of String(text ?? '').split('\n')) {
|
|
104
|
+
const line = raw.trim();
|
|
105
|
+
if (line === '') continue;
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
108
|
+
if (parsed === null || typeof parsed !== 'object' || typeof parsed['stage'] !== 'string') continue;
|
|
109
|
+
const ts = typeof parsed['ts'] === 'string' ? Date.parse(parsed['ts']) : Number.NaN;
|
|
110
|
+
observations.push({
|
|
111
|
+
runId,
|
|
112
|
+
stage: parsed['stage'],
|
|
113
|
+
result: parsed['result'],
|
|
114
|
+
...(Number.isFinite(ts) ? { tsMs: ts } : {}),
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
// One corrupt append is unknown evidence, not a zero-duration sample and not a fatal file.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return observations;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Split a slug's append-only history into monotonic timing slices.
|
|
125
|
+
*
|
|
126
|
+
* Distinct `design:*` siblings share one macro stage and may arrive in any order. A repeated sibling,
|
|
127
|
+
* repeated macro stage, or move backwards starts a resume slice. The latest router is carried as
|
|
128
|
+
* metadata into later slices, so their tier still comes from that invocation's router record.
|
|
129
|
+
*/
|
|
130
|
+
export function segmentRun(observations: readonly CheckpointObservation[]): RunSegment[] {
|
|
131
|
+
const segments: RunSegment[] = [];
|
|
132
|
+
let entries: CheckpointObservation[] = [];
|
|
133
|
+
let router: CheckpointObservation | undefined;
|
|
134
|
+
let lastIndex = -1;
|
|
135
|
+
let designKeys = new Set<string>();
|
|
136
|
+
let invocation = 0;
|
|
137
|
+
let segmentRunId: string | undefined;
|
|
138
|
+
|
|
139
|
+
const flush = (): void => {
|
|
140
|
+
if (entries.length === 0) return;
|
|
141
|
+
segments.push({
|
|
142
|
+
runId: segmentRunId ?? entries[0]!.runId,
|
|
143
|
+
observations: entries,
|
|
144
|
+
...(router !== undefined ? { router } : {}),
|
|
145
|
+
});
|
|
146
|
+
entries = [];
|
|
147
|
+
lastIndex = -1;
|
|
148
|
+
designKeys = new Set<string>();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const observation of observations) {
|
|
152
|
+
const macro = macroStage(observation.stage);
|
|
153
|
+
if (macro === undefined) continue;
|
|
154
|
+
const index = CHECKPOINT_STAGES.indexOf(macro);
|
|
155
|
+
|
|
156
|
+
if (macro === 'router') {
|
|
157
|
+
flush();
|
|
158
|
+
router = observation;
|
|
159
|
+
invocation += 1;
|
|
160
|
+
segmentRunId = invocation === 1 ? observation.runId : `${observation.runId}#${invocation}`;
|
|
161
|
+
entries.push(observation);
|
|
162
|
+
lastIndex = index;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const distinctDesignSibling = macro === 'design'
|
|
167
|
+
&& lastIndex === CHECKPOINT_STAGES.indexOf('design')
|
|
168
|
+
&& !designKeys.has(observation.stage);
|
|
169
|
+
if (entries.length > 0 && (index < lastIndex || (index === lastIndex && !distinctDesignSibling))) {
|
|
170
|
+
flush();
|
|
171
|
+
}
|
|
172
|
+
segmentRunId ??= observation.runId;
|
|
173
|
+
entries.push(observation);
|
|
174
|
+
lastIndex = index;
|
|
175
|
+
if (macro === 'design') designKeys.add(observation.stage);
|
|
176
|
+
}
|
|
177
|
+
flush();
|
|
178
|
+
return segments;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function tierOf(segment: RunSegment): string | undefined {
|
|
182
|
+
const result = segment.router?.result;
|
|
183
|
+
if (result === null || typeof result !== 'object') return undefined;
|
|
184
|
+
const tier = (result as Record<string, unknown>)['tier'];
|
|
185
|
+
return typeof tier === 'string' && tier.length > 0 ? tier : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function coderUsed(observation: CheckpointObservation | undefined): string | undefined {
|
|
189
|
+
const result = observation?.result;
|
|
190
|
+
if (result === null || typeof result !== 'object') return undefined;
|
|
191
|
+
const coder = (result as Record<string, unknown>)['coderUsed'];
|
|
192
|
+
return typeof coder === 'string' ? coder : undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface Completion {
|
|
196
|
+
readonly observation: CheckpointObservation;
|
|
197
|
+
readonly tsMs: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function completionsOf(segment: RunSegment): Map<CheckpointStage, Completion> {
|
|
201
|
+
const completions = new Map<CheckpointStage, Completion>();
|
|
202
|
+
for (const observation of segment.observations) {
|
|
203
|
+
const stage = macroStage(observation.stage);
|
|
204
|
+
if (stage === undefined || observation.tsMs === undefined) continue;
|
|
205
|
+
const current = completions.get(stage);
|
|
206
|
+
if (current === undefined || observation.tsMs > current.tsMs) {
|
|
207
|
+
completions.set(stage, { observation, tsMs: observation.tsMs });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return completions;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function usableDelta(from: Completion | undefined, to: Completion | undefined): number | undefined {
|
|
214
|
+
if (from === undefined || to === undefined) return undefined;
|
|
215
|
+
const delta = to.tsMs - from.tsMs;
|
|
216
|
+
return delta < 0 || delta > ETA_MAX_STAGE_MS ? undefined : delta;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Extract one tier-labelled duration per `(runId, tier, stage)`, summing resumed slices. */
|
|
220
|
+
export function extractStageSamples(segments: readonly RunSegment[]): StageSample[] {
|
|
221
|
+
const raw: StageSample[] = [];
|
|
222
|
+
for (const segment of segments) {
|
|
223
|
+
const tier = tierOf(segment);
|
|
224
|
+
if (tier === undefined) continue;
|
|
225
|
+
const completions = completionsOf(segment);
|
|
226
|
+
let suppressedStage: CheckpointStage | undefined;
|
|
227
|
+
|
|
228
|
+
for (let index = 1; index < CHECKPOINT_STAGES.length; index++) {
|
|
229
|
+
const stage = CHECKPOINT_STAGES[index]!;
|
|
230
|
+
if (stage === suppressedStage) continue;
|
|
231
|
+
const current = completions.get(stage);
|
|
232
|
+
if (current === undefined) continue;
|
|
233
|
+
|
|
234
|
+
let predecessor = completions.get(CHECKPOINT_STAGES[index - 1]!);
|
|
235
|
+
let endpoint = current;
|
|
236
|
+
const codexCoded = stage === 'code' && coderUsed(current.observation) === 'codex';
|
|
237
|
+
if (codexCoded) {
|
|
238
|
+
predecessor = completions.get('plan');
|
|
239
|
+
const nextStage = CHECKPOINT_STAGES[index + 1];
|
|
240
|
+
const candidate = nextStage === undefined ? undefined : completions.get(nextStage);
|
|
241
|
+
if (candidate !== undefined) {
|
|
242
|
+
endpoint = candidate;
|
|
243
|
+
suppressedStage = nextStage;
|
|
244
|
+
}
|
|
245
|
+
// A dispatch without a measurable landing is preserved as typed UNKNOWN evidence. It has
|
|
246
|
+
// no `ms`, so it cannot leak into a percentile, but it can invalidate an otherwise partial
|
|
247
|
+
// resumed run during coverage normalization.
|
|
248
|
+
if (predecessor === undefined || endpoint === current) {
|
|
249
|
+
raw.push({
|
|
250
|
+
runId: segment.runId,
|
|
251
|
+
tier,
|
|
252
|
+
stage: 'code',
|
|
253
|
+
codexCoded: true,
|
|
254
|
+
incompleteCoverage: true,
|
|
255
|
+
fromTsMs: predecessor?.tsMs ?? current.tsMs,
|
|
256
|
+
toTsMs: current.tsMs,
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const ms = usableDelta(predecessor, endpoint);
|
|
263
|
+
if (ms === undefined || predecessor === undefined) {
|
|
264
|
+
if (codexCoded) {
|
|
265
|
+
raw.push({
|
|
266
|
+
runId: segment.runId,
|
|
267
|
+
tier,
|
|
268
|
+
stage: 'code',
|
|
269
|
+
codexCoded: true,
|
|
270
|
+
incompleteCoverage: true,
|
|
271
|
+
fromTsMs: predecessor?.tsMs ?? current.tsMs,
|
|
272
|
+
toTsMs: endpoint.tsMs,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
raw.push({
|
|
278
|
+
runId: segment.runId,
|
|
279
|
+
tier,
|
|
280
|
+
stage,
|
|
281
|
+
ms,
|
|
282
|
+
fromTsMs: predecessor.tsMs,
|
|
283
|
+
toTsMs: endpoint.tsMs,
|
|
284
|
+
...(codexCoded ? { codexCoded: true, coveredStages: [stage, suppressedStage!] } : {}),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const aggregated = new Map<string, StageDurationSample>();
|
|
290
|
+
const coverageSlices: StageSample[] = [];
|
|
291
|
+
for (const current of raw) {
|
|
292
|
+
if ('incompleteCoverage' in current) {
|
|
293
|
+
coverageSlices.push(current);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (current.stage === 'code' || current.stage === 'qe') {
|
|
297
|
+
// Pairing code→qe is a slice-level proof. Aggregating these stages now would erase whether
|
|
298
|
+
// every resumed code slice actually acquired a landing witness.
|
|
299
|
+
coverageSlices.push(current);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
// Coverage is part of the unit. A resumed run may switch coder family: merging a raw `code`
|
|
303
|
+
// slice with a folded `code+qe` slice here would erase the standalone QE time before the
|
|
304
|
+
// estimator can normalize both shapes.
|
|
305
|
+
const coverage = (current.coveredStages ?? [current.stage]).join('+');
|
|
306
|
+
const key = `${current.runId}\u0000${current.tier}\u0000${current.stage}\u0000${coverage}`;
|
|
307
|
+
const previous = aggregated.get(key);
|
|
308
|
+
if (previous === undefined) {
|
|
309
|
+
aggregated.set(key, current);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
aggregated.set(key, {
|
|
313
|
+
runId: current.runId,
|
|
314
|
+
tier: current.tier,
|
|
315
|
+
stage: current.stage,
|
|
316
|
+
ms: previous.ms + current.ms,
|
|
317
|
+
fromTsMs: Math.min(previous.fromTsMs, current.fromTsMs),
|
|
318
|
+
toTsMs: Math.max(previous.toTsMs, current.toTsMs),
|
|
319
|
+
...(previous.codexCoded === true || current.codexCoded === true ? { codexCoded: true } : {}),
|
|
320
|
+
...(previous.coveredStages !== undefined || current.coveredStages !== undefined
|
|
321
|
+
? { coveredStages: [...new Set([...(previous.coveredStages ?? [previous.stage]), ...(current.coveredStages ?? [current.stage])])] }
|
|
322
|
+
: {}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return [...aggregated.values(), ...coverageSlices].sort((left, right) =>
|
|
326
|
+
left.runId.localeCompare(right.runId)
|
|
327
|
+
|| left.tier.localeCompare(right.tier)
|
|
328
|
+
|| CHECKPOINT_STAGES.indexOf(left.stage) - CHECKPOINT_STAGES.indexOf(right.stage)
|
|
329
|
+
|| left.fromTsMs - right.fromTsMs);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function quantile(sorted: readonly number[], q: number): number {
|
|
333
|
+
if (sorted.length === 0) return Number.NaN;
|
|
334
|
+
const position = (sorted.length - 1) * q;
|
|
335
|
+
const lower = Math.floor(position);
|
|
336
|
+
const upper = Math.ceil(position);
|
|
337
|
+
const low = sorted[lower]!;
|
|
338
|
+
const high = sorted[upper]!;
|
|
339
|
+
return low + (high - low) * (position - lower);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
343
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isoDay(tsMs: number): string {
|
|
347
|
+
return new Date(tsMs).toISOString().slice(0, 10);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function evidenceWindow(samples: readonly StageDurationSample[]): { windowFrom?: string; windowTo?: string } {
|
|
351
|
+
if (samples.length === 0) return {};
|
|
352
|
+
return {
|
|
353
|
+
windowFrom: isoDay(Math.min(...samples.map((sample) => sample.fromTsMs))),
|
|
354
|
+
windowTo: isoDay(Math.max(...samples.map((sample) => sample.toTsMs))),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface StageStats {
|
|
359
|
+
readonly stage: CheckpointStage;
|
|
360
|
+
readonly coveredStages: readonly CheckpointStage[];
|
|
361
|
+
readonly samples: readonly StageDurationSample[];
|
|
362
|
+
readonly runs: number;
|
|
363
|
+
readonly p25: number;
|
|
364
|
+
readonly median: number;
|
|
365
|
+
readonly p75: number;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function stageBucket(samples: readonly StageSample[], tier: string, stage: CheckpointStage): StageDurationSample[] {
|
|
369
|
+
const byRun = new Map<string, StageDurationSample>();
|
|
370
|
+
for (const entry of samples) {
|
|
371
|
+
if ('incompleteCoverage' in entry) continue;
|
|
372
|
+
if (entry.tier !== tier || entry.stage !== stage || !Number.isFinite(entry.ms) || entry.ms < 0) continue;
|
|
373
|
+
const previous = byRun.get(entry.runId);
|
|
374
|
+
byRun.set(entry.runId, previous === undefined ? entry : {
|
|
375
|
+
...entry,
|
|
376
|
+
ms: previous.ms + entry.ms,
|
|
377
|
+
fromTsMs: Math.min(previous.fromTsMs, entry.fromTsMs),
|
|
378
|
+
toTsMs: Math.max(previous.toTsMs, entry.toTsMs),
|
|
379
|
+
...(previous.codexCoded === true || entry.codexCoded === true ? { codexCoded: true } : {}),
|
|
380
|
+
...(previous.coveredStages !== undefined || entry.coveredStages !== undefined
|
|
381
|
+
? { coveredStages: [...new Set([...(previous.coveredStages ?? [previous.stage]), ...(entry.coveredStages ?? [entry.stage])])] }
|
|
382
|
+
: {}),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return [...byRun.values()];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function statsFor(samples: readonly StageSample[], tier: string, stage: CheckpointStage): StageStats {
|
|
389
|
+
const bucket = stageBucket(samples, tier, stage);
|
|
390
|
+
const durations = bucket.map((entry) => entry.ms).sort((a, b) => a - b);
|
|
391
|
+
return {
|
|
392
|
+
stage,
|
|
393
|
+
coveredStages: [stage],
|
|
394
|
+
samples: bucket,
|
|
395
|
+
runs: bucket.length,
|
|
396
|
+
p25: quantile(durations, 0.25),
|
|
397
|
+
median: quantile(durations, 0.5),
|
|
398
|
+
p75: quantile(durations, 0.75),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface CodeQeParts {
|
|
403
|
+
folded: StageDurationSample[];
|
|
404
|
+
rawCode: StageDurationSample[];
|
|
405
|
+
qe: StageDurationSample[];
|
|
406
|
+
incomplete: boolean;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Normalize Codex's folded code+qe sample and Claude's two ordinary samples to one comparable block. */
|
|
410
|
+
function codeQeBucket(samples: readonly StageSample[], tier: string): StageDurationSample[] {
|
|
411
|
+
const byRun = new Map<string, CodeQeParts>();
|
|
412
|
+
for (const sample of samples) {
|
|
413
|
+
if (sample.tier !== tier || (sample.stage !== 'code' && sample.stage !== 'qe')) continue;
|
|
414
|
+
const parts = byRun.get(sample.runId) ?? { folded: [], rawCode: [], qe: [], incomplete: false };
|
|
415
|
+
if ('incompleteCoverage' in sample) parts.incomplete = true;
|
|
416
|
+
else if (sample.stage === 'qe') parts.qe.push(sample);
|
|
417
|
+
else if (sample.coveredStages?.includes('qe') === true) parts.folded.push(sample);
|
|
418
|
+
else parts.rawCode.push(sample);
|
|
419
|
+
byRun.set(sample.runId, parts);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const bucket: StageDurationSample[] = [];
|
|
423
|
+
for (const [runId, parts] of byRun) {
|
|
424
|
+
if (parts.incomplete) continue;
|
|
425
|
+
// Every raw code slice needs its own immediately-linked QE slice. One QE somewhere in the run
|
|
426
|
+
// is not evidence that every resumed attempt landed.
|
|
427
|
+
const availableQe = [...parts.qe];
|
|
428
|
+
const pairedQe: StageDurationSample[] = [];
|
|
429
|
+
let incomplete = false;
|
|
430
|
+
for (const codeSample of parts.rawCode) {
|
|
431
|
+
const match = availableQe.findIndex((sample) => sample.fromTsMs === codeSample.toTsMs);
|
|
432
|
+
if (match < 0) {
|
|
433
|
+
incomplete = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
pairedQe.push(availableQe.splice(match, 1)[0]!);
|
|
437
|
+
}
|
|
438
|
+
if (incomplete) continue;
|
|
439
|
+
if (parts.folded.length === 0 && parts.rawCode.length === 0) continue;
|
|
440
|
+
const contributing = [...parts.folded, ...parts.rawCode, ...pairedQe];
|
|
441
|
+
bucket.push({
|
|
442
|
+
runId,
|
|
443
|
+
tier,
|
|
444
|
+
stage: 'code',
|
|
445
|
+
ms: contributing.reduce((sum, sample) => sum + sample.ms, 0),
|
|
446
|
+
fromTsMs: Math.min(...contributing.map((sample) => sample.fromTsMs)),
|
|
447
|
+
toTsMs: Math.max(...contributing.map((sample) => sample.toTsMs)),
|
|
448
|
+
coveredStages: ['code', 'qe'],
|
|
449
|
+
...(parts.folded.some((sample) => sample.codexCoded === true) ? { codexCoded: true } : {}),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return bucket;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function codeQeStats(samples: readonly StageSample[], tier: string): StageStats {
|
|
456
|
+
const bucket = codeQeBucket(samples, tier);
|
|
457
|
+
const durations = bucket.map((entry) => entry.ms).sort((a, b) => a - b);
|
|
458
|
+
return {
|
|
459
|
+
stage: 'code',
|
|
460
|
+
coveredStages: ['code', 'qe'],
|
|
461
|
+
samples: bucket,
|
|
462
|
+
runs: bucket.length,
|
|
463
|
+
p25: quantile(durations, 0.25),
|
|
464
|
+
median: quantile(durations, 0.5),
|
|
465
|
+
p75: quantile(durations, 0.75),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function normalizeCoverage(samples: readonly StageSample[], tier: string): StageDurationSample[] {
|
|
470
|
+
const normalized = codeQeBucket(samples, tier);
|
|
471
|
+
for (const stage of CHECKPOINT_STAGES) {
|
|
472
|
+
if (stage === 'router' || stage === 'code' || stage === 'qe') continue;
|
|
473
|
+
normalized.push(...stageBucket(samples, tier, stage));
|
|
474
|
+
}
|
|
475
|
+
return normalized;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Estimate the remaining macro stages without consulting the filesystem or an ambient clock. */
|
|
479
|
+
export function estimateEta(input: EtaInput): EtaEstimate {
|
|
480
|
+
if (input.hasCurrentCheckpoints === false) return { kind: 'no-checkpoints' };
|
|
481
|
+
if (input.currentTier === undefined || input.currentTier.length === 0) return { kind: 'no-tier' };
|
|
482
|
+
|
|
483
|
+
const remaining = [...new Set(input.remainingStages)].filter((stage) => stage !== 'router');
|
|
484
|
+
if (remaining.length === 0) return { kind: 'no-checkpoints' };
|
|
485
|
+
|
|
486
|
+
const statistics: StageStats[] = [];
|
|
487
|
+
for (let index = 0; index < remaining.length; index++) {
|
|
488
|
+
const stage = remaining[index]!;
|
|
489
|
+
const stats = stage === 'code' && remaining.includes('qe')
|
|
490
|
+
? codeQeStats(input.samples, input.currentTier)
|
|
491
|
+
: statsFor(input.samples, input.currentTier, stage);
|
|
492
|
+
if (stats.runs < 3) {
|
|
493
|
+
const window = evidenceWindow(stats.samples);
|
|
494
|
+
return {
|
|
495
|
+
kind: 'insufficient-history',
|
|
496
|
+
tier: input.currentTier,
|
|
497
|
+
runsFound: stats.runs,
|
|
498
|
+
thinStage: stage,
|
|
499
|
+
...window,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
statistics.push(stats);
|
|
503
|
+
if (stats.coveredStages.includes('qe')) index = Math.max(index, remaining.indexOf('qe'));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const currentIndex = CHECKPOINT_STAGES.indexOf(input.currentStage);
|
|
507
|
+
const normalizedHistory = normalizeCoverage(input.samples, input.currentTier);
|
|
508
|
+
const normalizedCurrent = normalizeCoverage(input.currentRunSamples, input.currentTier);
|
|
509
|
+
let currentTotal = 0;
|
|
510
|
+
let historicalTotal = 0;
|
|
511
|
+
for (const current of normalizedCurrent) {
|
|
512
|
+
if (current.tier !== input.currentTier || CHECKPOINT_STAGES.indexOf(current.stage) >= currentIndex) continue;
|
|
513
|
+
const historical = statsFor(normalizedHistory, input.currentTier, current.stage);
|
|
514
|
+
if (historical.runs < 3 || !Number.isFinite(historical.median) || historical.median <= 0) continue;
|
|
515
|
+
currentTotal += current.ms;
|
|
516
|
+
historicalTotal += historical.median;
|
|
517
|
+
}
|
|
518
|
+
const paceFactor = historicalTotal > 0
|
|
519
|
+
? clamp(currentTotal / historicalTotal, 0.5, 2)
|
|
520
|
+
: 1;
|
|
521
|
+
|
|
522
|
+
const elapsed = input.lastCheckpointTsMs !== undefined
|
|
523
|
+
&& Number.isFinite(input.lastCheckpointTsMs)
|
|
524
|
+
&& Number.isFinite(input.nowMs)
|
|
525
|
+
? Math.max(0, input.nowMs - input.lastCheckpointTsMs)
|
|
526
|
+
: 0;
|
|
527
|
+
const currentStats = statistics.find((stats) => stats.stage === input.currentStage);
|
|
528
|
+
const currentBound = currentStats === undefined
|
|
529
|
+
? 0
|
|
530
|
+
: (input.currentStage === 'code' ? currentStats.p75 : currentStats.median) * paceFactor;
|
|
531
|
+
const overrunMs = currentBound > 0 ? Math.max(0, elapsed - currentBound) : 0;
|
|
532
|
+
const remainingValue = (stage: CheckpointStage, value: number): number => {
|
|
533
|
+
const paced = value * paceFactor;
|
|
534
|
+
if (stage !== input.currentStage || elapsed === 0) return paced;
|
|
535
|
+
// Once elapsed time outruns the historical bound, restarting the stale prior would ignore the
|
|
536
|
+
// only live evidence we have. Add the measured overrun so the estimate extends monotonically.
|
|
537
|
+
if (overrunMs > 0) return paced + overrunMs;
|
|
538
|
+
return Math.max(0, paced - elapsed);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const contributing = statistics.flatMap((stats) => stats.samples);
|
|
542
|
+
const window = evidenceWindow(contributing);
|
|
543
|
+
const p25Ms = statistics.reduce((sum, stats) => sum + remainingValue(stats.stage, stats.p25), 0);
|
|
544
|
+
const medianMs = statistics.reduce((sum, stats) => sum + remainingValue(stats.stage, stats.median), 0);
|
|
545
|
+
const p75Ms = statistics.reduce((sum, stats) => sum + remainingValue(stats.stage, stats.p75), 0);
|
|
546
|
+
const presentation = p75Ms / p25Ms > 1.5 ? 'range' : 'point';
|
|
547
|
+
return {
|
|
548
|
+
kind: 'eta',
|
|
549
|
+
presentation,
|
|
550
|
+
p25Ms,
|
|
551
|
+
medianMs,
|
|
552
|
+
p75Ms,
|
|
553
|
+
runsUsed: Math.min(...statistics.map((stats) => stats.runs)),
|
|
554
|
+
tier: input.currentTier,
|
|
555
|
+
paceFactor,
|
|
556
|
+
stagesCovered: [...new Set(statistics.flatMap((stats) => stats.coveredStages))],
|
|
557
|
+
windowFrom: window.windowFrom!,
|
|
558
|
+
windowTo: window.windowTo!,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function displayMinutes(ms: number): number {
|
|
563
|
+
return Math.max(1, Math.ceil(ms / 60_000));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Render the compact Russian statusline fragment, always including its evidence basis. */
|
|
567
|
+
export function formatEta(estimate: EtaEstimate): string | undefined {
|
|
568
|
+
switch (estimate.kind) {
|
|
569
|
+
case 'eta': {
|
|
570
|
+
const basis = `n=${estimate.runsUsed} ранам ${estimate.tier}; окно ${estimate.windowFrom}–${estimate.windowTo}`;
|
|
571
|
+
if (estimate.presentation === 'range') {
|
|
572
|
+
return `⏱ ~${displayMinutes(estimate.p25Ms)}–${displayMinutes(estimate.p75Ms)}м (p25–p75 по ${basis})`;
|
|
573
|
+
}
|
|
574
|
+
return `⏱ ~${displayMinutes(estimate.medianMs)}м (медиана по ${basis})`;
|
|
575
|
+
}
|
|
576
|
+
case 'insufficient-history': {
|
|
577
|
+
const window = estimate.windowFrom !== undefined && estimate.windowTo !== undefined
|
|
578
|
+
? `${estimate.windowFrom}–${estimate.windowTo}`
|
|
579
|
+
: 'нет данных';
|
|
580
|
+
return `ETA: недостаточно истории (n=${estimate.runsFound} ${estimate.tier}; окно ${window})`;
|
|
581
|
+
}
|
|
582
|
+
case 'no-tier':
|
|
583
|
+
case 'no-checkpoints':
|
|
584
|
+
return undefined;
|
|
585
|
+
default: {
|
|
586
|
+
const exhaustive: never = estimate;
|
|
587
|
+
return exhaustive;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
/** Blob version stamp read by scripts/gen-loop-blobs.mjs (feature loop-designer, ADR-004) — the
|
|
28
28
|
* ONLY loop-designer change to this canonical file; bump when any blob-exported semantic changes. */
|
|
29
|
-
export const BLOB_VERSION = '1.
|
|
29
|
+
export const BLOB_VERSION = '1.2.0';
|
|
30
30
|
|
|
31
31
|
/** Stages the workflow checkpoints, in pipeline order. Cheap side-channel agents (usage probes,
|
|
32
32
|
* fa-record, auto-cost selects) are never checkpointed; the opt-in Delivery gate re-runs by design
|
|
@@ -421,7 +421,7 @@ export function captureFailureRecord(stage: unknown, mode: unknown, reason: unkn
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
/** Training-pair record format version. Bump on any field-shape change. */
|
|
424
|
-
export const TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-
|
|
424
|
+
export const TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-3';
|
|
425
425
|
|
|
426
426
|
/** Oversize guard cap over input+output combined (same posture as
|
|
427
427
|
* CHECKPOINT_MAX_RESULT_CHARS, sized for full stage prompts): an over-cap pair is
|
|
@@ -460,6 +460,14 @@ export interface TrainingPairProvenance {
|
|
|
460
460
|
minutes: number | null;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/** Resolved routing axes captured with every pair for later Fable-vs-grade analysis. */
|
|
464
|
+
export interface TrainingPairBudget {
|
|
465
|
+
primary: 'claude' | 'codex';
|
|
466
|
+
claude: 'normal' | 'eco';
|
|
467
|
+
codex: 'normal' | 'eco';
|
|
468
|
+
preset: 'normal' | 'eco' | 'hybrid' | 'custom' | 'unset';
|
|
469
|
+
}
|
|
470
|
+
|
|
463
471
|
export interface TrainingPairTruncation {
|
|
464
472
|
/** Original (pre-truncation) char counts + full-text hashes — what was cut is NAMED. */
|
|
465
473
|
inputChars: number;
|
|
@@ -479,6 +487,7 @@ export interface TrainingPair {
|
|
|
479
487
|
output: string;
|
|
480
488
|
evaluation: TrainingPairEvaluation;
|
|
481
489
|
provenance: TrainingPairProvenance;
|
|
490
|
+
budgetMode: TrainingPairBudget | null;
|
|
482
491
|
truncated: TrainingPairTruncation | null;
|
|
483
492
|
captureMode: 'capture' | 'backfill';
|
|
484
493
|
resumed: boolean;
|
|
@@ -615,6 +624,31 @@ function coerceText(v: unknown): string {
|
|
|
615
624
|
}
|
|
616
625
|
}
|
|
617
626
|
|
|
627
|
+
/** Normalize routing metadata without ever blocking capture. Undefined means the operator supplied
|
|
628
|
+
* neither axis; malformed/throwing inputs are explicit null, never a fabricated nearest preset.
|
|
629
|
+
* An explicit `preset:'unset'` preserves primary while recording that no budget arg was supplied. */
|
|
630
|
+
function normalizeTrainingPairBudget(raw: unknown): TrainingPairBudget | null {
|
|
631
|
+
try {
|
|
632
|
+
if (raw === undefined) return { primary: 'claude', claude: 'normal', codex: 'normal', preset: 'unset' };
|
|
633
|
+
if (raw === null || typeof raw !== 'object') return null;
|
|
634
|
+
const value = raw as Record<string, unknown>;
|
|
635
|
+
const primary = value.primary;
|
|
636
|
+
const claude = value.claude;
|
|
637
|
+
const codex = value.codex;
|
|
638
|
+
if (primary !== 'claude' && primary !== 'codex') return null;
|
|
639
|
+
if (claude !== 'normal' && claude !== 'eco') return null;
|
|
640
|
+
if (codex !== 'normal' && codex !== 'eco') return null;
|
|
641
|
+
if (value.preset === 'unset') return { primary, claude, codex, preset: 'unset' };
|
|
642
|
+
let preset: TrainingPairBudget['preset'] = 'custom';
|
|
643
|
+
if (claude === 'normal' && codex === 'normal') preset = 'normal';
|
|
644
|
+
else if (claude === 'eco' && codex === 'eco') preset = 'eco';
|
|
645
|
+
else if (claude === 'eco' && codex === 'normal') preset = 'hybrid';
|
|
646
|
+
return { primary, claude, codex, preset };
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
618
652
|
/** Assemble one SFT-ready training pair. Deterministic (ts passed in). Applies the oversize
|
|
619
653
|
* guard: when input+output exceed TRAINPAIR_MAX_IO_CHARS combined, each over-budget side is
|
|
620
654
|
* truncated with a marker naming the cut char count + the fnv1a64 of its FULL text (the
|
|
@@ -630,6 +664,7 @@ export function buildTrainingPair(opts: {
|
|
|
630
664
|
output: unknown;
|
|
631
665
|
evaluation?: Partial<TrainingPairEvaluation> | null;
|
|
632
666
|
provenance?: Partial<TrainingPairProvenance> | null;
|
|
667
|
+
budgetMode?: unknown;
|
|
633
668
|
captureMode?: unknown;
|
|
634
669
|
resumed?: unknown;
|
|
635
670
|
}): TrainingPair {
|
|
@@ -670,6 +705,7 @@ export function buildTrainingPair(opts: {
|
|
|
670
705
|
tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,
|
|
671
706
|
minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,
|
|
672
707
|
},
|
|
708
|
+
budgetMode: normalizeTrainingPairBudget(opts.budgetMode),
|
|
673
709
|
truncated,
|
|
674
710
|
captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',
|
|
675
711
|
resumed: opts.resumed === true,
|
|
@@ -781,7 +817,7 @@ export function codeStageResultShapeValid(r: unknown): boolean {
|
|
|
781
817
|
if (!v.code || typeof v.code !== 'object') return false
|
|
782
818
|
if (typeof v.coderUsed !== 'string') return false
|
|
783
819
|
if (typeof v.landedNote !== 'string') return false
|
|
784
|
-
if (v.landingProtocol !==
|
|
820
|
+
if (v.landingProtocol !== 3) return false
|
|
785
821
|
return v.landingStatus === 'landed' || v.landingStatus === 'genuinely-not-landed' || v.landingStatus === 'inconclusive' || v.landingStatus === 'synchronous'
|
|
786
822
|
}
|
|
787
823
|
|