@dzhechkov/harness-core 0.8.46 → 0.8.48

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.
Files changed (153) hide show
  1. package/.dz-manifest.json +200 -168
  2. package/README.md +130 -2
  3. package/dist/agentdb-index.d.ts.map +1 -1
  4. package/dist/agentdb-index.js +32 -2
  5. package/dist/agentdb-index.js.map +1 -1
  6. package/dist/agentdb-snapshot.d.ts +14 -1
  7. package/dist/agentdb-snapshot.d.ts.map +1 -1
  8. package/dist/agentdb-snapshot.js +100 -4
  9. package/dist/agentdb-snapshot.js.map +1 -1
  10. package/dist/apply-leg.d.ts +9 -1
  11. package/dist/apply-leg.d.ts.map +1 -1
  12. package/dist/apply-leg.js +26 -4
  13. package/dist/apply-leg.js.map +1 -1
  14. package/dist/architecture.d.ts +0 -5
  15. package/dist/architecture.d.ts.map +1 -1
  16. package/dist/architecture.js +25 -2
  17. package/dist/architecture.js.map +1 -1
  18. package/dist/codex-rollouts.d.ts +35 -7
  19. package/dist/codex-rollouts.d.ts.map +1 -1
  20. package/dist/codex-rollouts.js +201 -113
  21. package/dist/codex-rollouts.js.map +1 -1
  22. package/dist/cost-ledger.d.ts +42 -3
  23. package/dist/cost-ledger.d.ts.map +1 -1
  24. package/dist/cost-ledger.js +478 -15
  25. package/dist/cost-ledger.js.map +1 -1
  26. package/dist/feature-adr-routing.d.ts +16 -0
  27. package/dist/feature-adr-routing.d.ts.map +1 -1
  28. package/dist/feature-adr-routing.js +21 -0
  29. package/dist/feature-adr-routing.js.map +1 -1
  30. package/dist/guard.d.ts +7 -0
  31. package/dist/guard.d.ts.map +1 -1
  32. package/dist/guard.js +48 -0
  33. package/dist/guard.js.map +1 -1
  34. package/dist/index.d.ts +11 -6
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +10 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/loop-plan.d.ts +4 -0
  39. package/dist/loop-plan.d.ts.map +1 -1
  40. package/dist/loop-plan.js +4 -0
  41. package/dist/loop-plan.js.map +1 -1
  42. package/dist/mutation-gate.d.ts +36 -1
  43. package/dist/mutation-gate.d.ts.map +1 -1
  44. package/dist/mutation-gate.js +109 -19
  45. package/dist/mutation-gate.js.map +1 -1
  46. package/dist/npm-homepage.d.ts +64 -0
  47. package/dist/npm-homepage.d.ts.map +1 -0
  48. package/dist/npm-homepage.js +109 -0
  49. package/dist/npm-homepage.js.map +1 -0
  50. package/dist/operations.d.ts +6 -0
  51. package/dist/operations.d.ts.map +1 -1
  52. package/dist/operations.js +150 -24
  53. package/dist/operations.js.map +1 -1
  54. package/dist/pack-inventory.d.ts.map +1 -1
  55. package/dist/pack-inventory.js +6 -1
  56. package/dist/pack-inventory.js.map +1 -1
  57. package/dist/parity.d.ts.map +1 -1
  58. package/dist/parity.js +4 -1
  59. package/dist/parity.js.map +1 -1
  60. package/dist/publish-sibling-drift.d.ts +43 -1
  61. package/dist/publish-sibling-drift.d.ts.map +1 -1
  62. package/dist/publish-sibling-drift.js +269 -13
  63. package/dist/publish-sibling-drift.js.map +1 -1
  64. package/dist/publish.d.ts +5 -1
  65. package/dist/publish.d.ts.map +1 -1
  66. package/dist/publish.js +18 -1
  67. package/dist/publish.js.map +1 -1
  68. package/dist/qe-bridge.d.ts +63 -1
  69. package/dist/qe-bridge.d.ts.map +1 -1
  70. package/dist/qe-bridge.js +82 -0
  71. package/dist/qe-bridge.js.map +1 -1
  72. package/dist/release-package-audit.d.ts +86 -0
  73. package/dist/release-package-audit.d.ts.map +1 -0
  74. package/dist/release-package-audit.js +272 -0
  75. package/dist/release-package-audit.js.map +1 -0
  76. package/dist/release.d.ts +8 -1
  77. package/dist/release.d.ts.map +1 -1
  78. package/dist/release.js +58 -32
  79. package/dist/release.js.map +1 -1
  80. package/dist/round.d.ts +10 -1
  81. package/dist/round.d.ts.map +1 -1
  82. package/dist/round.js +5 -1
  83. package/dist/round.js.map +1 -1
  84. package/dist/run-records.d.ts +11 -0
  85. package/dist/run-records.d.ts.map +1 -1
  86. package/dist/run-records.js +124 -20
  87. package/dist/run-records.js.map +1 -1
  88. package/dist/setup-memory-deps.d.ts +14 -0
  89. package/dist/setup-memory-deps.d.ts.map +1 -0
  90. package/dist/setup-memory-deps.js +210 -0
  91. package/dist/setup-memory-deps.js.map +1 -0
  92. package/dist/setup.d.ts +2 -0
  93. package/dist/setup.d.ts.map +1 -1
  94. package/dist/setup.js +379 -378
  95. package/dist/setup.js.map +1 -1
  96. package/dist/stage-usage.d.ts +248 -0
  97. package/dist/stage-usage.d.ts.map +1 -0
  98. package/dist/stage-usage.js +511 -0
  99. package/dist/stage-usage.js.map +1 -0
  100. package/dist/statusline.d.ts +23 -0
  101. package/dist/statusline.d.ts.map +1 -1
  102. package/dist/statusline.js +167 -1
  103. package/dist/statusline.js.map +1 -1
  104. package/dist/workflow-run-dispatch.d.ts +36 -19
  105. package/dist/workflow-run-dispatch.d.ts.map +1 -1
  106. package/dist/workflow-run-dispatch.js +255 -102
  107. package/dist/workflow-run-dispatch.js.map +1 -1
  108. package/dist/workflow-run.d.ts +19 -3
  109. package/dist/workflow-run.d.ts.map +1 -1
  110. package/dist/workflow-run.js +93 -2
  111. package/dist/workflow-run.js.map +1 -1
  112. package/package.json +3 -3
  113. package/sbom.json +303 -223
  114. package/src/agentdb-index.ts +31 -2
  115. package/src/agentdb-snapshot.ts +105 -4
  116. package/src/apply-leg.ts +26 -4
  117. package/src/architecture.ts +15 -2
  118. package/src/codex-rollouts.ts +155 -147
  119. package/src/cost-ledger.ts +308 -19
  120. package/src/feature-adr-routing.ts +22 -0
  121. package/src/guard.ts +52 -0
  122. package/src/index.ts +15 -3
  123. package/src/loop-plan.ts +8 -0
  124. package/src/mutation-gate.ts +153 -19
  125. package/src/npm-homepage.ts +142 -0
  126. package/src/operations.ts +177 -22
  127. package/src/pack-inventory.ts +6 -1
  128. package/src/parity.ts +4 -1
  129. package/src/publish-sibling-drift.ts +214 -11
  130. package/src/publish.ts +20 -1
  131. package/src/qe-bridge.ts +86 -1
  132. package/src/release-package-audit.ts +264 -0
  133. package/src/release.ts +55 -22
  134. package/src/round.ts +8 -0
  135. package/src/run-records.ts +101 -22
  136. package/src/setup-memory-deps.ts +166 -0
  137. package/src/setup.ts +95 -98
  138. package/src/stage-usage.ts +385 -0
  139. package/src/statusline.ts +149 -1
  140. package/src/workflow-run-dispatch.ts +197 -93
  141. package/src/workflow-run.ts +101 -5
  142. package/dist/ledger-cost-fill.d.ts +0 -58
  143. package/dist/ledger-cost-fill.d.ts.map +0 -1
  144. package/dist/ledger-cost-fill.js +0 -78
  145. package/dist/ledger-cost-fill.js.map +0 -1
  146. package/dist/retro.d.ts +0 -131
  147. package/dist/retro.d.ts.map +0 -1
  148. package/dist/retro.js +0 -207
  149. package/dist/retro.js.map +0 -1
  150. package/dist/sbom.d.ts +0 -42
  151. package/dist/sbom.d.ts.map +0 -1
  152. package/dist/sbom.js +0 -120
  153. package/dist/sbom.js.map +0 -1
@@ -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
- if (!hasKnownPricing(stage.model)) b.pricingKnown = false;
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
- b.costUsd += Number.isFinite(cost) && cost > 0 ? cost : 0;
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
- let totalCostUsd = 0;
797
- for (const r of rows) totalCostUsd += r.costUsd;
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 += row.costUsd;
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 marked * uses sonnet-class FALLBACK pricing for: ${report.pricingFallbackModels.join(', ')}`);
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
- return buildCostLedger({
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)');