@dzhechkov/harness-core 0.8.37 → 0.8.38

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/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "8681fe97661ea41329df7e8d544edb9a9eb71bd563888dfcdf56c593e8f8de0f"
28
+ "content": "e245c714c75f1e53186aea9bb38c0767c72a1911c4abc3066f270d206ae410b7"
29
29
  }
30
30
  ]
31
31
  },
@@ -1715,7 +1715,7 @@
1715
1715
  "hashes": [
1716
1716
  {
1717
1717
  "alg": "SHA-256",
1718
- "content": "2bc158ecece08d9d3d829adadcfc82f309dd7921633cee96d808d9d3d24ac09b"
1718
+ "content": "d8c21e3cc28e57c56ee22b4eee3dff98c0fbf0b6df93f5b8acbacf1c787b16f1"
1719
1719
  }
1720
1720
  ]
1721
1721
  },
@@ -1725,7 +1725,7 @@
1725
1725
  "hashes": [
1726
1726
  {
1727
1727
  "alg": "SHA-256",
1728
- "content": "87284ca644e3b95d796ffba24095b2b70572e160246841f00b65c9ce2904307b"
1728
+ "content": "b8489d2f519a9090ad9053b9613952a733e258b6b58509a4a213534d8d987b4d"
1729
1729
  }
1730
1730
  ]
1731
1731
  },
@@ -1735,7 +1735,7 @@
1735
1735
  "hashes": [
1736
1736
  {
1737
1737
  "alg": "SHA-256",
1738
- "content": "61916c0c548741c299867992856f74289575f730c2bf541b24cac95d503c1135"
1738
+ "content": "046fa4d0e04a56b3b8dd6bba542de25a99b000f03f27e9d192046043702e1213"
1739
1739
  }
1740
1740
  ]
1741
1741
  },
@@ -1745,7 +1745,7 @@
1745
1745
  "hashes": [
1746
1746
  {
1747
1747
  "alg": "SHA-256",
1748
- "content": "eb25f9acdb75bfd6414d78c5d3d4eaf9c7ee55fdc473572e555f55d0ede1a269"
1748
+ "content": "36cb70ec0ea7b130e420c3c1baea0543958b5fbeb9d75e6f8064eedd7df89a25"
1749
1749
  }
1750
1750
  ]
1751
1751
  },
@@ -2445,7 +2445,7 @@
2445
2445
  "hashes": [
2446
2446
  {
2447
2447
  "alg": "SHA-256",
2448
- "content": "08e2f3281134246237badf405152df859b8c2b3c798bbf96e6a2cdea4e697551"
2448
+ "content": "fc45729c438028377eb646c9c66eaa5bee82ed37aa08bc2d14db64e3eb3cdc29"
2449
2449
  }
2450
2450
  ]
2451
2451
  },
@@ -2455,7 +2455,7 @@
2455
2455
  "hashes": [
2456
2456
  {
2457
2457
  "alg": "SHA-256",
2458
- "content": "5beac314ff52187934da91d8243e417156cb73f214c4bd6485ef1f7c4948f138"
2458
+ "content": "5a3de377ea062c34da34b2ed315997fdaf11a4ea90ce70c2f867294e87fb53bc"
2459
2459
  }
2460
2460
  ]
2461
2461
  },
@@ -2465,7 +2465,7 @@
2465
2465
  "hashes": [
2466
2466
  {
2467
2467
  "alg": "SHA-256",
2468
- "content": "0952a178e1942572adfde30c7449f8f11c6d57265fac93607cacb1cb9829d3f5"
2468
+ "content": "d5db78aadc6c4ae5482b44513e873b36347b1a5b9888c3bb18be9e49dff6b34b"
2469
2469
  }
2470
2470
  ]
2471
2471
  },
@@ -2795,7 +2795,7 @@
2795
2795
  "hashes": [
2796
2796
  {
2797
2797
  "alg": "SHA-256",
2798
- "content": "5f0446cc54af8c90e525bad6189f93873ad1592c1d1824c18a2f0991dd5959b9"
2798
+ "content": "5323d4f19ccbcf5929a61a4849b17e0cb39c248205a14967610dda85635c6ddb"
2799
2799
  }
2800
2800
  ]
2801
2801
  },
@@ -2805,7 +2805,7 @@
2805
2805
  "hashes": [
2806
2806
  {
2807
2807
  "alg": "SHA-256",
2808
- "content": "62df6d6a2f6f861b8485e9890c272a335cc9d697bee4064faa83de36fe3d7413"
2808
+ "content": "9371106d16ee809aa6bc19410087491840654c1f268f20928b8ccc148a52e7fd"
2809
2809
  }
2810
2810
  ]
2811
2811
  },
@@ -2815,7 +2815,7 @@
2815
2815
  "hashes": [
2816
2816
  {
2817
2817
  "alg": "SHA-256",
2818
- "content": "3d61d50137956af1322911629db07737f857d81fde22cf1594dfc7aed5b03341"
2818
+ "content": "8c1913c94939655785cf62534d8c61bbb8a0e85d8e77f432de4ecabb73ac6d73"
2819
2819
  }
2820
2820
  ]
2821
2821
  },
@@ -2825,7 +2825,7 @@
2825
2825
  "hashes": [
2826
2826
  {
2827
2827
  "alg": "SHA-256",
2828
- "content": "ffe8d30bc1182349088f2712a080cfb32240111a874c3fd3217a0e123953f2d7"
2828
+ "content": "7a46163decf648a7214ece84dadad0bef80fbc47d0f9f123f6cab475da40e1ba"
2829
2829
  }
2830
2830
  ]
2831
2831
  },
@@ -4515,7 +4515,7 @@
4515
4515
  "hashes": [
4516
4516
  {
4517
4517
  "alg": "SHA-256",
4518
- "content": "7bf6a9f4bbd79eef7858d3a5e073fbb897d7af01accf9257894635664432f449"
4518
+ "content": "f1964fe74508371aa7abc3ac067310e141548d3a5f99e1ebfecf8403d5e68daa"
4519
4519
  }
4520
4520
  ]
4521
4521
  },
@@ -4525,7 +4525,7 @@
4525
4525
  "hashes": [
4526
4526
  {
4527
4527
  "alg": "SHA-256",
4528
- "content": "73072e6cd684b6bb3638881f3719f1dd39b8d1b02c9ea1f2a1efb8b3ebfa6a83"
4528
+ "content": "1b323500c270897632a1322a07bf5955eaf08e67421096f573913ac3c6723322"
4529
4529
  }
4530
4530
  ]
4531
4531
  },
@@ -4545,7 +4545,7 @@
4545
4545
  "hashes": [
4546
4546
  {
4547
4547
  "alg": "SHA-256",
4548
- "content": "f5bf74e490c5a0f0b49042fa89f2c9156dae1ca60fdef822a14be21f8fc378a8"
4548
+ "content": "1f3d05d697dc24e811ea5ef194439c4b25d992dbf01b8600d266091ea5a52aeb"
4549
4549
  }
4550
4550
  ]
4551
4551
  },
@@ -5229,6 +5229,46 @@
5229
5229
  }
5230
5230
  ]
5231
5231
  },
5232
+ {
5233
+ "type": "file",
5234
+ "name": "dist/review-cost.d.ts",
5235
+ "hashes": [
5236
+ {
5237
+ "alg": "SHA-256",
5238
+ "content": "0488bbb484a390759262da1244870b2f3df10abe96169d7604e8585a618d7fd2"
5239
+ }
5240
+ ]
5241
+ },
5242
+ {
5243
+ "type": "file",
5244
+ "name": "dist/review-cost.d.ts.map",
5245
+ "hashes": [
5246
+ {
5247
+ "alg": "SHA-256",
5248
+ "content": "0a533e5d143a0e0ab3cc84c6218dfed3c8dfe3c86f3af25835f4d700b0fb1527"
5249
+ }
5250
+ ]
5251
+ },
5252
+ {
5253
+ "type": "file",
5254
+ "name": "dist/review-cost.js",
5255
+ "hashes": [
5256
+ {
5257
+ "alg": "SHA-256",
5258
+ "content": "5daa7df5632004fc6ccf4fd30425787c7ea7780ab1f2af183eafe4055d87fa36"
5259
+ }
5260
+ ]
5261
+ },
5262
+ {
5263
+ "type": "file",
5264
+ "name": "dist/review-cost.js.map",
5265
+ "hashes": [
5266
+ {
5267
+ "alg": "SHA-256",
5268
+ "content": "3c8abfadf727f2b4dfea0cdc58af3a7c73765a0636417e061fb1e010419fec6c"
5269
+ }
5270
+ ]
5271
+ },
5232
5272
  {
5233
5273
  "type": "file",
5234
5274
  "name": "dist/risk-scoring.d.ts",
@@ -5315,7 +5355,7 @@
5315
5355
  "hashes": [
5316
5356
  {
5317
5357
  "alg": "SHA-256",
5318
- "content": "c952216bc03cd2f8ad1d2b17ba5402b70bc8f1ec9d9557b25b3b064192ff56be"
5358
+ "content": "3f789196d023177040a7d0c4e2690186855714a8139b6d995b85022588bc6cbd"
5319
5359
  }
5320
5360
  ]
5321
5361
  },
@@ -5325,7 +5365,7 @@
5325
5365
  "hashes": [
5326
5366
  {
5327
5367
  "alg": "SHA-256",
5328
- "content": "dee616e546cfa366853c82707046b4cf2463f522632c121e584c1231efd299ef"
5368
+ "content": "93610d2de68db800c91478697a10864e3dd438d25b7b45aeeae62a3d5a5438f0"
5329
5369
  }
5330
5370
  ]
5331
5371
  },
@@ -5335,7 +5375,7 @@
5335
5375
  "hashes": [
5336
5376
  {
5337
5377
  "alg": "SHA-256",
5338
- "content": "6f746137311636982727ba5da3ae62f625cc19b2b78f1ebaa056017dbf91d6c8"
5378
+ "content": "c731c0fb46ba714cba4dc7b8129245de73c81d21a0e585053d16199ffd7b3011"
5339
5379
  }
5340
5380
  ]
5341
5381
  },
@@ -5345,7 +5385,7 @@
5345
5385
  "hashes": [
5346
5386
  {
5347
5387
  "alg": "SHA-256",
5348
- "content": "1d5c036f42c6d213b11a7441f3ab52186cd8e94a9aed9f5ce2b3ce2548c576ba"
5388
+ "content": "d4ba5ce8b11a36075d103d3be3d0785bd521ffc0fd5f0bfc1a0a9138410e730c"
5349
5389
  }
5350
5390
  ]
5351
5391
  },
@@ -5435,7 +5475,7 @@
5435
5475
  "hashes": [
5436
5476
  {
5437
5477
  "alg": "SHA-256",
5438
- "content": "ea6253a14d0eeb65ab671a989f44876088752a64e86fb7a2c6631f43aba552f4"
5478
+ "content": "4c139f0e13f547a8e03bcfbfb7530c9f0862c2b6d80d7d1f9d9fcd4dd4512c96"
5439
5479
  }
5440
5480
  ]
5441
5481
  },
@@ -5445,7 +5485,7 @@
5445
5485
  "hashes": [
5446
5486
  {
5447
5487
  "alg": "SHA-256",
5448
- "content": "72922e63f075128f46650dd7484db67571777dfc3a388e39efe2e7dad7ac007c"
5488
+ "content": "7085330bdcd668ab0a8e881ecb4f0737fe1e6cb360fa4fc58093e5e5b02ad6c4"
5449
5489
  }
5450
5490
  ]
5451
5491
  },
@@ -5455,7 +5495,7 @@
5455
5495
  "hashes": [
5456
5496
  {
5457
5497
  "alg": "SHA-256",
5458
- "content": "c4931905e808eb7cab306b5e3d350bd255b9a0633d6de2c8827e511b3cc91a29"
5498
+ "content": "edc206a0b721e8248945f24c2654efd6f425fa8daa4a3d8760575569117ec1ee"
5459
5499
  }
5460
5500
  ]
5461
5501
  },
@@ -5465,7 +5505,7 @@
5465
5505
  "hashes": [
5466
5506
  {
5467
5507
  "alg": "SHA-256",
5468
- "content": "b395c6d2d0f423a361ef0539a66ae84268537461877506739d9e3f58a6739a23"
5508
+ "content": "ccfa3e492154b92519af8cf40ab9af7f4710135020776775b8cdb3be208626a2"
5469
5509
  }
5470
5510
  ]
5471
5511
  },
@@ -7079,7 +7119,7 @@
7079
7119
  },
7080
7120
  {
7081
7121
  "name": "dz:canonical-json-sha256-v2",
7082
- "value": "9307673a743a014bc30e5897796a0dff724152f9325946aee2b4d35c05f91820"
7122
+ "value": "e9095406e69e4c15757b0489f742834880a940176c106aaeb7e9bab561dc87c0"
7083
7123
  }
7084
7124
  ]
7085
7125
  },
@@ -7509,7 +7549,7 @@
7509
7549
  "hashes": [
7510
7550
  {
7511
7551
  "alg": "SHA-256",
7512
- "content": "e1e447ab6cda0afb4518c73e482dda7bb27a6fe676e65999b8077da3b8c0a8b3"
7552
+ "content": "9236bcc66ddfa060fae3a7dfee4324f5a9a7c687aca109f6578d2d01bf34acb4"
7513
7553
  }
7514
7554
  ]
7515
7555
  },
@@ -7699,7 +7739,7 @@
7699
7739
  "hashes": [
7700
7740
  {
7701
7741
  "alg": "SHA-256",
7702
- "content": "89458ef7d48bfca28cab0cb47da3c97d65c3730b92005cae742f4414c27f84dc"
7742
+ "content": "8811f5256553c85d4f9fd5c817621467e3b492d8b3e1640e74c4b23b07320a16"
7703
7743
  }
7704
7744
  ]
7705
7745
  },
@@ -7789,7 +7829,7 @@
7789
7829
  "hashes": [
7790
7830
  {
7791
7831
  "alg": "SHA-256",
7792
- "content": "94826e7248002d6b3ae5cf1909018d0ee17a0eecb22c310dc0d94589ab74c5ad"
7832
+ "content": "54ec8ac2203964a7a409bf2987557c784137d9b0c9caf213db994b1b4784986a"
7793
7833
  }
7794
7834
  ]
7795
7835
  },
@@ -8229,7 +8269,7 @@
8229
8269
  "hashes": [
8230
8270
  {
8231
8271
  "alg": "SHA-256",
8232
- "content": "b8f881ddfa3c8c597b6c09d8e70f6548eb2318adf74b80d4942e6e665bb936cf"
8272
+ "content": "dc21e4af7023dbc15c19e951ed8b99228bcba8916cfe713cadc9694a10a8308b"
8233
8273
  }
8234
8274
  ]
8235
8275
  },
@@ -8393,6 +8433,16 @@
8393
8433
  }
8394
8434
  ]
8395
8435
  },
8436
+ {
8437
+ "type": "file",
8438
+ "name": "src/review-cost.ts",
8439
+ "hashes": [
8440
+ {
8441
+ "alg": "SHA-256",
8442
+ "content": "e4442e3732c494f700f3e651a6416c3c47136c3c1cb9c9ad86237326f5ae7894"
8443
+ }
8444
+ ]
8445
+ },
8396
8446
  {
8397
8447
  "type": "file",
8398
8448
  "name": "src/risk-scoring.ts",
@@ -8419,7 +8469,7 @@
8419
8469
  "hashes": [
8420
8470
  {
8421
8471
  "alg": "SHA-256",
8422
- "content": "ea21e8206f9a571a36a90f3219459466b94d0c5c3ea9c600979bcd07b015ed2d"
8472
+ "content": "33f933caad731cb20c95d04075214a308b024399b36259f6fb1bd2ce742ffef3"
8423
8473
  }
8424
8474
  ]
8425
8475
  },
@@ -8449,7 +8499,7 @@
8449
8499
  "hashes": [
8450
8500
  {
8451
8501
  "alg": "SHA-256",
8452
- "content": "73fe9ae5a262a30e2351df434e8e586090b660e4f9968e094c42f79ec169471a"
8502
+ "content": "f18c291059dcf8d8e103df61ef9a175b85763ec946a0fb9a26ed15613dc3f568"
8453
8503
  }
8454
8504
  ]
8455
8505
  },
@@ -579,10 +579,47 @@ export function buildControlRow(input: BuildControlRowInput): BuildControlRowRes
579
579
  };
580
580
  }
581
581
 
582
+ /**
583
+ * experiment-instrument FR-4/A6 (ADR-001): a `stage:'control'` row `dz control-review` writes when
584
+ * the run REFUSED before producing a diff — the failure-leaves-a-row half of the ADR's safety
585
+ * property. Distinguished from {@link ControlLedgerRow} by `outcome:'refused'`, which a successful
586
+ * row never carries; the two schemas share nothing else structurally on purpose — a refused run has
587
+ * no diff, no tree hashes, no grades to validate.
588
+ */
589
+ export interface ControlRefusedRow {
590
+ readonly slug: string;
591
+ readonly stage: 'control';
592
+ readonly outcome: 'refused';
593
+ /** Which half was responsible: the claude review, the codex review, or neither (a setup/tree/
594
+ * aggregation failure that belongs to neither half specifically). */
595
+ readonly half: 'claude' | 'codex' | 'setup';
596
+ readonly reason: string;
597
+ readonly runId: string;
598
+ /** Known only when the coder family was determined before the refusal — absent for the earliest
599
+ * failures (the claude half itself failing before it can report which family it reviewed). When
600
+ * present, the SAME `bucket(coderFamily, reviewerOfInterest)` a successful control row uses. */
601
+ readonly coderFamily?: 'codex' | 'claude';
602
+ readonly minutes: number | null;
603
+ }
604
+
605
+ function isValidControlRefusedRow(obj: Record<string, unknown>): boolean {
606
+ if (typeof obj['slug'] !== 'string' || obj['slug'].trim() === '') return false;
607
+ if (typeof obj['runId'] !== 'string' || obj['runId'].trim() === '') return false;
608
+ if (obj['half'] !== 'claude' && obj['half'] !== 'codex' && obj['half'] !== 'setup') return false;
609
+ if (typeof obj['reason'] !== 'string' || obj['reason'].trim() === '') return false;
610
+ if (!(obj['minutes'] === null || (typeof obj['minutes'] === 'number' && Number.isFinite(obj['minutes'])))) return false;
611
+ if (obj['coderFamily'] !== undefined && obj['coderFamily'] !== 'codex' && obj['coderFamily'] !== 'claude') return false;
612
+ return true;
613
+ }
614
+
582
615
  /* ── D5: reading the ledger back (FR-7; A8) ──────────────────────────────────────────────────── */
583
616
 
584
617
  export interface ParsedControlRows {
585
618
  readonly rows: readonly ControlLedgerRow[];
619
+ /** experiment-instrument FR-4/A6: `stage:'control'` rows with `outcome:'refused'` — a run that
620
+ * never produced a diff. Schema-validated (`isValidControlRefusedRow`) the same way `rows` is;
621
+ * one that fails validation is `unreadable`, same as a malformed successful row. */
622
+ readonly refusedRows: readonly ControlRefusedRow[];
586
623
  readonly roundRows: readonly Record<string, unknown>[];
587
624
  readonly fullRows: readonly Record<string, unknown>[];
588
625
  /** A8: a line that is not parseable JSON, or parses to something that is not a plain object, or
@@ -678,6 +715,7 @@ function isValidControlRow(obj: Record<string, unknown>): boolean {
678
715
  */
679
716
  export function parseControlRows(lines: readonly string[]): ParsedControlRows {
680
717
  const rows: ControlLedgerRow[] = [];
718
+ const refusedRows: ControlRefusedRow[] = [];
681
719
  const roundRows: Record<string, unknown>[] = [];
682
720
  const fullRows: Record<string, unknown>[] = [];
683
721
  let unreadable = 0;
@@ -698,6 +736,17 @@ export function parseControlRows(lines: readonly string[]): ParsedControlRows {
698
736
  const obj = parsed as Record<string, unknown>;
699
737
  const stage = obj['stage'];
700
738
  if (stage === 'control') {
739
+ // experiment-instrument FR-4/A6: `outcome:'refused'` is a DIFFERENT schema from a successful
740
+ // control row — checked FIRST, so a refused row is never mistaken for a malformed successful
741
+ // one (which would count it `unreadable`, losing exactly the receipt this feature adds).
742
+ if (obj['outcome'] === 'refused') {
743
+ if (!isValidControlRefusedRow(obj)) {
744
+ unreadable++;
745
+ continue;
746
+ }
747
+ refusedRows.push(obj as unknown as ControlRefusedRow);
748
+ continue;
749
+ }
701
750
  if (!isValidControlRow(obj)) {
702
751
  unreadable++;
703
752
  continue;
@@ -711,7 +760,7 @@ export function parseControlRows(lines: readonly string[]): ParsedControlRows {
711
760
  // Every other stage (plan/impl/fix/loop-run/round-exec/…) and the header/comment row (no
712
761
  // string `stage`) are readable, just not addressed by this module.
713
762
  }
714
- return { rows, roundRows, fullRows, unreadable };
763
+ return { rows, refusedRows, roundRows, fullRows, unreadable };
715
764
  }
716
765
 
717
766
  /* ── D6: the per-family-pair aggregate (FR-7; A6, A8) ────────────────────────────────────────── */
@@ -769,6 +818,10 @@ export interface FamilyPairAggregate {
769
818
  readonly refutedShare: { readonly n: number; readonly value: number | 'unknown' };
770
819
  readonly costPerConfirmed: { readonly n: number; readonly value: number | 'unknown' };
771
820
  readonly draftToShipped: ReadonlyArray<{ readonly slug: string; readonly first: string; readonly final: string; readonly finals: number }>;
821
+ /** experiment-instrument FR-4/A6: `stage:'control'` rows refused for THIS pair (attributed by a
822
+ * known `coderFamily` on the refused row) — a real cost line (the run was attempted and failed),
823
+ * never counted in `n` (which measures completed reviews). */
824
+ readonly refusedRuns: number;
772
825
  }
773
826
 
774
827
  export interface FamilyAggregate {
@@ -781,6 +834,11 @@ export interface FamilyAggregate {
781
834
  /** A6: the raw count of `stage:'control'` rows folded in — `0` means every `foreignUnique`
782
835
  * figure below is a true, honestly-printed absence, not a fabricated non-observation. */
783
836
  readonly controlRows: number;
837
+ /** experiment-instrument FR-4/A6: EVERY `stage:'control'` refused row seen, attributed or not —
838
+ * the total accounting figure `refusedRuns` (per pair) can never exceed, and the gap between the
839
+ * sum of per-pair `refusedRuns` and this total is exactly how many refusals had no determinable
840
+ * coderFamily (the earliest failures — printed here rather than silently dropped). */
841
+ readonly refusedControlRows: number;
784
842
  }
785
843
 
786
844
  interface MutablePair {
@@ -801,13 +859,14 @@ interface MutablePair {
801
859
  costN: number;
802
860
  costSum: number;
803
861
  drafts: Array<{ slug: string; first: string; final: string; finals: number }>;
862
+ refusedRuns: number;
804
863
  }
805
864
 
806
865
  function newPair(): MutablePair {
807
866
  return {
808
867
  n: 0, grades: {}, shipped: 0, shippedTotal: 0, fixRoundsList: [],
809
868
  foreignN: 0, foreignBySeverity: {}, foreignAuto: 0, foreignAdjudicated: 0, foreignAutoRuns: 0, foreignAdjudicatedRuns: 0, foreignIncomplete: 0,
810
- refutedN: 0, refutedSum: 0, costN: 0, costSum: 0, drafts: [],
869
+ refutedN: 0, refutedSum: 0, costN: 0, costSum: 0, drafts: [], refusedRuns: 0,
811
870
  };
812
871
  }
813
872
 
@@ -886,6 +945,18 @@ export function aggregateByFamily(
886
945
  else { b.foreignAutoRuns++; b.foreignAuto += foreignTotal; }
887
946
  }
888
947
 
948
+ // experiment-instrument FR-4/A6: a refused row is bucketed the SAME way a successful control row
949
+ // is — by its own coderFamily and the complementary reviewer — but ONLY when coderFamily is known
950
+ // (the earliest failures, before the claude half reports which family it reviewed, cannot be
951
+ // attributed to a pair; they still count toward `refusedControlRows` at the top level below,
952
+ // never silently dropped). `n` is deliberately untouched: `n` measures COMPLETED reviews.
953
+ for (const rr of parsed.refusedRows) {
954
+ if (rr.coderFamily === undefined) continue;
955
+ const reviewerOfInterest = rr.coderFamily === 'codex' ? 'claude' : 'codex';
956
+ const b = bucket(rr.coderFamily, reviewerOfInterest);
957
+ b.refusedRuns++;
958
+ }
959
+
889
960
  // draftToShipped: earliest known verdict per slug (a qe-bridge signoff, or this control row's
890
961
  // own claude-half grade when no signoff was given) against the slug's final grade — keyed by
891
962
  // slug PLUS the normalized (coder,reviewer) family pair (r1-14: two final rows for the same slug
@@ -952,9 +1023,16 @@ export function aggregateByFamily(
952
1023
  refutedShare: { n: b.refutedN, value: b.refutedN > 0 ? b.refutedSum / b.refutedN : 'unknown' },
953
1024
  costPerConfirmed: { n: b.costN, value: b.costN > 0 ? b.costSum / b.costN : 'unknown' },
954
1025
  draftToShipped: b.drafts,
1026
+ refusedRuns: b.refusedRuns,
955
1027
  };
956
1028
  }
957
1029
 
958
1030
  const incompleteControlRows = parsed.rows.filter((r) => !r.complete).length;
959
- return { pairs: out, incomplete: parsed.unreadable > 0 || incompleteControlRows > 0, incompleteControlRows, controlRows: parsed.rows.length };
1031
+ return {
1032
+ pairs: out,
1033
+ incomplete: parsed.unreadable > 0 || incompleteControlRows > 0,
1034
+ incompleteControlRows,
1035
+ controlRows: parsed.rows.length,
1036
+ refusedControlRows: parsed.refusedRows.length,
1037
+ };
960
1038
  }
@@ -2190,7 +2190,11 @@ export function gradeFromReviewFindings(findings: readonly CodexQeFinding[] | nu
2190
2190
  * The LOCKED decline taxonomy. A `kind` outside this set is a bug, not a new case — which is why
2191
2191
  * {@link codexQeDeclineReason} throws on one rather than rendering something plausible.
2192
2192
  */
2193
- export const CODEX_QE_DECLINE_KINDS: readonly string[] = ['timeout', 'no-verdict', 'tool-error', 'unusable-output', 'unavailable', 'over-ceiling', 'wrong-tree'];
2193
+ // 'scope-not-established' and 'base-ref-not-established' joined the set 2026-09-17 (feature
2194
+ // codex-review-scope + its fix-round-1, finding #7): the workflow's two inline mirrors grew both
2195
+ // kinds; this export copy diverged behind them until now. Appended, never reordered — the SAME
2196
+ // order the two workflow-twin mirrors carry.
2197
+ export const CODEX_QE_DECLINE_KINDS: readonly string[] = ['timeout', 'no-verdict', 'tool-error', 'unusable-output', 'unavailable', 'over-ceiling', 'wrong-tree', 'scope-not-established', 'base-ref-not-established'];
2194
2198
 
2195
2199
  export interface ClassifyCodexQeInput {
2196
2200
  readonly exit?: number | null;
@@ -2350,6 +2354,12 @@ export function codexQeDeclineReason(kind: string | null | undefined, detail?: C
2350
2354
  if (canonical === 'over-ceiling') {
2351
2355
  return 'prompt is ' + chars + ' chars / unscoped — refused before dispatch';
2352
2356
  }
2357
+ if (canonical === 'scope-not-established') {
2358
+ return 'review scope NOT ESTABLISHED for uncommitted QE — ' + (d.reason === undefined || d.reason === null || String(d.reason) === '' ? 'the change set could not be measured' : String(d.reason)) + ' — refusing to fall back to --uncommitted on the shared tree';
2359
+ }
2360
+ if (canonical === 'base-ref-not-established') {
2361
+ return 'base ref for the isolated review scope NOT ESTABLISHED — ' + (d.reason === undefined || d.reason === null || String(d.reason) === '' ? 'the base-ref probe failed or was unparseable' : String(d.reason)) + ' — refusing to silently fall back to HEAD';
2362
+ }
2353
2363
  throw new Error('codexQeDeclineReason: unknown kind ' + k);
2354
2364
  }
2355
2365
 
package/src/index.ts CHANGED
@@ -505,6 +505,7 @@ export {
505
505
  decideReadBack,
506
506
  recordVerdictLine,
507
507
  parseModelSpec,
508
+ applyTaskId,
508
509
  } from './run-records.js';
509
510
  export {
510
511
  ENVELOPE_SCHEMA,
@@ -527,7 +528,7 @@ export type {
527
528
  } from './feature-adr-envelope.js';
528
529
  export { decidePublishSigning, decidePostSigningVerification, decideSignableSet, publishSigningLine, signableSetLine } from './publish-signing.js';
529
530
  export type { PublishSigningVerdict, PublishSigningDecision, SignableSetDecision } from './publish-signing.js';
530
- export type { RecordKind, RecordVerdict, RecordDecision, LedgerEnrichInput, LedgerPriceEntry, ParsedModelSpec } from './run-records.js';
531
+ export type { RecordKind, RecordVerdict, RecordDecision, LedgerEnrichInput, LedgerPriceEntry, ParsedModelSpec, TaskIdLookup } from './run-records.js';
531
532
  export type { AmendmentAmbiguity, AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
532
533
  // contract-checklist (ADR-001): pure extraction, canonical rendering, typed report parsing, and
533
534
  // exact per-item verification. Filesystem discovery/containment stays in harness-cli.
@@ -1317,8 +1318,12 @@ export * from './run-registry.js';
1317
1318
 
1318
1319
  export { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from './journal.js';
1319
1320
  export type { JournalKind, JournalEvent, JournalLine, JournalIo } from './journal.js';
1320
- export { openRound, closeRound, listRounds, validateClosedRoundLedgerRow } from './round.js';
1321
+ export { openRound, closeRound, listRounds, validateClosedRoundLedgerRow, readOpenRoundTaskId } from './round.js';
1321
1322
  export type { RoundState, RoundExecState, RoundLedgerRow, RoundReviewSidecar } from './round.js';
1323
+
1324
+ // review-cost-ledger T1/FR-1 (ADR-001 п.1): the pure qe-bridge stdout cost parser.
1325
+ export { parseQeBridgeStdoutCost } from './review-cost.js';
1326
+ export type { QeBridgeCost, QeBridgeCostTokens } from './review-cost.js';
1322
1327
  export { parseCodexTokens, classifyRoundExecOutcome, buildRoundExecRow } from './round-exec.js';
1323
1328
  export type { RoundExecOutcome, RoundExecLedgerRow } from './round-exec.js';
1324
1329
  export * from './run-cleanup.js';
package/src/qe-bridge.ts CHANGED
@@ -151,6 +151,14 @@ export interface BridgeSignoff {
151
151
  promptSha256: string;
152
152
  elapsedMs: number;
153
153
  emittedAt: string;
154
+ /** experiment-instrument FR-1/FR-3/T5 (ADR-001): the task identity of the slug's single open
155
+ * round at the moment this signoff was emitted — filled by the cli (it owns `.dz/rounds/`), null
156
+ * when there was no open round to fill from, never guessed. Optional so a signoff written before
157
+ * this feature landed parses unchanged (NFR-1). */
158
+ taskId?: string | null;
159
+ /** experiment-instrument FR-1/FR-3/T5: present whenever `taskId` above was filled from a lookup —
160
+ * names where it came from (`readOpenRoundTaskId`'s own source, r1-1/r1-2 extended to five values). */
161
+ taskIdSource?: 'open-round' | 'derived-legacy' | 'no-open-round' | 'ambiguous' | 'unavailable';
154
162
  }
155
163
 
156
164
  /** Where each channel was found, for the audit bundle: an auditor can re-derive the verdict from