@dzhechkov/harness-core 0.5.1 → 0.5.2

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
@@ -15,7 +15,7 @@
15
15
  "hashes": [
16
16
  {
17
17
  "alg": "SHA-256",
18
- "content": "6aa58226ed2a38314929b9c93f1251932e9713c276bedb1006885757a0b0d59c"
18
+ "content": "1790811c8031105b1f281ecaff8a2e5b66a33ac9a323cbe63fb0caae67835b0d"
19
19
  }
20
20
  ]
21
21
  },
@@ -1525,7 +1525,7 @@
1525
1525
  "hashes": [
1526
1526
  {
1527
1527
  "alg": "SHA-256",
1528
- "content": "b5cc2034f02ea6c33a4e5a3c7009c5ba8357a3db35e6ee8e626ad5043b456d44"
1528
+ "content": "837b65bceef392d58a8b85257443e796c8da7219e2c7d5749df952ff4ba9a7d9"
1529
1529
  }
1530
1530
  ]
1531
1531
  },
@@ -1535,7 +1535,7 @@
1535
1535
  "hashes": [
1536
1536
  {
1537
1537
  "alg": "SHA-256",
1538
- "content": "4ab8fac26790b7cfc5b87ce715cb9ded2cce5fe16e1791ad255c421adb67dba7"
1538
+ "content": "2c88a22a96119c8ed14d67c1830bd85857e0c43f8a730951c1279ec9442decd6"
1539
1539
  }
1540
1540
  ]
1541
1541
  },
@@ -1545,7 +1545,7 @@
1545
1545
  "hashes": [
1546
1546
  {
1547
1547
  "alg": "SHA-256",
1548
- "content": "b67b9d7ea154657908e3f6c3a42c7156c34c769f0ba67dc462e8833cd1cd0f2c"
1548
+ "content": "5c208320312a34f0e4d09b2cea3d50a057253771a67b60a0ee0d0907a382546c"
1549
1549
  }
1550
1550
  ]
1551
1551
  },
@@ -1555,7 +1555,7 @@
1555
1555
  "hashes": [
1556
1556
  {
1557
1557
  "alg": "SHA-256",
1558
- "content": "55ef43a95452bd6e288f41a856283fc296c27a5ac489aa81832ff62605d25730"
1558
+ "content": "262dd0fca9a734cc81ba33c215adb9ef88b343ee56091c017abda45c68bc2508"
1559
1559
  }
1560
1560
  ]
1561
1561
  },
@@ -1625,7 +1625,7 @@
1625
1625
  "hashes": [
1626
1626
  {
1627
1627
  "alg": "SHA-256",
1628
- "content": "64c2189b94ec500649defd76ced51742807ebf7dba6ec3f4d68cc0ada6f86c6b"
1628
+ "content": "115740b3c04b6e906e135eb9d1e462ae4197cdba558d4eb620e29e541b4de288"
1629
1629
  }
1630
1630
  ]
1631
1631
  },
@@ -1635,7 +1635,7 @@
1635
1635
  "hashes": [
1636
1636
  {
1637
1637
  "alg": "SHA-256",
1638
- "content": "8fe4553cb49e6440c3c2453e9e5a0aa0827829e250670afa33ba1cd9e7ccf0e0"
1638
+ "content": "2cbadd2514704ccf6cfbf7e44fc50cad3d3701bc91b98651015896ebe209449a"
1639
1639
  }
1640
1640
  ]
1641
1641
  },
@@ -1785,7 +1785,7 @@
1785
1785
  "hashes": [
1786
1786
  {
1787
1787
  "alg": "SHA-256",
1788
- "content": "7fe2978192efae0c77f1b5da355fef75d035a2dd847b0111ae8f4e11c57af082"
1788
+ "content": "49b8fc106dee22095dc02c43e224de52b9026dbf77d2f2e2654219a776017a09"
1789
1789
  }
1790
1790
  ]
1791
1791
  },
@@ -1795,7 +1795,7 @@
1795
1795
  "hashes": [
1796
1796
  {
1797
1797
  "alg": "SHA-256",
1798
- "content": "79b8f649aab8bb4b7614cd35b24d4c102ca566ea1b7eb3b06fa35e4079b0becc"
1798
+ "content": "170a7cdafb569ce8d9d1bd7c63ab83d4689cd3b1ed8a1f02536f5affd9dd6d5f"
1799
1799
  }
1800
1800
  ]
1801
1801
  },
@@ -1845,7 +1845,7 @@
1845
1845
  "hashes": [
1846
1846
  {
1847
1847
  "alg": "SHA-256",
1848
- "content": "b9eaa43bfc3cd4c80274c27410abd307d85c57d1952ca4563b93bbf87fa81de7"
1848
+ "content": "bd228e00e217a99ee7f192c9e87d4e1c794c8ffb52323b77b87ee93f601f013a"
1849
1849
  }
1850
1850
  ]
1851
1851
  },
@@ -1855,7 +1855,7 @@
1855
1855
  "hashes": [
1856
1856
  {
1857
1857
  "alg": "SHA-256",
1858
- "content": "df095cb1ae2aa32df0286c29da55aae3fa5bd277f8242a2b3fb14d3e3069585a"
1858
+ "content": "4c37b21c750133def0498c6d1b0fddfb2525500ced4dc58634ae1234425337d3"
1859
1859
  }
1860
1860
  ]
1861
1861
  },
@@ -1865,7 +1865,7 @@
1865
1865
  "hashes": [
1866
1866
  {
1867
1867
  "alg": "SHA-256",
1868
- "content": "96345388c0efa6f05388fed69ceb54e17e9edac9124cf06745e8a5cd3bc486e4"
1868
+ "content": "83f2afea542e80bf09f9ccf9a56a9d352111a8a3cd7744bf8f2ccad84d796c6a"
1869
1869
  }
1870
1870
  ]
1871
1871
  },
@@ -1875,7 +1875,7 @@
1875
1875
  "hashes": [
1876
1876
  {
1877
1877
  "alg": "SHA-256",
1878
- "content": "6ce9f923b87ee9ae53c3a9a758609bde70decf039440e863ced8b407fc574366"
1878
+ "content": "93a3007d6a7a906344f8f85272aef1928811997d331202171558763ab917b9d6"
1879
1879
  }
1880
1880
  ]
1881
1881
  },
@@ -3599,6 +3599,46 @@
3599
3599
  }
3600
3600
  ]
3601
3601
  },
3602
+ {
3603
+ "type": "file",
3604
+ "name": "dist/trace-corroborate.d.ts",
3605
+ "hashes": [
3606
+ {
3607
+ "alg": "SHA-256",
3608
+ "content": "ebc36a47e4f8359eb7359a1e1697e9a135b2a281ecb1f795e99da7f9476f7f4c"
3609
+ }
3610
+ ]
3611
+ },
3612
+ {
3613
+ "type": "file",
3614
+ "name": "dist/trace-corroborate.d.ts.map",
3615
+ "hashes": [
3616
+ {
3617
+ "alg": "SHA-256",
3618
+ "content": "7a2f343525b6585d0cd54644282bc49b741baffd066ebff0f023c036b622e6f8"
3619
+ }
3620
+ ]
3621
+ },
3622
+ {
3623
+ "type": "file",
3624
+ "name": "dist/trace-corroborate.js",
3625
+ "hashes": [
3626
+ {
3627
+ "alg": "SHA-256",
3628
+ "content": "1b78df722776f6e253e3d0babb90c8557b5d733528125c793b7519ec724db96b"
3629
+ }
3630
+ ]
3631
+ },
3632
+ {
3633
+ "type": "file",
3634
+ "name": "dist/trace-corroborate.js.map",
3635
+ "hashes": [
3636
+ {
3637
+ "alg": "SHA-256",
3638
+ "content": "7c95e98228f86fda4f4503b04efaf149466f9bd78fbb549a8aad49c8a4f3b7ab"
3639
+ }
3640
+ ]
3641
+ },
3602
3642
  {
3603
3643
  "type": "file",
3604
3644
  "name": "dist/upgrade.d.ts",
@@ -3765,7 +3805,7 @@
3765
3805
  "hashes": [
3766
3806
  {
3767
3807
  "alg": "SHA-256",
3768
- "content": "9ab363920599845c6d7b895d7c79217c8eb547752f5a440c14841cec41d8d03a"
3808
+ "content": "94d154db3feb9676e515fc04129ac299f967ff46b8a7daedc51ec201f7eb5259"
3769
3809
  }
3770
3810
  ]
3771
3811
  },
@@ -3775,7 +3815,7 @@
3775
3815
  "hashes": [
3776
3816
  {
3777
3817
  "alg": "SHA-256",
3778
- "content": "998e470015beb24d24b0510e435d327786295c8c735bc145f9b7ec068f243e63"
3818
+ "content": "0bda62d918a427203c946a0d6ec9fdb67a2223573939bc358fa9d48545e34361"
3779
3819
  }
3780
3820
  ]
3781
3821
  },
@@ -3785,7 +3825,7 @@
3785
3825
  "hashes": [
3786
3826
  {
3787
3827
  "alg": "SHA-256",
3788
- "content": "7877d5dcda3394aae5349a8b935d6cae67b0ba822e9a155d69f3476bca129ab4"
3828
+ "content": "c5d24bc9e5a8731ecd9e83e0fc3c3bac02421b76028906b20d2a113f04efbe59"
3789
3829
  }
3790
3830
  ]
3791
3831
  },
@@ -3795,7 +3835,7 @@
3795
3835
  "hashes": [
3796
3836
  {
3797
3837
  "alg": "SHA-256",
3798
- "content": "5832275f40d831d58f18f0dd99434157ccb98f56ad689b4d10a373cbbd4f177d"
3838
+ "content": "f9a9a320dde4bae0d6f6b0b2e7c03c08127e452f7fa7e00d78ea9f9b816bc6e7"
3799
3839
  }
3800
3840
  ]
3801
3841
  },
@@ -3845,7 +3885,7 @@
3845
3885
  "hashes": [
3846
3886
  {
3847
3887
  "alg": "SHA-256",
3848
- "content": "317e79f0d14348171eee5ca0d0666ecfb98a4ad05bd438f6c9478686fcadad96"
3888
+ "content": "f44d3dd18b5332681d861707c5644b1cbb027b217b9fda07a0b8510b6346d1a8"
3849
3889
  }
3850
3890
  ]
3851
3891
  },
@@ -4225,7 +4265,7 @@
4225
4265
  "hashes": [
4226
4266
  {
4227
4267
  "alg": "SHA-256",
4228
- "content": "6e6ebced34cfe7be2d69ccd257ca4382d312910c75164a6eb2248f6ced652a7a"
4268
+ "content": "b741c4477d04f5fdb88f88601b8528f56e9b1fdc6a849dece4af157a81c0fe57"
4229
4269
  }
4230
4270
  ]
4231
4271
  },
@@ -4245,7 +4285,7 @@
4245
4285
  "hashes": [
4246
4286
  {
4247
4287
  "alg": "SHA-256",
4248
- "content": "21ca2d73e0833cf70120a06d970b68e7c7e9ba39698b93c390d89b7d5655852c"
4288
+ "content": "929be36a3c9fd2cf3ba6e6b7328dd8d7928d9c031c38bb62880ba234c9553334"
4249
4289
  }
4250
4290
  ]
4251
4291
  },
@@ -4285,7 +4325,7 @@
4285
4325
  "hashes": [
4286
4326
  {
4287
4327
  "alg": "SHA-256",
4288
- "content": "f6cd7fbc071b59804cd0a7f2ea37e2ebd80fe347806b338f618e11508bcf8301"
4328
+ "content": "69d0daac70fda89235352be2cc2e40bcfe62651aedab8056c1919ed062d4a75b"
4289
4329
  }
4290
4330
  ]
4291
4331
  },
@@ -4305,7 +4345,7 @@
4305
4345
  "hashes": [
4306
4346
  {
4307
4347
  "alg": "SHA-256",
4308
- "content": "26b22b86c80307158d78e587d27b9a09cd597f5a491c8046c3aebb8dabd6c654"
4348
+ "content": "ea7d5b64c7805fd4622e7a892e0e3f1f03e4fdbb883f5fe377fcd64330775330"
4309
4349
  }
4310
4350
  ]
4311
4351
  },
@@ -4729,6 +4769,16 @@
4729
4769
  }
4730
4770
  ]
4731
4771
  },
4772
+ {
4773
+ "type": "file",
4774
+ "name": "src/trace-corroborate.ts",
4775
+ "hashes": [
4776
+ {
4777
+ "alg": "SHA-256",
4778
+ "content": "28a872378dce68940237a99e8ffe57d3da049cccb0ffc315fbddf8ba65e80ee8"
4779
+ }
4780
+ ]
4781
+ },
4732
4782
  {
4733
4783
  "type": "file",
4734
4784
  "name": "src/upgrade.ts",
@@ -4775,7 +4825,7 @@
4775
4825
  "hashes": [
4776
4826
  {
4777
4827
  "alg": "SHA-256",
4778
- "content": "36a7e633dbd7bde8a15f8e0a426bda7c308d67194a21094621654a7022b99c87"
4828
+ "content": "f1aaf39fcb5088afe3c63185c05a0d22f0507d29c342e3024836f46e3996d430"
4779
4829
  }
4780
4830
  ]
4781
4831
  },
@@ -5339,6 +5389,26 @@
5339
5389
  }
5340
5390
  ]
5341
5391
  },
5392
+ {
5393
+ "type": "file",
5394
+ "name": "test/feature-adr-workflow-write-discipline.test.ts",
5395
+ "hashes": [
5396
+ {
5397
+ "alg": "SHA-256",
5398
+ "content": "f491c12d9bb0a361095e9ec7cab216b60ea51f1c839256b8b4476bf0f8d5f4ca"
5399
+ }
5400
+ ]
5401
+ },
5402
+ {
5403
+ "type": "file",
5404
+ "name": "test/feature-adr-write-discipline.test.ts",
5405
+ "hashes": [
5406
+ {
5407
+ "alg": "SHA-256",
5408
+ "content": "e44795d46da74d4f21bf6817d3884f1428f8adfd814c5174490c9c372299932e"
5409
+ }
5410
+ ]
5411
+ },
5342
5412
  {
5343
5413
  "type": "file",
5344
5414
  "name": "test/files-whitelist-guard.test.ts",
@@ -5435,7 +5505,7 @@
5435
5505
  "hashes": [
5436
5506
  {
5437
5507
  "alg": "SHA-256",
5438
- "content": "0a8d4ea5c160aa4e08e39a439907948dfeb92b622e42bf4bdd578c9a05848c45"
5508
+ "content": "f20a7850a3313d5bb2431d81e8717694f2edf4cda5c67133c0b80d7549e2cbab"
5439
5509
  }
5440
5510
  ]
5441
5511
  },
@@ -5445,7 +5515,7 @@
5445
5515
  "hashes": [
5446
5516
  {
5447
5517
  "alg": "SHA-256",
5448
- "content": "842d2425c72b12356512ebcfe66fe7f878f58daee947bbdd8ef1f2c908c748a7"
5518
+ "content": "2bc8d4bd9c2bf15a3522d00b01a18611ef973f4b9e71ef5c1894ea72628294a8"
5449
5519
  }
5450
5520
  ]
5451
5521
  },
@@ -5655,7 +5725,7 @@
5655
5725
  "hashes": [
5656
5726
  {
5657
5727
  "alg": "SHA-256",
5658
- "content": "f4b88439e1319490d6bcff87f507d8f01a8ab11469057a7060693df70dbe8802"
5728
+ "content": "95d04dbdb46e7f1fac3f6cfd5ec7705579a436ea7118823e11cb752da7f9adea"
5659
5729
  }
5660
5730
  ]
5661
5731
  },
@@ -5855,7 +5925,7 @@
5855
5925
  "hashes": [
5856
5926
  {
5857
5927
  "alg": "SHA-256",
5858
- "content": "a0f9cf5a77de387993d9c1860458469ab3015c26e6f7242aa170802ae0a85fc4"
5928
+ "content": "e9ccae5e13df6370a8564326e5766aa1e03973c595bfdf73765d54191acf98c4"
5859
5929
  }
5860
5930
  ]
5861
5931
  },
@@ -6209,6 +6279,16 @@
6209
6279
  }
6210
6280
  ]
6211
6281
  },
6282
+ {
6283
+ "type": "file",
6284
+ "name": "test/trace-attestation.test.ts",
6285
+ "hashes": [
6286
+ {
6287
+ "alg": "SHA-256",
6288
+ "content": "f4888117c97180797e6437abc1ef0c6aa1f1e48ee7c2df33d60a65e8bf11b35c"
6289
+ }
6290
+ ]
6291
+ },
6212
6292
  {
6213
6293
  "type": "file",
6214
6294
  "name": "test/trace-bundle.test.ts",
@@ -6219,6 +6299,26 @@
6219
6299
  }
6220
6300
  ]
6221
6301
  },
6302
+ {
6303
+ "type": "file",
6304
+ "name": "test/trace-corroborate.test.ts",
6305
+ "hashes": [
6306
+ {
6307
+ "alg": "SHA-256",
6308
+ "content": "9e845bb448c1300966e76a030b2b0d88c3aa3cf666d85b58ddd981e15d4dcd85"
6309
+ }
6310
+ ]
6311
+ },
6312
+ {
6313
+ "type": "file",
6314
+ "name": "test/trace-qe-round1.test.ts",
6315
+ "hashes": [
6316
+ {
6317
+ "alg": "SHA-256",
6318
+ "content": "7790c41628d7223a45aa61977511d1ddd6cd2cd8dc2dfdcdd248bb25c2effeca"
6319
+ }
6320
+ ]
6321
+ },
6222
6322
  {
6223
6323
  "type": "file",
6224
6324
  "name": "test/trusttier-schema.test.ts",
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export {
65
65
  type LintOptions,
66
66
  } from './loop-lint.js';
67
67
  export * from './loop-trace.js';
68
+ export * from './trace-corroborate.js';
68
69
  export * from './trace-bundle.js';
69
70
  export { BLOBS as LOOP_BLOBS, LOOP_BLOB_NAMES, BLOB_COVERAGE_MANIFEST } from './loop-blobs.generated.js';
70
71
  export type { LoopBlob } from './loop-blobs.generated.js';
@@ -97,11 +97,11 @@ export const BLOBS: Record<string, LoopBlob> = {
97
97
  "trace": {
98
98
  name: "trace",
99
99
  version: "1.1.0",
100
- contentHash: "fc59098bb0deb747c4c1c9a6ba843df70edfe0dc5ef8370c0f7192afc7657890",
100
+ contentHash: "1270f9aac5fbe5693af8bcc26af55e81aeb41ef4b911dbda72c23ea248072344",
101
101
  sourcePath: "packages/@dzhechkov/harness-core/src/loop-trace.ts",
102
102
  requires: [],
103
103
  exports: ["LOOP_TRACE_SCHEMA_VERSION","TRACE_RUNID_RE","TRACE_KEY_RE","traceShellQuote","traceValidateEvent","traceInit","traceOnDispatch","traceOnSettle","traceClose","traceFlushCmd","traceFaRecordCmd","traceLedgerLine","traceLedgerAppendCmd"],
104
- code: "const LOOP_TRACE_SCHEMA_VERSION = 1;\nconst TRACE_RUNID_RE = /^[a-z0-9-]{1,40}$/;\nconst TRACE_KEY_RE = /^[a-z0-9_.:-]{1,64}$/i;\nfunction traceShellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction traceValidateEvent(e) {\n if (typeof e !== 'object' || e === null || Array.isArray(e))\n return 'event must be an object';\n const ev = e;\n if (ev['v'] !== 1)\n return 'v must be 1';\n if (typeof ev['runId'] !== 'string' || !TRACE_RUNID_RE.test(ev['runId']))\n return 'runId fails its VO regex';\n if (typeof ev['seq'] !== 'number' || !Number.isInteger(ev['seq']) || ev['seq'] < 1)\n return 'seq must be a positive integer';\n const kind = ev['event'];\n if (kind === 'dispatched') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (typeof ev['stepId'] !== 'string' || !TRACE_KEY_RE.test(ev['stepId']))\n return 'stepId fails its VO regex';\n if (ev['itemKey'] !== null && (typeof ev['itemKey'] !== 'string' || !TRACE_KEY_RE.test(ev['itemKey'])))\n return 'itemKey fails its VO regex';\n if (typeof ev['attempt'] !== 'number' || ev['attempt'] < 1)\n return 'attempt must be >= 1';\n if (typeof ev['phase'] !== 'string' || ev['phase'] === '')\n return 'phase required';\n if (!Array.isArray(ev['causedBy']) || ev['causedBy'].some((n) => typeof n !== 'number'))\n return 'causedBy must be a number array';\n return null;\n }\n if (kind === 'settled') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (ev['outcome'] !== 'ok' && ev['outcome'] !== 'null' && ev['outcome'] !== 'error')\n return 'outcome must be ok|null|error';\n return null;\n }\n if (kind === 'run.opened') {\n if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string')\n return 'run.opened needs planDigest + execFp';\n return null;\n }\n if (kind === 'run.closed') {\n const c = ev['counts'];\n if (typeof c !== 'object' || c === null)\n return 'run.closed needs counts';\n return null;\n }\n return 'unknown event kind';\n}\nfunction traceInit(runId, planDigest, execFp) {\n if (!TRACE_RUNID_RE.test(runId))\n throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));\n const state = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };\n const opened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp };\n traceBuffer(state, opened);\n return state;\n}\nfunction traceBuffer(state, e) {\n const err = traceValidateEvent(e);\n if (err !== null)\n throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');\n state.buffer.push(JSON.stringify(e));\n}\nfunction traceOnDispatch(state, e) {\n const seq = ++state.seq;\n state.dispatched++;\n traceBuffer(state, {\n v: 1,\n runId: state.runId,\n seq,\n event: 'dispatched',\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n attempt: e.attempt,\n phase: e.phase,\n model: e.model,\n causedBy: e.causedBy,\n });\n return seq;\n}\nfunction traceOnSettle(state, e) {\n const seq = ++state.seq;\n state.settled++;\n traceBuffer(state, { v: 1, runId: state.runId, seq, event: 'settled', invocationId: e.invocationId, outcome: e.outcome });\n return seq;\n}\nfunction traceClose(state) {\n const closed = {\n v: 1,\n runId: state.runId,\n seq: ++state.seq,\n event: 'run.closed',\n counts: { dispatched: state.dispatched, settled: state.settled },\n };\n traceBuffer(state, closed);\n}\nfunction traceFlushCmd(state, traceFileAbs) {\n if (state.buffer.length === 0)\n return null;\n const lines = state.buffer.splice(0, state.buffer.length);\n const file = traceShellQuote(traceFileAbs);\n const dir = traceShellQuote(traceFileAbs.replace(/\\/[^/]*$/, ''));\n const printfs = lines\n .map((l) => \"printf '%s\\\\n' \" + traceShellQuote(l) + ' | sed \"s/}$/,\\\\\"wallTime\\\\\":\\\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\\\"}/\" >> ' + file)\n .join(' && ');\n return 'mkdir -p ' + dir + ' && ' + printfs;\n}\nfunction traceFaRecordCmd(dzBin, slug, stepLabel, projectAbs) {\n if (typeof slug !== 'string' || slug === ''\n || typeof stepLabel !== 'string' || stepLabel === ''\n || typeof projectAbs !== 'string' || projectAbs === '')\n return null;\n const bin = typeof dzBin === 'string' && dzBin !== '' ? dzBin : 'dz';\n const cmd = traceShellQuote(bin) + ' statusline --fa-record --slug ' + traceShellQuote(slug)\n + ' --step ' + traceShellQuote(stepLabel) + ' --kind loop --project ' + traceShellQuote(projectAbs);\n return cmd + ' >/dev/null 2>&1';\n}\nfunction traceLedgerLine(opts) {\n try {\n if (typeof opts.slug !== 'string' || opts.slug === '')\n return null;\n const agents = typeof opts.agents === 'number'\n && Number.isFinite(opts.agents)\n && Number.isInteger(opts.agents)\n && opts.agents >= 0\n ? opts.agents\n : 0;\n const date = typeof opts.date === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(opts.date) ? opts.date : null;\n const outcome = typeof opts.outcome === 'string' && opts.outcome !== '' ? opts.outcome : 'unknown';\n const line = JSON.stringify({\n slug: opts.slug,\n stage: 'loop-run',\n tier: null,\n tokens: null,\n minutes: null,\n agents,\n coder: null,\n grade: null,\n date,\n auto: true,\n outcome,\n runId: typeof opts.runId === 'string' ? opts.runId : null,\n planDigest: typeof opts.planDigest === 'string' ? opts.planDigest : null,\n });\n return line.length <= 4000 ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction traceLedgerAppendCmd(repoAbs, line) {\n if (typeof repoAbs !== 'string' || repoAbs === '' || typeof line !== 'string' || line === '')\n return null;\n const dir = traceShellQuote(repoAbs + '/.dz/feature-adr');\n const file = traceShellQuote(repoAbs + '/.dz/feature-adr/run-cost-ledger.jsonl');\n return 'mkdir -p ' + dir\n + \" && printf '%s' \" + traceShellQuote(line)\n + ' | sed \"s/\\\\\"date\\\\\":null/\\\\\"date\\\\\":\\\\\"$(date -u +%Y-%m-%d)\\\\\"/\" >> ' + file\n + \" && printf '\\\\n' >> \" + file\n + ' && echo LEDGER-OK';\n}\nfunction invocations(run) {\n const out = new Map();\n for (const e of run.events) {\n if (e.event === 'dispatched') {\n out.set(e.invocationId, {\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n dispatchSeq: e.seq,\n settleSeq: null,\n causedBy: e.causedBy,\n });\n }\n else if (e.event === 'settled') {\n const inv = out.get(e.invocationId);\n if (inv)\n inv.settleSeq = e.seq;\n }\n }\n return [...out.values()];\n}",
104
+ code: "const LOOP_TRACE_SCHEMA_VERSION = 1;\nconst TRACE_RUNID_RE = /^[a-z0-9-]{1,40}$/;\nconst TRACE_KEY_RE = /^[a-z0-9_.:-]{1,64}$/i;\nfunction traceShellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction traceValidateEvent(e) {\n if (typeof e !== 'object' || e === null || Array.isArray(e))\n return 'event must be an object';\n const ev = e;\n if (ev['v'] !== 1)\n return 'v must be 1';\n if (typeof ev['runId'] !== 'string' || !TRACE_RUNID_RE.test(ev['runId']))\n return 'runId fails its VO regex';\n if (typeof ev['seq'] !== 'number' || !Number.isInteger(ev['seq']) || ev['seq'] < 1)\n return 'seq must be a positive integer';\n const kind = ev['event'];\n if (kind === 'dispatched') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (typeof ev['stepId'] !== 'string' || !TRACE_KEY_RE.test(ev['stepId']))\n return 'stepId fails its VO regex';\n if (ev['itemKey'] !== null && (typeof ev['itemKey'] !== 'string' || !TRACE_KEY_RE.test(ev['itemKey'])))\n return 'itemKey fails its VO regex';\n if (typeof ev['attempt'] !== 'number' || ev['attempt'] < 1)\n return 'attempt must be >= 1';\n if (typeof ev['phase'] !== 'string' || ev['phase'] === '')\n return 'phase required';\n if (!Array.isArray(ev['causedBy']) || ev['causedBy'].some((n) => typeof n !== 'number'))\n return 'causedBy must be a number array';\n return null;\n }\n if (kind === 'settled') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (ev['outcome'] !== 'ok' && ev['outcome'] !== 'null' && ev['outcome'] !== 'error')\n return 'outcome must be ok|null|error';\n return null;\n }\n if (kind === 'run.opened') {\n if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string')\n return 'run.opened needs planDigest + execFp';\n const ep = ev['emitterPath'];\n if (ep !== undefined && ep !== 'dz-process' && ep !== 'rendered-script')\n return 'emitterPath must be dz-process|rendered-script';\n return null;\n }\n if (kind === 'run.closed') {\n const c = ev['counts'];\n if (typeof c !== 'object' || c === null)\n return 'run.closed needs counts';\n return null;\n }\n return 'unknown event kind';\n}\nfunction traceInit(runId, planDigest, execFp, emitterPath) {\n if (!TRACE_RUNID_RE.test(runId))\n throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));\n const state = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };\n const opened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp, emitterPath };\n traceBuffer(state, opened);\n return state;\n}\nfunction traceBuffer(state, e) {\n const err = traceValidateEvent(e);\n if (err !== null)\n throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');\n state.buffer.push(JSON.stringify(e));\n}\nfunction traceOnDispatch(state, e) {\n const seq = ++state.seq;\n state.dispatched++;\n traceBuffer(state, {\n v: 1,\n runId: state.runId,\n seq,\n event: 'dispatched',\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n attempt: e.attempt,\n phase: e.phase,\n model: e.model,\n causedBy: e.causedBy,\n });\n return seq;\n}\nfunction traceOnSettle(state, e) {\n const seq = ++state.seq;\n state.settled++;\n traceBuffer(state, { v: 1, runId: state.runId, seq, event: 'settled', invocationId: e.invocationId, outcome: e.outcome });\n return seq;\n}\nfunction traceClose(state) {\n const closed = {\n v: 1,\n runId: state.runId,\n seq: ++state.seq,\n event: 'run.closed',\n counts: { dispatched: state.dispatched, settled: state.settled },\n };\n traceBuffer(state, closed);\n}\nfunction traceFlushCmd(state, traceFileAbs) {\n if (state.buffer.length === 0)\n return null;\n const lines = state.buffer.splice(0, state.buffer.length);\n const file = traceShellQuote(traceFileAbs);\n const dir = traceShellQuote(traceFileAbs.replace(/\\/[^/]*$/, ''));\n const printfs = lines\n .map((l) => \"printf '%s\\\\n' \" + traceShellQuote(l) + ' | sed \"s/}$/,\\\\\"wallTime\\\\\":\\\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\\\"}/\" >> ' + file)\n .join(' && ');\n return 'mkdir -p ' + dir + ' && ' + printfs;\n}\nfunction traceFaRecordCmd(dzBin, slug, stepLabel, projectAbs) {\n if (typeof slug !== 'string' || slug === ''\n || typeof stepLabel !== 'string' || stepLabel === ''\n || typeof projectAbs !== 'string' || projectAbs === '')\n return null;\n const bin = typeof dzBin === 'string' && dzBin !== '' ? dzBin : 'dz';\n const cmd = traceShellQuote(bin) + ' statusline --fa-record --slug ' + traceShellQuote(slug)\n + ' --step ' + traceShellQuote(stepLabel) + ' --kind loop --project ' + traceShellQuote(projectAbs);\n return cmd + ' >/dev/null 2>&1';\n}\nfunction traceLedgerLine(opts) {\n try {\n if (typeof opts.slug !== 'string' || opts.slug === '')\n return null;\n const agents = typeof opts.agents === 'number'\n && Number.isFinite(opts.agents)\n && Number.isInteger(opts.agents)\n && opts.agents >= 0\n ? opts.agents\n : 0;\n const date = typeof opts.date === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(opts.date) ? opts.date : null;\n const outcome = typeof opts.outcome === 'string' && opts.outcome !== '' ? opts.outcome : 'unknown';\n const line = JSON.stringify({\n slug: opts.slug,\n stage: 'loop-run',\n tier: null,\n tokens: null,\n minutes: null,\n agents,\n coder: null,\n grade: null,\n date,\n auto: true,\n outcome,\n runId: typeof opts.runId === 'string' ? opts.runId : null,\n planDigest: typeof opts.planDigest === 'string' ? opts.planDigest : null,\n });\n return line.length <= 4000 ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction traceLedgerAppendCmd(repoAbs, line) {\n if (typeof repoAbs !== 'string' || repoAbs === '' || typeof line !== 'string' || line === '')\n return null;\n const dir = traceShellQuote(repoAbs + '/.dz/feature-adr');\n const file = traceShellQuote(repoAbs + '/.dz/feature-adr/run-cost-ledger.jsonl');\n return 'mkdir -p ' + dir\n + \" && printf '%s' \" + traceShellQuote(line)\n + ' | sed \"s/\\\\\"date\\\\\":null/\\\\\"date\\\\\":\\\\\"$(date -u +%Y-%m-%d)\\\\\"/\" >> ' + file\n + \" && printf '\\\\n' >> \" + file\n + ' && echo LEDGER-OK';\n}\nfunction invocations(run) {\n const out = new Map();\n for (const e of run.events) {\n if (e.event === 'dispatched') {\n out.set(e.invocationId, {\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n dispatchSeq: e.seq,\n settleSeq: null,\n causedBy: e.causedBy,\n });\n }\n else if (e.event === 'settled') {\n const inv = out.get(e.invocationId);\n if (inv)\n inv.settleSeq = e.seq;\n }\n }\n return [...out.values()];\n}",
105
105
  },
106
106
  "loop-semantics": {
107
107
  name: "loop-semantics",
@@ -653,7 +653,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
653
653
  }
654
654
  if (traceOn) {
655
655
  lines.push(`// trace wiring (blob-provided emitter; hooks INSIDE runStep — ADR-003)`);
656
- lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP)`);
656
+ lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP, 'rendered-script')`);
657
657
  lines.push(`__hooks.onDispatch = function (e) { return traceOnDispatch(__traceState, e) }`);
658
658
  lines.push(`__hooks.onSettle = function (e) { return traceOnSettle(__traceState, e) }`);
659
659
  lines.push(`async function __traceFlushNow(phaseName, stepLabel) {`);
package/src/loop-trace.ts CHANGED
@@ -60,6 +60,15 @@ export interface TraceSettleEvent {
60
60
  wallTime?: string | null;
61
61
  }
62
62
 
63
+ /**
64
+ * WHICH CODE PATH was meant to write this file. A HINT, never a trust statement — the rendered
65
+ * script's flush is performed BY AN AGENT, which therefore controls these bytes before the file
66
+ * exists and could write any value here. It is deliberately NOT called `attestedBy`, so nothing in
67
+ * the codebase can read it as provenance (ADR-001 round 1 made exactly that mistake). The trust
68
+ * question is answered by `deriveAttestation`, from an artifact the sandboxed script cannot write.
69
+ */
70
+ export type TraceEmitterPath = 'dz-process' | 'rendered-script';
71
+
63
72
  export interface TraceRunOpened {
64
73
  v: 1;
65
74
  runId: string;
@@ -67,6 +76,9 @@ export interface TraceRunOpened {
67
76
  event: 'run.opened';
68
77
  planDigest: string;
69
78
  execFp: string;
79
+ /** Optional ON THE WIRE (NFR-1): an older reader ignores it, a newer reader over an older trace
80
+ * gets `unknown` — which is never `instrument`. */
81
+ emitterPath?: TraceEmitterPath;
70
82
  }
71
83
 
72
84
  export interface TraceRunClosed {
@@ -122,6 +134,10 @@ export function traceValidateEvent(e: unknown): string | null {
122
134
  }
123
135
  if (kind === 'run.opened') {
124
136
  if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string') return 'run.opened needs planDigest + execFp';
137
+ const ep = ev['emitterPath'];
138
+ // Absent is legal (NFR-1). Present-but-outside-the-union is a REFUSAL, not a downgrade: a value
139
+ // like 'trusted' is someone trying to say something the vocabulary does not permit.
140
+ if (ep !== undefined && ep !== 'dz-process' && ep !== 'rendered-script') return 'emitterPath must be dz-process|rendered-script';
125
141
  return null;
126
142
  }
127
143
  if (kind === 'run.closed') {
@@ -132,11 +148,17 @@ export function traceValidateEvent(e: unknown): string | null {
132
148
  return 'unknown event kind';
133
149
  }
134
150
 
135
- /** Open a trace state and buffer the run.opened frame. Throws on an invalid runId (fail-closed). */
136
- export function traceInit(runId: string, planDigest: string, execFp: string): TraceState {
151
+ /**
152
+ * Open a trace state and buffer the run.opened frame. Throws on an invalid runId (fail-closed).
153
+ *
154
+ * `emitterPath` is REQUIRED and has NO DEFAULT, on purpose: a default would be chosen once, by
155
+ * whoever added the parameter, and every future caller that forgot it would silently inherit that
156
+ * choice. A missing argument must be a compile error instead.
157
+ */
158
+ export function traceInit(runId: string, planDigest: string, execFp: string, emitterPath: TraceEmitterPath): TraceState {
137
159
  if (!TRACE_RUNID_RE.test(runId)) throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));
138
160
  const state: TraceState = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };
139
- const opened: TraceRunOpened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp };
161
+ const opened: TraceRunOpened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp, emitterPath };
140
162
  traceBuffer(state, opened);
141
163
  return state;
142
164
  }
@@ -314,6 +336,11 @@ export interface TraceRun {
314
336
  /** No run.closed frame ⇒ the tail may be lost; truncated-window invariants report inconclusive. */
315
337
  incomplete: boolean;
316
338
  parseErrors: string[];
339
+ /** The self-declared HINT (see TraceEmitterPath) — null when absent. NEVER read as provenance. */
340
+ emitterPath: TraceEmitterPath | null;
341
+ /** Two `run.opened` frames that DISAGREE. The scan used to keep the last one silently, which
342
+ * would let a spliced frame overwrite the genuine one; a conflict now forbids `instrument`. */
343
+ openConflict: boolean;
317
344
  }
318
345
 
319
346
  /**
@@ -339,7 +366,7 @@ export interface TraceRun {
339
366
  * the only ordering the file can still testify to. INV-14 fails such a trace on uniqueness anyway.
340
367
  */
341
368
  export function parseTrace(text: string): TraceRun {
342
- const run: TraceRun = { runId: null, planDigest: null, execFp: null, events: [], incomplete: true, parseErrors: [] };
369
+ const run: TraceRun = { runId: null, planDigest: null, execFp: null, events: [], incomplete: true, parseErrors: [], emitterPath: null, openConflict: false };
343
370
  const scanned: TraceEvent[] = [];
344
371
  for (const line of String(text ?? '').split('\n')) {
345
372
  const t = line.trim();
@@ -364,9 +391,22 @@ export function parseTrace(text: string): TraceRun {
364
391
  const dispatchSeen = new Set<string>();
365
392
  for (const ev of scanned) {
366
393
  if (ev.event === 'run.opened') {
394
+ const prior = run.events.some((p) => p.event === 'run.opened');
395
+ if (prior) {
396
+ const disagrees =
397
+ run.runId !== ev.runId ||
398
+ run.planDigest !== ev.planDigest ||
399
+ run.execFp !== ev.execFp ||
400
+ run.emitterPath !== (ev.emitterPath ?? null);
401
+ if (disagrees) {
402
+ run.openConflict = true;
403
+ run.parseErrors.push('conflicting run.opened frames — refusing to prefer either (ADR-001)');
404
+ }
405
+ }
367
406
  run.runId = ev.runId;
368
407
  run.planDigest = ev.planDigest;
369
408
  run.execFp = ev.execFp;
409
+ run.emitterPath = ev.emitterPath ?? null;
370
410
  }
371
411
  if (ev.event === 'run.closed') run.incomplete = false;
372
412
  if (ev.event === 'dispatched') {
@@ -386,12 +426,84 @@ export function parseTrace(text: string): TraceRun {
386
426
  return run;
387
427
  }
388
428
 
429
+ /**
430
+ * ATTESTATION — the trust question, answered by the READER, never by the file.
431
+ *
432
+ * `instrument` means, EXACTLY (ADR-001 round 3, wording taken from the cross-family reviewer):
433
+ * *the bytes read match the identifiers, the hash and the line count asserted by the co-located
434
+ * `run-state.json`.* It does NOT mean `dz` historically wrote those bytes, nor that this is the
435
+ * directory it wrote them in. A byte-identical replay passes. Copying the matched trace+run-state
436
+ * pair elsewhere passes. Both are accepted and tested, not patched.
437
+ *
438
+ * What it DOES buy: `run-state.json` is written by the dz process, and the rendered script runs in a
439
+ * sandbox with no filesystem — so an agent that mis-writes, truncates, reorders or invents its
440
+ * flush cannot reach `instrument`. What it cannot buy: a defence against an actor with write access
441
+ * to the run directory. On this machine the agent IS such an actor and the signing key is readable
442
+ * by the same account, so no local artifact — signature included — closes that. Named, not hidden.
443
+ *
444
+ * `unknown` is kept DISTINCT from `agent` (a legacy instrument run, a stripped field and explicit
445
+ * agent testimony are different facts) while sharing its conservative tier for every decision.
446
+ */
447
+ export type TraceAttestation = 'instrument' | 'agent' | 'unknown';
448
+
449
+ /** The subset of `run-state.json` the attestation reads. Unknown-shaped input is not an error — it
450
+ * simply fails to bind, which yields `unknown`. */
451
+ export interface TraceRunStateBinding {
452
+ runId?: unknown;
453
+ planDigest?: unknown;
454
+ execFp?: unknown;
455
+ traceSha256?: unknown;
456
+ traceLines?: unknown;
457
+ }
458
+
459
+ /** What the CALLER measured about the trace text it actually read. Passed in rather than computed
460
+ * here so this module keeps its zero dependencies (no node:crypto) and stays trivially testable. */
461
+ export interface TraceObserved {
462
+ sha256: string;
463
+ lines: number;
464
+ }
465
+
466
+ export function deriveAttestation(run: TraceRun, state: TraceRunStateBinding | null | undefined, observed: TraceObserved): TraceAttestation {
467
+ // A conflict between two run.opened frames forbids the favourable reading outright: preferring
468
+ // either one is exactly the silent choice this feature exists to remove.
469
+ if (run.openConflict) return 'unknown';
470
+ // QE round 1 / H1: EVERY bound value must be non-empty. Equality alone let a trace whose
471
+ // planDigest and execFp are both '' bind against a state carrying the same two empty strings —
472
+ // an "identity" that identifies nothing. A binding over absent values is not a binding.
473
+ const nonEmpty = (a: unknown, b: unknown): boolean => typeof a === 'string' && a !== '' && a === b;
474
+ const bound =
475
+ state != null &&
476
+ nonEmpty(state.runId, run.runId) &&
477
+ nonEmpty(state.planDigest, run.planDigest) &&
478
+ nonEmpty(state.execFp, run.execFp) &&
479
+ // CONTENT binding, not just identifiers: a legacy run-state with no traceSha256 can never mint
480
+ // `instrument` (that was the reviewer's stale-directory counterexample).
481
+ nonEmpty(state.traceSha256, observed.sha256) &&
482
+ typeof state.traceLines === 'number' && state.traceLines === observed.lines;
483
+ if (bound) return 'instrument';
484
+ if (run.emitterPath === 'rendered-script') return 'agent';
485
+ return 'unknown';
486
+ }
487
+
389
488
  export type InvariantStatus = 'pass' | 'fail' | 'inconclusive';
390
489
 
391
490
  export interface InvariantVerdict {
392
491
  id: string;
393
492
  status: InvariantStatus;
394
493
  message: string;
494
+ /**
495
+ * FR-3 — the qualifier travels IN the verdict, never beside it. A caller that stores a verdict
496
+ * and reads it back later must not be able to end up holding a bare `pass` whose attestation was
497
+ * dropped in transit. Optional only so a caller that has not derived one is a compile-time
498
+ * possibility; `stampAttestation` is how the reader attaches it.
499
+ */
500
+ attestation?: TraceAttestation;
501
+ }
502
+
503
+ /** Attach one attestation to every verdict in a batch. Kept as a named function rather than a spread
504
+ * at each call site so a NEW verdict producer cannot silently ship unstamped verdicts. */
505
+ export function stampAttestation(verdicts: InvariantVerdict[], attestation: TraceAttestation): InvariantVerdict[] {
506
+ return verdicts.map((v) => ({ ...v, attestation }));
395
507
  }
396
508
 
397
509
  interface Invocation {
@@ -697,7 +809,12 @@ export interface Timeline {
697
809
  /**
698
810
  * Merge one timeline: trace.jsonl is the AUTHORITATIVE order (rows sorted by seq); checkpoints,
699
811
  * cost-ledger lines and usageEvents are appended as unordered context rows (seq 0); journal.jsonl
700
- * contributes DIAGNOSTIC agentId correlation only, never ordering (the ACL of 04 §8).
812
+ * contributes a DIAGNOSTIC LINE COUNT only, never ordering (the ACL of 04 §8).
813
+ *
814
+ * Honesty fix (2026-08-20): this note used to promise "agentId correlation", which the code never
815
+ * did — it counts lines. A comment claiming an analysis that does not exist is the same defect class
816
+ * this feature was built to remove, one layer up. The real correlation now lives in
817
+ * `trace-corroborate.ts`, behind `dz workflow-trace --corroborate`, where it is scoped and tested.
701
818
  */
702
819
  export function assembleTimeline(input: {
703
820
  trace: string;
@@ -749,7 +866,7 @@ export function assembleTimeline(input: {
749
866
  sources.push('journal (diagnostic only — never ordering)');
750
867
  let n = 0;
751
868
  for (const line of input.journal.split('\n')) if (line.trim() !== '') n++;
752
- rows.push({ seq: 0, kind: 'journal', label: 'journal', detail: `${n} host-journal line(s) — agentId correlation only; the host journal carries no seq/ts and NEVER orders this timeline`, wallTime: null });
869
+ rows.push({ seq: 0, kind: 'journal', label: 'journal', detail: n + ' host-journal line(s) — COUNT only; the host journal carries no seq/ts and NEVER orders this timeline. For an actual comparison run: dz workflow-trace --corroborate <hostRunDir>', wallTime: null });
753
870
  }
754
871
  return { runId: run.runId, incomplete: run.incomplete, rows, sources };
755
872
  }