@dzhechkov/harness-core 0.8.46 → 0.8.47
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 +175 -163
- package/README.md +84 -2
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +32 -2
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-snapshot.d.ts +14 -1
- package/dist/agentdb-snapshot.d.ts.map +1 -1
- package/dist/agentdb-snapshot.js +100 -4
- package/dist/agentdb-snapshot.js.map +1 -1
- package/dist/apply-leg.d.ts +9 -1
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +26 -4
- package/dist/apply-leg.js.map +1 -1
- package/dist/architecture.d.ts +0 -5
- package/dist/architecture.d.ts.map +1 -1
- package/dist/architecture.js +25 -2
- package/dist/architecture.js.map +1 -1
- package/dist/codex-rollouts.d.ts +35 -7
- package/dist/codex-rollouts.d.ts.map +1 -1
- package/dist/codex-rollouts.js +201 -113
- package/dist/codex-rollouts.js.map +1 -1
- package/dist/cost-ledger.d.ts +42 -3
- package/dist/cost-ledger.d.ts.map +1 -1
- package/dist/cost-ledger.js +478 -15
- package/dist/cost-ledger.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +16 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +21 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +7 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +48 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-plan.d.ts +4 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +4 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/mutation-gate.d.ts +36 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +109 -19
- package/dist/mutation-gate.js.map +1 -1
- package/dist/npm-homepage.d.ts +64 -0
- package/dist/npm-homepage.d.ts.map +1 -0
- package/dist/npm-homepage.js +109 -0
- package/dist/npm-homepage.js.map +1 -0
- package/dist/operations.d.ts +6 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +150 -24
- package/dist/operations.js.map +1 -1
- package/dist/pack-inventory.d.ts.map +1 -1
- package/dist/pack-inventory.js +6 -1
- package/dist/pack-inventory.js.map +1 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +4 -1
- package/dist/parity.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +43 -1
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +269 -13
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/publish.d.ts +5 -1
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +18 -1
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +63 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +82 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/release-package-audit.d.ts +86 -0
- package/dist/release-package-audit.d.ts.map +1 -0
- package/dist/release-package-audit.js +272 -0
- package/dist/release-package-audit.js.map +1 -0
- package/dist/release.d.ts +8 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +58 -32
- package/dist/release.js.map +1 -1
- package/dist/round.d.ts +10 -1
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +5 -1
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +11 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +124 -20
- package/dist/run-records.js.map +1 -1
- package/dist/stage-usage.d.ts +148 -0
- package/dist/stage-usage.d.ts.map +1 -0
- package/dist/stage-usage.js +261 -0
- package/dist/stage-usage.js.map +1 -0
- package/dist/statusline.d.ts +23 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +167 -1
- package/dist/statusline.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +21 -19
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +206 -95
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/dist/workflow-run.d.ts +12 -3
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +29 -2
- package/dist/workflow-run.js.map +1 -1
- package/package.json +3 -3
- package/sbom.json +264 -234
- package/src/agentdb-index.ts +31 -2
- package/src/agentdb-snapshot.ts +105 -4
- package/src/apply-leg.ts +26 -4
- package/src/architecture.ts +15 -2
- package/src/codex-rollouts.ts +155 -147
- package/src/cost-ledger.ts +308 -19
- package/src/feature-adr-routing.ts +22 -0
- package/src/guard.ts +52 -0
- package/src/index.ts +15 -3
- package/src/loop-plan.ts +8 -0
- package/src/mutation-gate.ts +153 -19
- package/src/npm-homepage.ts +142 -0
- package/src/operations.ts +177 -22
- package/src/pack-inventory.ts +6 -1
- package/src/parity.ts +4 -1
- package/src/publish-sibling-drift.ts +214 -11
- package/src/publish.ts +20 -1
- package/src/qe-bridge.ts +86 -1
- package/src/release-package-audit.ts +264 -0
- package/src/release.ts +55 -22
- package/src/round.ts +8 -0
- package/src/run-records.ts +101 -22
- package/src/stage-usage.ts +191 -0
- package/src/statusline.ts +149 -1
- package/src/workflow-run-dispatch.ts +148 -89
- package/src/workflow-run.ts +40 -4
- package/dist/ledger-cost-fill.d.ts +0 -58
- package/dist/ledger-cost-fill.d.ts.map +0 -1
- package/dist/ledger-cost-fill.js +0 -78
- package/dist/ledger-cost-fill.js.map +0 -1
- package/dist/retro.d.ts +0 -131
- package/dist/retro.d.ts.map +0 -1
- package/dist/retro.js +0 -207
- package/dist/retro.js.map +0 -1
- package/dist/sbom.d.ts +0 -42
- package/dist/sbom.d.ts.map +0 -1
- package/dist/sbom.js +0 -120
- package/dist/sbom.js.map +0 -1
package/src/cost-ledger.ts
CHANGED
|
@@ -48,12 +48,18 @@
|
|
|
48
48
|
* @packageDocumentation
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
52
|
-
import { dirname, join } from 'node:path';
|
|
51
|
+
import { closeSync, constants, fstatSync, openSync, readSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
52
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
53
53
|
|
|
54
|
-
import { hasKnownPricing, usageCost } from './cost-scoring.js';
|
|
54
|
+
import { MODEL_PRICES, hasKnownPricing, usageCost } from './cost-scoring.js';
|
|
55
55
|
import { CANONICAL_STAGES, canonicalStage } from './feature-adr-stage-canon.js';
|
|
56
56
|
import type { CanonicalStage } from './feature-adr-stage-canon.js';
|
|
57
|
+
import { resolveLedgerModelProvenance } from './run-records.js';
|
|
58
|
+
import { buildStageUsageReport } from './stage-usage.js';
|
|
59
|
+
import { parseCodexRollout } from './codex-rollouts.js';
|
|
60
|
+
import { fnv1a64 } from './feature-adr-checkpoints.js';
|
|
61
|
+
import { createHash } from 'node:crypto';
|
|
62
|
+
import { parseTrace } from './loop-trace.js';
|
|
57
63
|
import { claudeProjectsRoot, rawTokenMixOf, weightedTokensOf } from './usage.js';
|
|
58
64
|
|
|
59
65
|
// ── Scope + vocabulary ──────────────────────────────────────
|
|
@@ -199,7 +205,10 @@ export interface CostLedgerRow {
|
|
|
199
205
|
/** Cost-weighted input-equivalent tokens — the PRIMARY number of the row. */
|
|
200
206
|
readonly weightedTokens: number;
|
|
201
207
|
/** Secondary, derived estimate. `pricingKnown === false` ⇒ sonnet-class fallback pricing. */
|
|
202
|
-
readonly costUsd: number;
|
|
208
|
+
readonly costUsd: number | null;
|
|
209
|
+
readonly knownEstimatedCostUsd?: number;
|
|
210
|
+
readonly familyEstimatedCostUsd?: number | null;
|
|
211
|
+
readonly pricingProvenance?: readonly { model: string; tableKey: string | null; matchKind: string; source: string; fingerprint: string; current: false; billed: false }[];
|
|
203
212
|
readonly pricingKnown: boolean;
|
|
204
213
|
/** ISO, from the run record's stage boundaries (ADR-001) — `null` when the record lacked them. */
|
|
205
214
|
readonly startedTs: string | null;
|
|
@@ -249,6 +258,8 @@ export interface CostLedgerReconciliation {
|
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
export interface CostLedgerReport {
|
|
261
|
+
/** Actual readonly input inventory for derived-export admission; absent on pure builder results. */
|
|
262
|
+
readonly authoritativeEvidence?: { readonly roots: readonly string[]; readonly files: readonly string[] };
|
|
252
263
|
readonly runId: string;
|
|
253
264
|
readonly slug: string | null;
|
|
254
265
|
readonly workflowName: string | null;
|
|
@@ -264,7 +275,8 @@ export interface CostLedgerReport {
|
|
|
264
275
|
readonly reconciliation: CostLedgerReconciliation;
|
|
265
276
|
/** The record's cached raw sum — reported, never the invariant's right-hand side (ADR-002). */
|
|
266
277
|
readonly recordTotalTokens: number | null;
|
|
267
|
-
readonly totalCostUsd: number;
|
|
278
|
+
readonly totalCostUsd: number | null;
|
|
279
|
+
readonly knownEstimatedCostUsd?: number;
|
|
268
280
|
/** Model ids whose USD figures used sonnet-class fallback pricing (ADR-003). */
|
|
269
281
|
readonly pricingFallbackModels: readonly string[];
|
|
270
282
|
/** ALWAYS `true` — a local aggregation, not an official API (mirrors `dz usage`). */
|
|
@@ -280,7 +292,7 @@ export interface StageCostAggregate {
|
|
|
280
292
|
readonly avgTokens: number;
|
|
281
293
|
readonly runs: number;
|
|
282
294
|
readonly totalTokens: number;
|
|
283
|
-
readonly avgCostUsd: number;
|
|
295
|
+
readonly avgCostUsd: number | null;
|
|
284
296
|
}
|
|
285
297
|
|
|
286
298
|
// ── Small clamped helpers ───────────────────────────────────
|
|
@@ -528,6 +540,7 @@ export function buildCostLedger(input: BuildCostLedgerInput): CostLedgerReport {
|
|
|
528
540
|
startedAtMs: number | null;
|
|
529
541
|
endedAtMs: number | null;
|
|
530
542
|
costUsd: number;
|
|
543
|
+
familyCostUsd: number | null;
|
|
531
544
|
pricingKnown: boolean;
|
|
532
545
|
}
|
|
533
546
|
// measurement-integrity FR-4: a bucket is now keyed by (label, occurrence) rather than by label
|
|
@@ -574,6 +587,7 @@ export function buildCostLedger(input: BuildCostLedgerInput): CostLedgerReport {
|
|
|
574
587
|
startedAtMs: null,
|
|
575
588
|
endedAtMs: null,
|
|
576
589
|
costUsd: 0,
|
|
590
|
+
familyCostUsd: null,
|
|
577
591
|
pricingKnown: true,
|
|
578
592
|
};
|
|
579
593
|
buckets.set(bucketKey, b);
|
|
@@ -586,7 +600,9 @@ export function buildCostLedger(input: BuildCostLedgerInput): CostLedgerReport {
|
|
|
586
600
|
const end = stage.durationMs === null ? stage.startedAtMs : stage.startedAtMs + stage.durationMs;
|
|
587
601
|
b.endedAtMs = b.endedAtMs === null ? end : Math.max(b.endedAtMs, end);
|
|
588
602
|
}
|
|
589
|
-
|
|
603
|
+
const rateKey = Object.keys(MODEL_PRICES).filter((key) => stage.model.toLowerCase().replace(/^[a-z0-9-]+\//, '').startsWith(key)).sort((a,b) => b.length-a.length)[0];
|
|
604
|
+
const exactPrice = rateKey === stage.model.toLowerCase() && !rateKey?.includes('claude');
|
|
605
|
+
if (!exactPrice) b.pricingKnown = false;
|
|
590
606
|
|
|
591
607
|
// Price per AGENT, using that agent's own model, then aggregate — a `mixed` label must not be
|
|
592
608
|
// priced at one arbitrary model's rate.
|
|
@@ -614,8 +630,8 @@ export function buildCostLedger(input: BuildCostLedgerInput): CostLedgerReport {
|
|
|
614
630
|
completionTokens: mix.completionTokens + s.output,
|
|
615
631
|
};
|
|
616
632
|
}
|
|
617
|
-
const cost = usageCost(mix, stage.model);
|
|
618
|
-
|
|
633
|
+
const cost = hasKnownPricing(stage.model) ? usageCost(mix, stage.model) : null;
|
|
634
|
+
if (cost !== null && Number.isFinite(cost)) { if (exactPrice) b.costUsd += cost; else b.familyCostUsd = (b.familyCostUsd ?? 0) + cost; }
|
|
619
635
|
}
|
|
620
636
|
|
|
621
637
|
// accountedTokens — the DEDUPED union of stage-claimed samples, so a double-claim inflates
|
|
@@ -661,7 +677,15 @@ export function buildCostLedger(input: BuildCostLedgerInput): CostLedgerReport {
|
|
|
661
677
|
tokensCacheRead,
|
|
662
678
|
tokensOut,
|
|
663
679
|
weightedTokens: b.sum,
|
|
664
|
-
costUsd: b.costUsd,
|
|
680
|
+
costUsd: b.pricingKnown ? b.costUsd : null,
|
|
681
|
+
knownEstimatedCostUsd: b.costUsd,
|
|
682
|
+
familyEstimatedCostUsd: b.pricingKnown ? null : b.familyCostUsd,
|
|
683
|
+
pricingProvenance: models.map((model) => {
|
|
684
|
+
const normalized = model.toLowerCase().replace(/^[a-z0-9-]+\//, '');
|
|
685
|
+
const tableKey = Object.keys(MODEL_PRICES).filter((key) => normalized.startsWith(key)).sort((a,b) => b.length-a.length)[0] ?? null;
|
|
686
|
+
return { model, tableKey, matchKind: tableKey === null ? 'unknown' : tableKey === normalized && !tableKey.includes('claude') ? 'exact' : 'family-estimate',
|
|
687
|
+
source: 'MODEL_PRICES static snapshot', fingerprint: fnv1a64(JSON.stringify(MODEL_PRICES)), current: false as const, billed: false as const };
|
|
688
|
+
}),
|
|
665
689
|
pricingKnown: b.pricingKnown,
|
|
666
690
|
startedTs: isoOrNull(b.startedAtMs),
|
|
667
691
|
endedTs: isoOrNull(b.endedAtMs),
|
|
@@ -793,8 +817,8 @@ export function buildCostLedger(input: BuildCostLedgerInput): CostLedgerReport {
|
|
|
793
817
|
? 'INCOMPLETE_INVENTORY'
|
|
794
818
|
: 'BALANCED';
|
|
795
819
|
|
|
796
|
-
|
|
797
|
-
|
|
820
|
+
const knownEstimatedCostUsd = rows.reduce((n,r) => n + (r.knownEstimatedCostUsd ?? 0), 0);
|
|
821
|
+
const totalCostUsd = rows.every((r) => r.costUsd !== null) ? knownEstimatedCostUsd : null;
|
|
798
822
|
const fallbackModels = [...new Set(record.stages.filter((s) => !hasKnownPricing(s.model)).map((s) => s.model))].sort();
|
|
799
823
|
|
|
800
824
|
// ── byCanonicalStage (FR-2/T1 + FR-3/T2 combined: every canon bucket, plus `unknown` for rows
|
|
@@ -833,6 +857,7 @@ export function buildCostLedger(input: BuildCostLedgerInput): CostLedgerReport {
|
|
|
833
857
|
},
|
|
834
858
|
recordTotalTokens: record.recordTotalTokens,
|
|
835
859
|
totalCostUsd,
|
|
860
|
+
knownEstimatedCostUsd,
|
|
836
861
|
pricingFallbackModels: fallbackModels,
|
|
837
862
|
estimated: true,
|
|
838
863
|
scope: COST_LEDGER_SCOPE,
|
|
@@ -887,7 +912,7 @@ export function verifyCostLedgerReport(report: CostLedgerReport): readonly CostL
|
|
|
887
912
|
* run with a known attribution defect can never quietly become a routing input.
|
|
888
913
|
*/
|
|
889
914
|
export function stageCostAggregates(reports: readonly CostLedgerReport[]): StageCostAggregate[] {
|
|
890
|
-
const acc = new Map<string, { stage: string; model: string; total: number; cost: number; runs: Set<string> }>();
|
|
915
|
+
const acc = new Map<string, { stage: string; model: string; total: number; cost: number | null; runs: Set<string> }>();
|
|
891
916
|
for (const report of reports) {
|
|
892
917
|
// measurement-integrity T2: `!== 'BALANCED'` already excludes `INCOMPLETE_INVENTORY` — a run
|
|
893
918
|
// with a named orphan transcript is exactly as unfit for a routing input as one with a DoubleAttributed
|
|
@@ -905,7 +930,7 @@ export function stageCostAggregates(reports: readonly CostLedgerReport[]): Stage
|
|
|
905
930
|
acc.set(key, a);
|
|
906
931
|
}
|
|
907
932
|
a.total += row.weightedTokens;
|
|
908
|
-
a.cost
|
|
933
|
+
a.cost = a.cost === null || row.costUsd === null ? null : a.cost + row.costUsd;
|
|
909
934
|
a.runs.add(row.runId);
|
|
910
935
|
}
|
|
911
936
|
}
|
|
@@ -918,7 +943,7 @@ export function stageCostAggregates(reports: readonly CostLedgerReport[]): Stage
|
|
|
918
943
|
avgTokens: runs > 0 ? Math.round(a.total / runs) : 0,
|
|
919
944
|
runs,
|
|
920
945
|
totalTokens: a.total,
|
|
921
|
-
avgCostUsd: runs > 0 ? a.cost / runs : 0,
|
|
946
|
+
avgCostUsd: a.cost === null ? null : runs > 0 ? a.cost / runs : 0,
|
|
922
947
|
});
|
|
923
948
|
}
|
|
924
949
|
out.sort((x, y) => y.avgTokens - x.avgTokens || x.stage.localeCompare(y.stage));
|
|
@@ -932,7 +957,8 @@ function fmt(n: number): string {
|
|
|
932
957
|
return Math.round(n).toLocaleString('en-US');
|
|
933
958
|
}
|
|
934
959
|
|
|
935
|
-
function usd(n: number): string {
|
|
960
|
+
function usd(n: number | null): string {
|
|
961
|
+
if (n === null) return 'unavailable';
|
|
936
962
|
if (!Number.isFinite(n) || n <= 0) return '$0.00';
|
|
937
963
|
return '$' + n.toFixed(n < 1 ? 4 : 2);
|
|
938
964
|
}
|
|
@@ -1018,7 +1044,7 @@ export function renderCostLedger(report: CostLedgerReport): string {
|
|
|
1018
1044
|
);
|
|
1019
1045
|
}
|
|
1020
1046
|
if (report.pricingFallbackModels.length > 0) {
|
|
1021
|
-
lines.push(` note: ~USD
|
|
1047
|
+
lines.push(` note: primary ~USD unavailable; FALLBACK pricing is not used for: ${report.pricingFallbackModels.join(', ')}`);
|
|
1022
1048
|
}
|
|
1023
1049
|
lines.push(` scope: ${COST_LEDGER_SCOPE}`);
|
|
1024
1050
|
return lines.join('\n');
|
|
@@ -1174,6 +1200,7 @@ export function listCostLedgerRuns(opts: CostLedgerIoOptions = {}): CostLedgerRu
|
|
|
1174
1200
|
}
|
|
1175
1201
|
|
|
1176
1202
|
export interface DeriveCostLedgerOptions extends CostLedgerIoOptions {
|
|
1203
|
+
readonly projectRoot?: string;
|
|
1177
1204
|
/** Exact run id. Must match `[A-Za-z0-9_.-]{1,128}` — it becomes a path segment. */
|
|
1178
1205
|
readonly runId?: string;
|
|
1179
1206
|
/** Most recent run with this `args.slug`. Same pattern restriction. */
|
|
@@ -1188,7 +1215,7 @@ export function deriveCostLedger(opts: DeriveCostLedgerOptions = {}): CostLedger
|
|
|
1188
1215
|
try {
|
|
1189
1216
|
if (opts.runId !== undefined && !RUN_ID_PATTERN.test(opts.runId)) return null;
|
|
1190
1217
|
if (opts.slug !== undefined && !SLUG_PATTERN.test(opts.slug)) return null;
|
|
1191
|
-
const runs = listCostLedgerRuns(opts);
|
|
1218
|
+
const runs = listCostLedgerRuns({ ...opts, ...(opts.projectRoot !== undefined && opts.projectDir === undefined ? { projectDir: resolve(opts.projectRoot).replace(/[\\/]/g, '-') } : {}) }).filter((ref) => opts.projectRoot === undefined || relative(opts.projectsRoot ?? claudeProjectsRoot(), ref.recordPath).split(sep)[0] === resolve(opts.projectRoot).replace(/[\\/]/g, '-'));
|
|
1192
1219
|
const ref =
|
|
1193
1220
|
opts.runId !== undefined
|
|
1194
1221
|
? runs.find((r) => r.runId === opts.runId)
|
|
@@ -1255,7 +1282,7 @@ export function deriveCostLedger(opts: DeriveCostLedgerOptions = {}): CostLedger
|
|
|
1255
1282
|
}
|
|
1256
1283
|
}
|
|
1257
1284
|
|
|
1258
|
-
|
|
1285
|
+
const report = buildCostLedger({
|
|
1259
1286
|
record,
|
|
1260
1287
|
stageSamples: [...perAgent.entries()].map(([agentId, samples]) => ({ agentId, samples })),
|
|
1261
1288
|
runSamples,
|
|
@@ -1264,6 +1291,8 @@ export function deriveCostLedger(opts: DeriveCostLedgerOptions = {}): CostLedger
|
|
|
1264
1291
|
...(listingTruncated ? { transcriptListingTruncated: true } : {}),
|
|
1265
1292
|
...(opts.epsilon !== undefined ? { epsilon: opts.epsilon } : {}),
|
|
1266
1293
|
});
|
|
1294
|
+
return { ...report, authoritativeEvidence: { roots: [opts.projectsRoot ?? claudeProjectsRoot(), dirname(dirname(dirname(ref.recordPath))), ref.transcriptDir],
|
|
1295
|
+
files: [ref.recordPath, ...files.map((file) => join(ref.transcriptDir, file)), ...record.stages.map((stage) => join(ref.transcriptDir, 'agent-' + stage.agentId + '.jsonl'))] } };
|
|
1267
1296
|
} catch {
|
|
1268
1297
|
return null; // never-throw contract
|
|
1269
1298
|
}
|
|
@@ -1311,3 +1340,263 @@ export function writeCostLedgerJsonl(path: string, report: CostLedgerReport): bo
|
|
|
1311
1340
|
return false;
|
|
1312
1341
|
}
|
|
1313
1342
|
}
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
// Capture contract: ordered named fields, with absent distinct from explicit null.
|
|
1346
|
+
// Kept private at producer/reader boundaries; both use this exact sha256 representation.
|
|
1347
|
+
function capturedPayload(evidence: Record<string, unknown>): string {
|
|
1348
|
+
const value = (v: unknown) => v === undefined ? { absent: true } : v;
|
|
1349
|
+
const receipts = Array.isArray(evidence['receipts']) ? evidence['receipts'] : [];
|
|
1350
|
+
return JSON.stringify([
|
|
1351
|
+
...['schema', 'sessionId', 'turnId', 'sourcePath', 'matchBasis', 'capturedFrom', 'capturedTo', 'model', 'cwd', 'reportedTotalBasis', 'inputCacheSemantics'].map((k) => [k, value(evidence[k])]),
|
|
1352
|
+
['owner', ...['runId', 'taskId', 'stage', 'attempt', 'role'].map((k) => [k, value((evidence['owner'] as Record<string, unknown> | undefined)?.[k])])],
|
|
1353
|
+
receipts.map((raw) => {
|
|
1354
|
+
const r = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {}; const t = r['totals'] as Record<string, unknown> | undefined;
|
|
1355
|
+
return [...['key', 'responseId', 'turnId', 'turnIndex', 'timestamp', 'payloadDigest', 'source'].map((k) => [k, value(r[k])]),
|
|
1356
|
+
['totals', ...['input', 'output', 'cachedInput', 'cachedWrite', 'reasoning', 'total'].map((k) => [k, value(t?.[k])])]];
|
|
1357
|
+
}),
|
|
1358
|
+
]);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Existing report IO boundary; no new persistent ledger. Diagnostics never contain raw source text.
|
|
1362
|
+
type StageRecord = Record<string, unknown>;
|
|
1363
|
+
const stageObject = (value: unknown): value is StageRecord => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
1364
|
+
function stageReadText(path: string, allowedRoot: string, maxBytes = 64 * 1024 * 1024): { text: string | null; diagnostic: string | null } {
|
|
1365
|
+
let fd: number | undefined;
|
|
1366
|
+
try {
|
|
1367
|
+
const root = realpathSync(allowedRoot); const abs = resolve(path); const actual = realpathSync(abs); const stat = lstatSync(abs);
|
|
1368
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (actual !== root && !actual.startsWith(root + sep))) return { text: null, diagnostic: 'source-outside-root-or-nonregular' };
|
|
1369
|
+
let ancestor = dirname(abs);
|
|
1370
|
+
while (ancestor !== root && ancestor !== dirname(ancestor)) { if (lstatSync(ancestor).isSymbolicLink()) return { text: null, diagnostic: 'source-symlink' }; ancestor = dirname(ancestor); }
|
|
1371
|
+
fd = openSync(abs, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1372
|
+
const opened = fstatSync(fd); if (!opened.isFile() || opened.ino !== stat.ino || opened.dev !== stat.dev) return { text: null, diagnostic: 'source-changed-during-read' };
|
|
1373
|
+
const chunks: Buffer[] = []; let bytes = 0;
|
|
1374
|
+
while (true) { const chunk = Buffer.alloc(Math.min(65536, maxBytes + 1 - bytes)); const n = readSync(fd, chunk, 0, chunk.length, null);
|
|
1375
|
+
if (!n) break; bytes += n; if (bytes > maxBytes) return { text: null, diagnostic: 'source-input-too-large' }; chunks.push(chunk.subarray(0, n)); }
|
|
1376
|
+
return { text: new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)), diagnostic: null };
|
|
1377
|
+
} catch { return { text: null, diagnostic: 'missing-or-unreadable-source' }; }
|
|
1378
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
1379
|
+
}
|
|
1380
|
+
function stageReadRows(path: string, root: string, maxBytes?: number, maxRecords = 100000) {
|
|
1381
|
+
const read = stageReadText(path, root, maxBytes); const diagnostics: string[] = [];
|
|
1382
|
+
const rows: StageRecord[] = []; if (read.diagnostic) diagnostics.push(read.diagnostic);
|
|
1383
|
+
let scanned = 0;
|
|
1384
|
+
for (const line of (read.text ?? '').split('\n')) {
|
|
1385
|
+
if (!line.trim()) continue; if (++scanned > maxRecords) { diagnostics.push('inventory-truncated'); break; }
|
|
1386
|
+
try { const value: unknown = JSON.parse(line); if (stageObject(value)) rows.push(value); else diagnostics.push('malformed-record'); }
|
|
1387
|
+
catch { diagnostics.push('malformed-record'); }
|
|
1388
|
+
}
|
|
1389
|
+
return { rows, diagnostics, fingerprint: read.text === null ? null : createHash('sha256').update(read.text).digest('hex') };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/** Scoped source selection. Explicit project/run never falls through to an unrelated global run. */
|
|
1393
|
+
export function deriveStageUsageReport(opts: {
|
|
1394
|
+
projectRoot: string; source?: string; runId?: string; slug?: string; runDir?: string;
|
|
1395
|
+
codexSessionsRoot?: string; projectsRoot?: string; maxBytes?: number; maxRecords?: number; epsilon?: number;
|
|
1396
|
+
}): ReturnType<typeof buildStageUsageReport> & { legacyCostLedger?: CostLedgerReport; authoritativeEvidence: { roots: string[]; files: string[] } } {
|
|
1397
|
+
const project = resolve(opts.projectRoot); const source = opts.source ?? 'auto';
|
|
1398
|
+
const sourceRoot = resolve(opts.codexSessionsRoot ?? join(process.env['HOME'] ?? '', '.codex/sessions'));
|
|
1399
|
+
const roots = [join(project, '.dz'), sourceRoot, resolve(opts.projectsRoot ?? claudeProjectsRoot()), join(resolve(opts.projectsRoot ?? claudeProjectsRoot()), project.replace(/[\\/]/g, '-'))]; const files: string[] = [];
|
|
1400
|
+
if (opts.runDir !== undefined) roots.push(resolve(project, opts.runDir));
|
|
1401
|
+
if (source === 'claude-transcript') roots.push(resolve(opts.projectsRoot ?? claudeProjectsRoot()));
|
|
1402
|
+
const build = (input: Parameters<typeof buildStageUsageReport>[0]) => ({ ...buildStageUsageReport(input), authoritativeEvidence: { roots, files } });
|
|
1403
|
+
const insufficient = (reason: string) => build({ sourceKind: source, sourcePath: project, runId: opts.runId ?? null, rows: [], diagnostics: [reason] });
|
|
1404
|
+
if (!['auto', 'workflow-budget', 'fa-ledger', 'claude-transcript'].includes(source)) return insufficient('unknown-source');
|
|
1405
|
+
const faPath = join(project, '.dz/feature-adr/run-cost-ledger.jsonl');
|
|
1406
|
+
files.push(faPath);
|
|
1407
|
+
const fa = stageReadRows(faPath, project, opts.maxBytes, opts.maxRecords);
|
|
1408
|
+
const faMatches = fa.rows.filter((row) => (opts.runId === undefined || row['runId'] === opts.runId) && (opts.slug === undefined || row['slug'] === opts.slug));
|
|
1409
|
+
const faIds = [...new Set(faMatches.map((row) => typeof row['runId'] === 'string' ? row['runId'] : '').filter(Boolean))];
|
|
1410
|
+
const projections = faMatches.filter((row) => row['summary'] === true && row['sourceProjection'] === 'workflow-budget');
|
|
1411
|
+
const projectionDiagnostics: string[] = []; const projectedIds = new Set<string>();
|
|
1412
|
+
const safeRunId = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,128}$/.test(value) && value !== '.' && value !== '..';
|
|
1413
|
+
for (const row of projections) {
|
|
1414
|
+
if (!safeRunId(row['workflowRunId']) || row['runId'] !== row['workflowRunId'] || (row['projectRoot'] !== undefined && (typeof row['projectRoot'] !== 'string' || resolve(row['projectRoot']) !== project))) {
|
|
1415
|
+
projectionDiagnostics.push('inventory-workflow-projection-identity-invalid'); continue;
|
|
1416
|
+
}
|
|
1417
|
+
projectedIds.add(row['workflowRunId']);
|
|
1418
|
+
}
|
|
1419
|
+
const projectedRunId = projectedIds.size === 1 ? [...projectedIds][0]! : null;
|
|
1420
|
+
const traceRoot = join(project, '.dz/loop-trace'); let wfDirs: string[] = [];
|
|
1421
|
+
if (opts.runDir !== undefined) wfDirs = [resolve(project, opts.runDir)];
|
|
1422
|
+
else if (opts.runId !== undefined && safeRunId(opts.runId)) wfDirs = [join(traceRoot, opts.runId)].filter((dir) => existsSync(join(dir, 'budget.jsonl')));
|
|
1423
|
+
else if (opts.slug !== undefined) wfDirs = [...projectedIds].map((id) => join(traceRoot, id));
|
|
1424
|
+
else if (opts.runId === undefined) {
|
|
1425
|
+
try { wfDirs = readdirSync(traceRoot).filter(safeRunId).map((name) => join(traceRoot, name)).filter((dir) => existsSync(join(dir, 'budget.jsonl'))); } catch { /* no selected Wf inventory */ }
|
|
1426
|
+
}
|
|
1427
|
+
roots.push(...wfDirs);
|
|
1428
|
+
for (const dir of wfDirs) files.push(...['budget.jsonl', 'trace.jsonl', 'run-state.json'].map((name) => join(dir, name)));
|
|
1429
|
+
|
|
1430
|
+
// Scoped native census, preserving latest-within-source semantics while refusing unresolved ties.
|
|
1431
|
+
const claudeRoot = resolve(opts.projectsRoot ?? claudeProjectsRoot()); const projectDir = project.replace(/[\\/]/g, '-');
|
|
1432
|
+
const claudeRefs = (opts.runId !== undefined && !RUN_ID_PATTERN.test(opts.runId)) || (opts.slug !== undefined && !SLUG_PATTERN.test(opts.slug)) ? []
|
|
1433
|
+
: listCostLedgerRuns({ projectsRoot: claudeRoot, projectDir }).filter((ref) => (opts.runId === undefined || ref.runId === opts.runId) && (opts.slug === undefined || ref.slug === opts.slug));
|
|
1434
|
+
roots.push(claudeRoot, join(claudeRoot, projectDir));
|
|
1435
|
+
for (const ref of claudeRefs) { roots.push(ref.transcriptDir); files.push(ref.recordPath, ...safeListDir(ref.transcriptDir).filter((f) => f.endsWith('.jsonl')).slice(0, MAX_RUN_TRANSCRIPT_FILES).map((f) => join(ref.transcriptDir, f))); }
|
|
1436
|
+
const claudeAmbiguous = claudeRefs.length > 1 && claudeRefs[0]!.startedAtMs === claudeRefs[1]!.startedAtMs;
|
|
1437
|
+
const faIndependent = faMatches.some((row) => !(!stageObject(row['usageEvidence']) && (row['summary'] === true || ['full', 'round', 'control', 'publish'].includes(String(row['stage'])))));
|
|
1438
|
+
const authorities = Number(wfDirs.length > 0) + Number(faIndependent) + Number(claudeRefs.length > 0);
|
|
1439
|
+
if (source === 'auto' && (authorities > 1 || wfDirs.length > 1 || (faIndependent && faIds.length > 1) || claudeAmbiguous)) return insufficient('source-selection-ambiguous');
|
|
1440
|
+
if (source === 'auto' && projectionDiagnostics.length) return insufficient(projectionDiagnostics[0]!);
|
|
1441
|
+
const chosen = source === 'auto' ? wfDirs.length === 1 ? 'workflow-budget' : faIndependent ? 'fa-ledger' : claudeRefs.length ? 'claude-transcript' : projections.length ? 'fa-ledger' : 'none' : source;
|
|
1442
|
+
if (chosen === 'workflow-budget') {
|
|
1443
|
+
if (opts.slug !== undefined && opts.runId === undefined && projectedRunId === null) return insufficient(projectionDiagnostics[0] ?? 'workflow-source-missing-or-ambiguous');
|
|
1444
|
+
if (wfDirs.length !== 1) return insufficient('workflow-source-missing-or-ambiguous');
|
|
1445
|
+
const dir = wfDirs[0]!; const allowed = opts.runDir === undefined ? project : dir;
|
|
1446
|
+
const budget = stageReadRows(join(dir, 'budget.jsonl'), allowed, opts.maxBytes, opts.maxRecords);
|
|
1447
|
+
const trace = stageReadRows(join(dir, 'trace.jsonl'), allowed, opts.maxBytes, opts.maxRecords);
|
|
1448
|
+
const stateRead = stageReadText(join(dir, 'run-state.json'), allowed, opts.maxBytes);
|
|
1449
|
+
let state: StageRecord | null = null; try { const value: unknown = JSON.parse(stateRead.text ?? 'null'); if (stageObject(value)) state = value; } catch { /* named below */ }
|
|
1450
|
+
const runId = opts.runId ?? projectedRunId ?? (typeof state?.['runId'] === 'string' ? state['runId'] : null);
|
|
1451
|
+
const diagnostics = [...budget.diagnostics, ...trace.diagnostics, ...projectionDiagnostics];
|
|
1452
|
+
if (!state || state['runId'] !== runId) diagnostics.push('inventory-run-state-unavailable-or-foreign');
|
|
1453
|
+
if (state && state['traceSha256'] == null) diagnostics.push('inventory-trace-binding-unavailable');
|
|
1454
|
+
else if (state && state['traceSha256'] !== trace.fingerprint) diagnostics.push('inventory-trace-binding-mismatch');
|
|
1455
|
+
const validatedTrace = parseTrace(trace.rows.map((row) => JSON.stringify(row)).join('\n'));
|
|
1456
|
+
for (const row of projections) if (row['workflowRunId'] !== runId || row['runId'] !== runId || (row['planDigest'] !== undefined && (typeof row['planDigest'] !== 'string' || row['planDigest'] !== state?.['planDigest'] || row['planDigest'] !== validatedTrace.planDigest))) diagnostics.push('inventory-workflow-projection-identity-mismatch');
|
|
1457
|
+
if (validatedTrace.parseErrors.length || validatedTrace.openConflict) diagnostics.push('inventory-trace-invalid');
|
|
1458
|
+
const rows = budget.rows.filter((row) => {
|
|
1459
|
+
if (row['schema'] !== 'wf-budget-1' || (row['kind'] !== 'stage' && row['kind'] !== 'probe')) { diagnostics.push('malformed-budget-schema'); return false; }
|
|
1460
|
+
if (row['projectRoot'] !== undefined && resolve(String(row['projectRoot'])) !== project) { diagnostics.push('foreign-project-root'); return false; }
|
|
1461
|
+
return true;
|
|
1462
|
+
});
|
|
1463
|
+
const expected = validatedTrace.events.filter((row) => row.event === 'dispatched' && row.runId === runId).map((row) => ({ ...row, dispatchSeq: row.seq, evidenceKey: JSON.stringify(['wf-dispatch', row.runId, row.seq]) }));
|
|
1464
|
+
if (trace.rows.some((row) => row['runId'] !== runId)) diagnostics.push('foreign-trace-run');
|
|
1465
|
+
return build({ sourceKind: 'workflow-budget', sourcePath: dir, runId, rows,
|
|
1466
|
+
...(trace.diagnostics.length === 0 ? { expected } : {}), diagnostics });
|
|
1467
|
+
}
|
|
1468
|
+
if (chosen === 'fa-ledger') {
|
|
1469
|
+
if (opts.runDir !== undefined) return insufficient('source-selection-conflicting-run-dir');
|
|
1470
|
+
if (opts.runId === undefined && faIds.length !== 1) return insufficient('fa-run-selection-ambiguous-or-unidentified');
|
|
1471
|
+
const runId = opts.runId ?? faIds[0]!;
|
|
1472
|
+
const selected = faMatches.filter((row) => row['runId'] === runId);
|
|
1473
|
+
const diagnostics = [...fa.diagnostics]; const sourceDiagnostics: string[] = [];
|
|
1474
|
+
const rows: StageRecord[] = []; const expected: StageRecord[] = []; const witnesses: StageRecord[] = []; const moneyObservations: StageRecord[] = [];
|
|
1475
|
+
let independent = true;
|
|
1476
|
+
for (const row of selected) {
|
|
1477
|
+
const identity = resolveLedgerModelProvenance(row);
|
|
1478
|
+
const evidence = stageObject(row['usageEvidence']) ? row['usageEvidence'] : null;
|
|
1479
|
+
if (!evidence && (row['summary'] === true || ['full', 'round', 'control', 'publish'].includes(String(row['stage'])))) continue;
|
|
1480
|
+
const suppliedMoney = stageObject(row['reportedCostObservation']) ? row['reportedCostObservation'] : null;
|
|
1481
|
+
const moneyScope = evidence && Array.isArray(evidence['receipts']) ? JSON.stringify([evidence['sessionId'], evidence['turnId'], evidence['receipts'].filter(stageObject).map((r) => r['key']).sort()]) : null;
|
|
1482
|
+
moneyObservations.push({ id: suppliedMoney?.['id'] ?? (moneyScope === null ? null : 'captured-scope:' + fnv1a64(moneyScope)),
|
|
1483
|
+
scope: suppliedMoney?.['scope'] ?? moneyScope, basis: suppliedMoney?.['basis'] ?? 'caller-reported-captured-scope', runId: row['runId'], amount: row['reportedCostUsd'] ?? null });
|
|
1484
|
+
if (!evidence || !Array.isArray(evidence['receipts']) || evidence['schema'] !== 'codex-rollout-scope-1') { rows.push(row); independent = false; diagnostics.push('source-scope-unavailable'); continue; }
|
|
1485
|
+
for (const field of ['capturedFrom', 'capturedTo']) if (evidence[field] != null && (typeof evidence[field] !== 'string' || !Number.isFinite(Date.parse(evidence[field] as string)))) sourceDiagnostics.push('invalid-captured-window:' + field);
|
|
1486
|
+
if (!['codex', 'openai'].includes(identity.family ?? '')) sourceDiagnostics.push('captured-source-model-family-conflict');
|
|
1487
|
+
if (identity.model === null || identity.diagnostics.length) sourceDiagnostics.push(...identity.diagnostics.map((d) => 'captured-' + d));
|
|
1488
|
+
const captured = evidence['receipts'].filter(stageObject);
|
|
1489
|
+
if (captured.some((r) => !stageObject(r['totals']))) sourceDiagnostics.push('invalid-captured-totals');
|
|
1490
|
+
if (evidence['captureSha256'] !== undefined) {
|
|
1491
|
+
if (createHash('sha256').update(capturedPayload(evidence)).digest('hex') !== evidence['captureSha256']) sourceDiagnostics.push('captured-payload-sha256-mismatch');
|
|
1492
|
+
const owner = stageObject(evidence['owner']) ? evidence['owner'] : {};
|
|
1493
|
+
for (const key of ['runId', 'taskId', 'stage', 'attempt', 'role']) if (owner[key] !== (row[key] ?? null)) sourceDiagnostics.push('captured-owner-mismatch:' + key);
|
|
1494
|
+
if (evidence['model'] !== identity.model || evidence['cwd'] !== project || evidence['reportedTotalBasis'] !== row['reportedTotalBasis'] || evidence['inputCacheSemantics'] !== row['inputCacheSemantics']) sourceDiagnostics.push('captured-scope-mismatch');
|
|
1495
|
+
}
|
|
1496
|
+
if (fnv1a64(JSON.stringify(captured.map((receipt) => [receipt['key'], receipt['payloadDigest']]))) !== evidence['payloadDigest']) sourceDiagnostics.push('captured-scope-fingerprint-mismatch');
|
|
1497
|
+
if (captured.length !== evidence['receipts'].length) { sourceDiagnostics.push('invalid-captured-scope'); independent = false; }
|
|
1498
|
+
const sourcePath = typeof evidence['sourcePath'] === 'string' ? evidence['sourcePath'] : '';
|
|
1499
|
+
if (sourcePath) files.push(resolve(sourcePath));
|
|
1500
|
+
const original = stageReadText(sourcePath, sourceRoot, opts.maxBytes);
|
|
1501
|
+
const parsed = original.text === null ? null : parseCodexRollout(original.text, sourcePath);
|
|
1502
|
+
if (evidence['matchBasis'] !== 'exact') { independent = false; diagnostics.push('legacy-window-assurance'); }
|
|
1503
|
+
let scopedTotal = 0; let allKnown = true; const scopeAssociations = new Set<string>(); const matchedSourceKeys = new Set<string>(); let maxSourceRecord = -1;
|
|
1504
|
+
const scopedDimensions: Record<string, number | null> = { tokensIn: 0, tokensOut: 0, tokensCacheRead: 0, tokensCacheWrite: 0, tokensReasoning: 0 };
|
|
1505
|
+
for (const receipt of captured) {
|
|
1506
|
+
const key = typeof receipt['key'] === 'string' ? receipt['key'] : null;
|
|
1507
|
+
const totals = stageObject(receipt['totals']) ? receipt['totals'] : {};
|
|
1508
|
+
const claim = { ...row, reportedCostUsd: null, evidenceKey: key, tokensTotal: totals['total'], tokensIn: totals['input'], tokensOut: totals['output'],
|
|
1509
|
+
tokensCacheRead: totals['cachedInput'], tokensCacheWrite: totals['cachedWrite'], tokensReasoning: totals['reasoning'],
|
|
1510
|
+
reportedTotalBasis: 'raw-inclusive', inputCacheSemantics: 'includes-cache-read-write' };
|
|
1511
|
+
rows.push(claim); expected.push({ evidenceKey: key });
|
|
1512
|
+
if (!parsed || 'error' in parsed || parsed.id !== evidence['sessionId']) { independent = false; diagnostics.push('source-witness-unavailable'); continue; }
|
|
1513
|
+
const found = parsed.receipts?.find((r) => JSON.stringify([parsed.id, r.key]) === key);
|
|
1514
|
+
if (!found) { sourceDiagnostics.push('captured-source-receipt-missing-or-foreign'); independent = false; continue; }
|
|
1515
|
+
matchedSourceKeys.add(found.key); maxSourceRecord = Math.max(maxSourceRecord, found.sourceRecord ?? -1);
|
|
1516
|
+
for (const diagnostic of found.diagnostics ?? []) sourceDiagnostics.push('source-receipt:' + diagnostic);
|
|
1517
|
+
for (const field of ['input', 'output', 'cachedInput', 'cachedWrite', 'reasoning', 'total'] as const) if (totals[field] !== found.totals[field]) sourceDiagnostics.push('captured-source-dimension-mismatch:' + field);
|
|
1518
|
+
for (const field of ['responseId', 'turnId', 'timestamp', 'source'] as const) if (receipt[field] !== found[field]) sourceDiagnostics.push('captured-source-identity-mismatch:' + field);
|
|
1519
|
+
// Association refers to the original independently parsed array, never a filtered view.
|
|
1520
|
+
let turn: typeof parsed.turns[number] | undefined; let sourceTurnIndex: number | null = null;
|
|
1521
|
+
let sessionScope = false;
|
|
1522
|
+
if (found.turnIndex != null) {
|
|
1523
|
+
if (Number.isSafeInteger(found.turnIndex) && found.turnIndex >= 0 && found.turnIndex < parsed.turns.length) {
|
|
1524
|
+
sourceTurnIndex = found.turnIndex; turn = parsed.turns[sourceTurnIndex];
|
|
1525
|
+
} else sourceDiagnostics.push('source-turn-index-invalid');
|
|
1526
|
+
} else if (found.turnId !== null) {
|
|
1527
|
+
const matches = parsed.turns.map((t, index) => ({ t, index })).filter(({ t }) => t.turnId === found.turnId);
|
|
1528
|
+
if (matches.length === 1) { sourceTurnIndex = matches[0]!.index; turn = matches[0]!.t; }
|
|
1529
|
+
else sourceDiagnostics.push('source-turn-identity-missing-or-ambiguous');
|
|
1530
|
+
} else if (parsed.granularity === 'session' && parsed.turns.length === 0 && evidence['turnId'] == null) sessionScope = true;
|
|
1531
|
+
if (!turn && !sessionScope) { sourceDiagnostics.push('source-turn-association-unavailable'); independent = false; }
|
|
1532
|
+
if (turn?.turnId != null && (parsed.turns.filter((t) => t.turnId === turn!.turnId).length !== 1 || (found.turnId !== null && found.turnId !== turn.turnId))) sourceDiagnostics.push('source-turn-identity-conflict');
|
|
1533
|
+
if (evidence['turnId'] != null && (evidence['turnId'] !== found.turnId || (turn?.turnId != null && evidence['turnId'] !== turn.turnId))) sourceDiagnostics.push('captured-turn-identity-conflict');
|
|
1534
|
+
const explicitLegacyAssociation = receipt['turnIndex'] == null && receipt['turnId'] != null && receipt['turnId'] === found.turnId
|
|
1535
|
+
&& turn?.turnId === found.turnId && parsed.turns.filter((t) => t.turnId === found.turnId).length === 1;
|
|
1536
|
+
if (receipt['turnIndex'] !== found.turnIndex && !explicitLegacyAssociation && !(sessionScope && receipt['turnIndex'] == null && found.turnIndex == null)) sourceDiagnostics.push('captured-source-identity-mismatch:turnIndex');
|
|
1537
|
+
if (turn || sessionScope) {
|
|
1538
|
+
scopeAssociations.add(turn ? 'turn:' + sourceTurnIndex : 'session');
|
|
1539
|
+
const authority = turn ?? parsed;
|
|
1540
|
+
if (evidence['capturedFrom'] !== authority.startedAt) sourceDiagnostics.push('captured-source-start-scope-mismatch');
|
|
1541
|
+
if (identity.model !== authority.model || authority.cwd === null || resolve(authority.cwd) !== project
|
|
1542
|
+
|| row['reportedTotalBasis'] !== 'raw-inclusive' || row['inputCacheSemantics'] !== 'includes-cache-read-write') sourceDiagnostics.push('captured-source-scope-mismatch');
|
|
1543
|
+
}
|
|
1544
|
+
if (found.timestamp !== null && ((typeof evidence['capturedFrom'] === 'string' && Date.parse(found.timestamp) < Date.parse(evidence['capturedFrom'])) || (typeof evidence['capturedTo'] === 'string' && Date.parse(found.timestamp) > Date.parse(evidence['capturedTo'])))) sourceDiagnostics.push('captured-source-window-mismatch');
|
|
1545
|
+
if (found.payloadDigest !== receipt['payloadDigest']) sourceDiagnostics.push('source-payload-conflict');
|
|
1546
|
+
if (!found.responseId) { independent = false; diagnostics.push('source-receipt-identity-unavailable'); }
|
|
1547
|
+
witnesses.push({ evidenceKey: key, tokensTotal: found.totals.total, reportedTotalBasis: 'raw-inclusive' });
|
|
1548
|
+
if (found.totals.total === null) allKnown = false; else scopedTotal += found.totals.total;
|
|
1549
|
+
for (const [field, value] of Object.entries({ tokensIn: found.totals.input, tokensOut: found.totals.output, tokensCacheRead: found.totals.cachedInput,
|
|
1550
|
+
tokensCacheWrite: found.totals.cachedWrite ?? null, tokensReasoning: found.totals.reasoning })) scopedDimensions[field] = scopedDimensions[field] === null || value === null ? null : scopedDimensions[field]! + value;
|
|
1551
|
+
}
|
|
1552
|
+
if (allKnown && !Number.isSafeInteger(scopedTotal)) sourceDiagnostics.push('source-scoped-aggregate-overflow:total');
|
|
1553
|
+
for (const [field, value] of Object.entries(scopedDimensions)) if (value !== null && !Number.isSafeInteger(value)) sourceDiagnostics.push('source-scoped-aggregate-overflow:' + field);
|
|
1554
|
+
if (parsed && !('error' in parsed)) for (const scope of parsed.scopeDiagnostics ?? []) {
|
|
1555
|
+
// A matching prefix and recorded end bound establish the SAME used witness scope.
|
|
1556
|
+
if (scope.receiptCount !== matchedSourceKeys.size || maxSourceRecord < 0 || maxSourceRecord > scope.sourceRecord
|
|
1557
|
+
|| (scope.timestamp !== null && typeof evidence['capturedTo'] === 'string' && Date.parse(scope.timestamp) > Date.parse(evidence['capturedTo']))) continue;
|
|
1558
|
+
for (const diagnostic of scope.diagnostics) sourceDiagnostics.push('source-scope:' + diagnostic);
|
|
1559
|
+
if (scope.witness) for (const [field, actual] of Object.entries({ total: allKnown ? scopedTotal : null, input: scopedDimensions['tokensIn'], output: scopedDimensions['tokensOut'], cachedInput: scopedDimensions['tokensCacheRead'], cachedWrite: scopedDimensions['tokensCacheWrite'], reasoning: scopedDimensions['tokensReasoning'] })) {
|
|
1560
|
+
const witness = scope.witness[field as keyof typeof scope.witness];
|
|
1561
|
+
if (actual != null && witness != null && actual !== witness) sourceDiagnostics.push('source-scoped-witness-mismatch:' + field);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (scopeAssociations.size > 1) sourceDiagnostics.push('captured-mixed-turn-association');
|
|
1565
|
+
// Verify the captured receipt subset, not an expanded session's later cumulative total.
|
|
1566
|
+
// A malformed record still leaves the source census undecidable; receipt mutations are
|
|
1567
|
+
// detected above by their captured payload hashes and identities.
|
|
1568
|
+
if (parsed && !('error' in parsed) && parsed.diagnostics?.includes('malformed-record')) sourceDiagnostics.push('source:malformed-record');
|
|
1569
|
+
if (Array.isArray(row['usageDiagnostics']) && row['usageDiagnostics'].some((d) => typeof d === 'string' && /invalid|mismatch|conflict|reset|overflow|foreign/.test(d))) sourceDiagnostics.push('captured-source-was-invalid');
|
|
1570
|
+
const claimTotal = row['tokensTotal'] ?? row['tokens'];
|
|
1571
|
+
if (parsed && !('error' in parsed) && allKnown && Number.isSafeInteger(scopedTotal) && claimTotal !== scopedTotal) sourceDiagnostics.push('source-claim-total-mismatch');
|
|
1572
|
+
if (parsed && !('error' in parsed) && allKnown && row['tokens'] != null && row['tokens'] !== scopedTotal) sourceDiagnostics.push('source-compatibility-total-mismatch');
|
|
1573
|
+
for (const [field, value] of Object.entries(scopedDimensions)) if (parsed && !('error' in parsed) && row[field] != null && value !== null && row[field] !== value) sourceDiagnostics.push('source-dimension-mismatch:' + field);
|
|
1574
|
+
if (!captured.length) { rows.push(row); independent = false; diagnostics.push('source-scope-empty'); }
|
|
1575
|
+
}
|
|
1576
|
+
if (!rows.length && selected.some((row) => row['summary'] === true)) diagnostics.push('projection-only-spend-unavailable');
|
|
1577
|
+
if (sourceDiagnostics.length) for (const row of rows) row['usageDiagnostics'] = [...(Array.isArray(row['usageDiagnostics']) ? row['usageDiagnostics'] : []), 'source-payload-conflict'];
|
|
1578
|
+
return build({ sourceKind: 'fa-ledger', sourcePath: faPath, runId, rows,
|
|
1579
|
+
...(independent ? { expected, witnesses } : {}), moneyObservations, diagnostics, sourceDiagnostics });
|
|
1580
|
+
}
|
|
1581
|
+
if (chosen === 'claude-transcript') {
|
|
1582
|
+
if (claudeAmbiguous) return insufficient('source-selection-ambiguous');
|
|
1583
|
+
const legacy = deriveCostLedger({ ...(opts.epsilon !== undefined ? { epsilon: opts.epsilon } : {}), projectRoot: project, ...(claudeRefs[0] !== undefined ? { runId: claudeRefs[0].runId } : opts.runId !== undefined ? { runId: opts.runId } : {}), ...(opts.slug !== undefined ? { slug: opts.slug } : {}), ...(opts.projectsRoot !== undefined ? { projectsRoot: opts.projectsRoot } : {}) });
|
|
1584
|
+
if (!legacy || (opts.slug !== undefined && legacy.slug !== opts.slug)) return insufficient('claude-source-missing');
|
|
1585
|
+
if (legacy.authoritativeEvidence) { roots.push(...legacy.authoritativeEvidence.roots); files.push(...legacy.authoritativeEvidence.files); }
|
|
1586
|
+
const report = build({ sourceKind: 'claude-transcript', sourcePath: opts.projectsRoot ?? claudeProjectsRoot(), runId: legacy.runId,
|
|
1587
|
+
rows: legacy.rows.map((row) => ({ runId: row.runId, stage: row.stage, model: row.model, family: 'claude', attempt: row.attempt,
|
|
1588
|
+
tokensTotal: row.weightedTokens, reportedTotalBasis: 'weighted-input-equivalent', evidenceKey: JSON.stringify([row.runId, row.stage, row.attempt]) })), diagnostics: ['legacy-weighted-transcript-view'] });
|
|
1589
|
+
return { ...report, legacyCostLedger: legacy };
|
|
1590
|
+
}
|
|
1591
|
+
return insufficient('no-selected-project-source');
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
export function renderStageUsageReport(report: ReturnType<typeof buildStageUsageReport>): string {
|
|
1595
|
+
const number = (n: number | null) => n === null ? 'unavailable' : String(n);
|
|
1596
|
+
const rows = report.rows.map((row) => `${row.stage ?? 'unattributed'} | ${row.model ?? 'unknown model'} | total ${number(row.tokensTotal)} (${row.reportedTotalBasis}) | input ${number(row.tokensIn)} cache-read ${number(row.tokensCacheRead)} cache-write ${number(row.tokensCacheWrite)} output ${number(row.tokensOut)} | estimated USD ${number(row.estimatedCostUsd)}`);
|
|
1597
|
+
return [`usage --by-stage: ${report.verdict} — ${report.sourceKind} ${report.sourcePath}`, `metric: ${report.metric}; known subtotal ${number(report.knownRunTotalTokens)}; full total ${number(report.runTotalTokens)}`,
|
|
1598
|
+
`conservation: ${report.conservation.status}; inventory: ${report.inventory.status}; source verification: ${report.sourceVerification.status}; source verified total ${number(report.sourceVerifiedTotalTokens)}`,
|
|
1599
|
+
`reported USD ${number(report.reportedCostUsd)}; known reported subtotal ${number(report.knownReportedCostUsd)}; money coverage ${report.reportedCostCoverage.status}`,
|
|
1600
|
+
`estimated USD ${number(report.estimatedCostUsd)}; known estimated subtotal ${number(report.knownEstimatedCostUsd)}; billed USD unavailable (not observed)`, ...rows,
|
|
1601
|
+
...report.diagnostics.map((d) => 'diagnostic: ' + d), ...report.sourceVerification.diagnostics.map((d) => 'source: ' + d)].join('\n');
|
|
1602
|
+
}
|
|
@@ -2681,6 +2681,28 @@ export function parsePlanGateVerdict(raw: string | null | undefined): PlanGateVe
|
|
|
2681
2681
|
return { verdict: byName, exit: exitCode, reason: reason, output: output }
|
|
2682
2682
|
}
|
|
2683
2683
|
|
|
2684
|
+
/** Verdict of the design/plan/plan-repair Codex landing barrier (landed-barrier-anchored-line). */
|
|
2685
|
+
export interface LandedProbeVerdict { landed: boolean; bytes: number | null; reason: 'landed' | 'empty-agent-reply' | 'no-landed-line' | 'zero-bytes' }
|
|
2686
|
+
|
|
2687
|
+
/**
|
|
2688
|
+
* Read the transport agent's reply to `landedProbeCmd` (backlog 1f0353f7bdb53588). The probe writes its
|
|
2689
|
+
* verdict as its LAST line — `landed=<bytes>` for a non-empty file, `absent` otherwise — so only the
|
|
2690
|
+
* last non-empty line decides, and it must be exactly `landed=<digits>` with a positive number. A
|
|
2691
|
+
* `landed=` anywhere else (a diagnostic the agent added, an earlier line) is not a landing: the old
|
|
2692
|
+
* substring read accepted it. After the probe line only blank lines and code-fence lines may follow —
|
|
2693
|
+
* an agent wrapping "stdout verbatim" in a fence adds no content; any other trailing text means the
|
|
2694
|
+
* last word is no longer the probe's, and the barrier falls back (the safe direction).
|
|
2695
|
+
*/
|
|
2696
|
+
export function parseLandedProbe(raw: string | null | undefined): LandedProbeVerdict {
|
|
2697
|
+
const lines = String(raw === null || raw === undefined ? '' : raw).split('\n').map(function (l) { return l.trim() }).filter(function (l) { return l !== '' && !/^\x60\x60\x60+[\w-]*$/.test(l) })
|
|
2698
|
+
if (lines.length === 0) return { landed: false, bytes: null, reason: 'empty-agent-reply' }
|
|
2699
|
+
const m = /^landed=(\d+)$/.exec(String(lines[lines.length - 1]))
|
|
2700
|
+
if (m === null) return { landed: false, bytes: null, reason: 'no-landed-line' }
|
|
2701
|
+
const bytes = Number(m[1])
|
|
2702
|
+
if (!(bytes > 0)) return { landed: false, bytes: bytes, reason: 'zero-bytes' }
|
|
2703
|
+
return { landed: true, bytes: bytes, reason: 'landed' }
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2684
2706
|
/** Snapshot of a plan taken around the ONE repair round (FR-6 preservation check, plan-inherits-requirements). */
|
|
2685
2707
|
export interface PlanSnapshot { present: boolean; len: number; cksum: number | null; headings: string[]; targets: string[] }
|
|
2686
2708
|
|
package/src/guard.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type VolumeShadowResult,
|
|
23
23
|
} from './guard-volume.js';
|
|
24
24
|
import { findReleaseLine } from './release-line.js';
|
|
25
|
+
import type { NpmHomepageFactSet } from './npm-homepage.js';
|
|
25
26
|
|
|
26
27
|
export type GuardSeverity = 'hard' | 'soft';
|
|
27
28
|
// The 'code' operation checks facts already established when code changes, such as shared skill-copy
|
|
@@ -216,6 +217,12 @@ export interface GuardFacts {
|
|
|
216
217
|
};
|
|
217
218
|
/** for skills-registrable: per skill pack, dirs that would ship un-registrable (no depth-1 SKILL.md). */
|
|
218
219
|
readonly skillPacks?: readonly { readonly name: string; readonly nonRegistrable: readonly string[] }[];
|
|
220
|
+
/**
|
|
221
|
+
* for npm-homepage: one entry per `packages/@dzhechkov/*` package (private and hidden dirs included) plus
|
|
222
|
+
* every entry the reader could not decide, built by the pure `npmHomepageFacts`. No packages and no
|
|
223
|
+
* failures, or an unreadable packages directory, is not a pass — the rule is then NOT ESTABLISHED.
|
|
224
|
+
*/
|
|
225
|
+
readonly npmHomepage?: NpmHomepageFactSet;
|
|
219
226
|
/** for readme-first: per publishable package, is a version bump staged without a README change? */
|
|
220
227
|
readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean; readonly versionUnknown?: boolean }[];
|
|
221
228
|
/**
|
|
@@ -541,6 +548,7 @@ export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
|
541
548
|
{ id: 'rounds-closed', severity: 'soft', ops: ['publish'], description: 'focused rounds older than 120 minutes are named before publish; a dead owner is reported as an abandoned round' },
|
|
542
549
|
{ id: 'rounds-traced', severity: 'soft', ops: ['publish'], description: '10 or more package-code commits without a round ledger receipt are named; unavailable git evidence is not a pass' },
|
|
543
550
|
{ id: 'no-workspace-star', severity: 'hard', ops: ['publish'], description: 'a published package.json must carry no workspace:* dep (npm ships it verbatim → the install breaks)' },
|
|
551
|
+
{ id: 'npm-homepage', severity: 'hard', ops: ['publish'], description: 'every packages/@dzhechkov/* package.json (private too) has homepage = https://aicoding.space exactly, and keeps GitHub in repository.url + repository.directory and bugs.url' },
|
|
544
552
|
{ id: 'plugin-manifest-audit', severity: 'hard', ops: ['publish'], description: 'every .claude-plugin/plugin.json parses and declares a non-empty name, description and a STRICT N.N.N version' },
|
|
545
553
|
{ id: 'sibling-dep-protocol', severity: 'hard', ops: ['publish'], description: 'a dependencies/devDependencies entry on a sibling @dzhechkov package must use the workspace: protocol on disk (peer/optional deps are deliberately exempt — a range is their point)' },
|
|
546
554
|
{ id: 'no-skill-drift', severity: 'hard', ops: ['publish', 'consolidate', 'code'], description: 'no unexpected byte-drift between shared skill copies' },
|
|
@@ -725,6 +733,38 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
|
|
|
725
733
|
}
|
|
726
734
|
return out;
|
|
727
735
|
},
|
|
736
|
+
'npm-homepage': (f, sev) => {
|
|
737
|
+
// Owner rule 2026-09-28 (.claude/rules/npm-homepage.md), lifted from rules text to a gate. One violation
|
|
738
|
+
// per package, listing all of its defects, so the fix list is the violation list. A malformed fact entry
|
|
739
|
+
// is a violation too: a rule that cannot read its evidence accuses, it does not acquit.
|
|
740
|
+
// Fix round 1 (review r1 finding 3): an entry is accepted only with a non-empty dir AND name and a
|
|
741
|
+
// problems array of non-empty strings — `{ problems: [] }` used to count as clean evidence.
|
|
742
|
+
const out: Violation[] = [];
|
|
743
|
+
const nonEmpty = (v: unknown): v is string => typeof v === 'string' && v !== '';
|
|
744
|
+
const set = f.npmHomepage!; // Presence is established by HAS_INPUT.
|
|
745
|
+
const packages: readonly unknown[] = Array.isArray(set.packages) ? set.packages : [];
|
|
746
|
+
packages.forEach((p, i) => {
|
|
747
|
+
const e = (p && typeof p === 'object' ? p : {}) as Record<string, unknown>;
|
|
748
|
+
const problems = e['problems'];
|
|
749
|
+
if (!nonEmpty(e['dir']) || !nonEmpty(e['name']) || !Array.isArray(problems) || !problems.every(nonEmpty)) {
|
|
750
|
+
const who = nonEmpty(e['name']) ? e['name'] : nonEmpty(e['dir']) ? `packages/@dzhechkov/${e['dir']}` : `entry #${i}`;
|
|
751
|
+
out.push({ rule: 'npm-homepage', severity: sev, detail: `${who}: malformed npm-homepage fact (needs non-empty dir, name and string problems) — ${JSON.stringify(p)}` });
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (problems.length === 0) return;
|
|
755
|
+
out.push({ rule: 'npm-homepage', severity: sev, detail: `${e['name']} (packages/@dzhechkov/${e['dir']}): ${(problems as string[]).join('; ')}` });
|
|
756
|
+
});
|
|
757
|
+
// Fix round 1 (review r1 finding 2): an entry the reader could not decide is a violation naming the path,
|
|
758
|
+
// never a silent omission — partial discovery must not look complete.
|
|
759
|
+
const failures: readonly unknown[] = Array.isArray(set.discoveryFailures) ? set.discoveryFailures : [];
|
|
760
|
+
failures.forEach((d, i) => {
|
|
761
|
+
const e = (d && typeof d === 'object' ? d : {}) as Record<string, unknown>;
|
|
762
|
+
const where = nonEmpty(e['path']) ? e['path'] : `discovery failure #${i}`;
|
|
763
|
+
const why = nonEmpty(e['reason']) ? e['reason'] : 'no reason given';
|
|
764
|
+
out.push({ rule: 'npm-homepage', severity: sev, detail: `${where}: could not be inspected (${why}) — a package here would go unchecked` });
|
|
765
|
+
});
|
|
766
|
+
return out;
|
|
767
|
+
},
|
|
728
768
|
'plugin-manifest-audit': (f, sev) => {
|
|
729
769
|
/**
|
|
730
770
|
* ЗНАЧЕНИЕ ЕСТЬ, ТОЛЬКО ЕСЛИ ОНО ВИДНО. `trim()` не убирает нулевой ширины пробел и его
|
|
@@ -1182,6 +1222,12 @@ const HAS_INPUT: Partial<Record<string, (f: GuardFacts) => boolean>> = {
|
|
|
1182
1222
|
'readme-consistency': (f) => Array.isArray(f.counts),
|
|
1183
1223
|
'skills-registrable': (f) => Array.isArray(f.skillPacks),
|
|
1184
1224
|
'readme-first': (f) => Array.isArray(f.readmeFirst),
|
|
1225
|
+
// Zero packages is "nobody looked", not "all compliant": vacuity is NOT ESTABLISHED, never green.
|
|
1226
|
+
'npm-homepage': (f) => {
|
|
1227
|
+
const s = f.npmHomepage;
|
|
1228
|
+
if (typeof s !== 'object' || s === null || s.unreadableRoot !== undefined) return false;
|
|
1229
|
+
return (Array.isArray(s.packages) && s.packages.length > 0) || (Array.isArray(s.discoveryFailures) && s.discoveryFailures.length > 0);
|
|
1230
|
+
},
|
|
1185
1231
|
'store-bloat-cap': (f) => f.store !== undefined,
|
|
1186
1232
|
'template-context-token-weight': volumeInputPresent,
|
|
1187
1233
|
'template-context-largest-file-share': volumeInputPresent,
|
|
@@ -1397,6 +1443,12 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
|
|
|
1397
1443
|
notes.push(`backlog-covers-features: летопись переходов не прочитана (${error}) — покрытие проверено только по текстам записей бэклога`);
|
|
1398
1444
|
}
|
|
1399
1445
|
}
|
|
1446
|
+
// Only an UNREADABLE packages directory earns a note: a tree that simply has no packages/@dzhechkov (most
|
|
1447
|
+
// test fixtures, any non-monorepo root) is "nothing to check", and a note there would be noise on every run.
|
|
1448
|
+
if (notEstablished.includes('npm-homepage')) {
|
|
1449
|
+
const root = facts.npmHomepage?.unreadableRoot;
|
|
1450
|
+
if (typeof root === 'string') notes.push(`npm-homepage: NOT ESTABLISHED — the packages directory could not be read (${root})`);
|
|
1451
|
+
}
|
|
1400
1452
|
if (notEstablished.includes('rounds-traced')) {
|
|
1401
1453
|
const fact = facts.codeCommitsSinceLastRound;
|
|
1402
1454
|
if (fact?.enabled === false) notes.push('rounds-traced: skipped (.dz/config.json rounds.traced=false)');
|