gentle-pi 1.2.0 → 2.0.0

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 (107) hide show
  1. package/README.md +40 -9
  2. package/assets/agents/review-readability.md +0 -1
  3. package/assets/agents/review-reliability.md +0 -1
  4. package/assets/agents/review-resilience.md +0 -1
  5. package/assets/agents/review-risk.md +0 -1
  6. package/contracts/review-integration/v1/fixtures/binding-revision-conflict.fixture.json +25 -0
  7. package/contracts/review-integration/v1/fixtures/capabilities-v1.2.fixture.json +263 -0
  8. package/contracts/review-integration/v1/fixtures/capabilities-v1.3.fixture.json +301 -0
  9. package/contracts/review-integration/v1/fixtures/capabilities-v1.4.fixture.json +312 -0
  10. package/contracts/review-integration/v1/fixtures/capabilities-v1.5.fixture.json +321 -0
  11. package/contracts/review-integration/v1/fixtures/consent.fixture.json +36 -0
  12. package/contracts/review-integration/v1/fixtures/final-verification-incident.fixture.json +1 -0
  13. package/contracts/review-integration/v1/fixtures/operation.fixture.json +4 -0
  14. package/contracts/review-integration/v1/fixtures/repair-preflight.fixture.json +54 -0
  15. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +97 -0
  16. package/contracts/review-integration/v1/fixtures/start.fixture.json +25 -1
  17. package/contracts/review-integration/v1/fixtures/status-ambiguous.fixture.json +4 -0
  18. package/contracts/review-integration/v1/fixtures/status-corrupted.fixture.json +4 -0
  19. package/contracts/review-integration/v1/fixtures/status-recover.fixture.json +4 -0
  20. package/contracts/review-integration/v1/fixtures/status-unrelated.fixture.json +4 -0
  21. package/contracts/review-integration/v1/fixtures/status-v2-ambiguous.fixture.json +110 -0
  22. package/contracts/review-integration/v1/fixtures/status-v2-corrupted.fixture.json +107 -0
  23. package/contracts/review-integration/v1/fixtures/status-v2-final-verification-retry.fixture.json +192 -0
  24. package/contracts/review-integration/v1/fixtures/status-v2-recover.fixture.json +130 -0
  25. package/contracts/review-integration/v1/fixtures/status-v2-repair.fixture.json +161 -0
  26. package/contracts/review-integration/v1/fixtures/status-v2-unrelated.fixture.json +103 -0
  27. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +193 -0
  28. package/contracts/review-integration/v1/fixtures/status.fixture.json +19 -5
  29. package/contracts/review-integration/v1/fixtures/verification-evidence.fixture.json +1 -0
  30. package/contracts/review-integration/v1/schemas/admitted-result.schema.json +35 -0
  31. package/contracts/review-integration/v1/schemas/artifact-subject.schema.json +33 -0
  32. package/contracts/review-integration/v1/schemas/authority-repair-assessment.schema.json +247 -0
  33. package/contracts/review-integration/v1/schemas/capabilities-v1.2.schema.json +249 -0
  34. package/contracts/review-integration/v1/schemas/capabilities-v1.3.schema.json +429 -0
  35. package/contracts/review-integration/v1/schemas/capabilities-v1.4.schema.json +432 -0
  36. package/contracts/review-integration/v1/schemas/capabilities-v1.5.schema.json +154 -0
  37. package/contracts/review-integration/v1/schemas/consent.schema.json +86 -0
  38. package/contracts/review-integration/v1/schemas/failure.schema.json +54 -8
  39. package/contracts/review-integration/v1/schemas/final-verification-incident.schema.json +51 -0
  40. package/contracts/review-integration/v1/schemas/operation.schema.json +204 -31
  41. package/contracts/review-integration/v1/schemas/repair.schema.json +251 -0
  42. package/contracts/review-integration/v1/schemas/result-artifact-v2.schema.json +38 -0
  43. package/contracts/review-integration/v1/schemas/start-v2.schema.json +216 -0
  44. package/contracts/review-integration/v1/schemas/start.schema.json +79 -3
  45. package/contracts/review-integration/v1/schemas/status-v2.schema.json +313 -0
  46. package/contracts/review-integration/v1/schemas/status.schema.json +37 -10
  47. package/contracts/review-integration/v1/schemas/targeted-validation-request.schema.json +46 -0
  48. package/contracts/review-integration/v1/schemas/verification-evidence.schema.json +64 -0
  49. package/contracts/review-integration/v2/fixtures/capabilities.fixture.json +111 -0
  50. package/contracts/review-integration/v2/fixtures/consent.fixture.json +36 -0
  51. package/contracts/review-integration/v2/fixtures/start.fixture.json +93 -0
  52. package/contracts/review-integration/v2/fixtures/status.fixture.json +113 -0
  53. package/contracts/review-integration/v2/schemas/admitted-result.schema.json +14 -0
  54. package/contracts/review-integration/v2/schemas/artifact-subject.schema.json +36 -0
  55. package/contracts/review-integration/v2/schemas/capabilities.schema.json +111 -0
  56. package/contracts/review-integration/v2/schemas/consent.schema.json +70 -0
  57. package/contracts/review-integration/v2/schemas/failure.schema.json +35 -0
  58. package/contracts/review-integration/v2/schemas/operation.schema.json +46 -0
  59. package/contracts/review-integration/v2/schemas/repair.schema.json +22 -0
  60. package/contracts/review-integration/v2/schemas/start.schema.json +106 -0
  61. package/contracts/review-integration/v2/schemas/status.schema.json +104 -0
  62. package/docs/native-authority-architecture.md +18 -3
  63. package/docs/review-integration.md +175 -27
  64. package/extensions/gentle-ai.ts +414 -19
  65. package/lib/gentle-ai-binary.ts +1 -1
  66. package/lib/native-review-cli.ts +679 -57
  67. package/lib/review-candidate-view.ts +131 -1
  68. package/lib/review-compact-contract.ts +12 -3
  69. package/lib/review-consent-latch.ts +51 -0
  70. package/lib/review-correction-lifecycle.ts +170 -0
  71. package/lib/review-integration-v2.ts +1563 -0
  72. package/package.json +15 -11
  73. package/runtime/gentle-ai-binary.mjs +1 -1
  74. package/runtime/native-review-cli.mjs +663 -41
  75. package/runtime/review-integration-v2.mjs +1564 -0
  76. package/scripts/build-git-commit-transaction-runner.mjs +1 -1
  77. package/scripts/gentle-ai-installer.mjs +16 -11
  78. package/scripts/install-gentle-ai.mjs +7 -3
  79. package/scripts/test-packed-runner.mjs +2 -2
  80. package/scripts/verify-package-files.mjs +216 -64
  81. package/skills/release/SKILL.md +38 -11
  82. package/tests/devbinary/native-review-parity.devtest.ts +348 -0
  83. package/tests/gentle-ai-binary.test.ts +8 -1
  84. package/tests/gentle-ai-installer.test.ts +24 -21
  85. package/tests/native-binary-gate.test.ts +145 -0
  86. package/tests/native-review-capability-contract.test.ts +87 -0
  87. package/tests/native-review-cli.test.ts +44 -1
  88. package/tests/native-review-parity-runtime.test.ts +70 -23
  89. package/tests/native-review-parity.test.ts +673 -0
  90. package/tests/native-risk-evidence-parity.test.ts +91 -0
  91. package/tests/package-manifest.test.ts +72 -7
  92. package/tests/review-actor-tool-deny.test.ts +20 -5
  93. package/tests/review-candidate-view.test.ts +331 -1
  94. package/tests/review-compact-contract.test.ts +5 -0
  95. package/tests/review-consent-latch.test.ts +67 -0
  96. package/tests/review-controller-native-recovery.test.ts +180 -5
  97. package/tests/review-controller-native-routing.test.ts +385 -12
  98. package/tests/review-controller-workspace-root.test.ts +26 -6
  99. package/tests/review-correction-lifecycle.test.ts +174 -0
  100. package/tests/review-integration-v2.test.ts +539 -0
  101. package/tests/runtime-harness.mjs +187 -0
  102. package/tests/support/native-binary-gate.ts +86 -0
  103. package/tests/verify-package-files.test.ts +119 -0
  104. package/lib/review-integration-v1.ts +0 -795
  105. package/runtime/review-integration-v1.mjs +0 -796
  106. package/tests/native-review-integration-v1.test.ts +0 -102
  107. package/tests/review-integration-v1.test.ts +0 -530
@@ -0,0 +1,1564 @@
1
+ // Generated by scripts/build-git-commit-transaction-runner.mjs. Do not edit.
2
+ export const REVIEW_INTEGRATION_CONTRACT = "gentle-ai.review-integration/v2";
3
+
4
+ export const REVIEW_INTEGRATION_OPERATION = {
5
+ BIND_SDD: "review.bind_sdd",
6
+ CAPABILITIES: "review.capabilities",
7
+ FINALIZE: "review.finalize",
8
+ REPAIR: "review.repair",
9
+ START: "review.start",
10
+ STATUS: "review.status",
11
+ VALIDATE: "review.validate",
12
+ } ;
13
+
14
+
15
+ // Retained enums/unions, identity-neutral: copied verbatim from lib/review-integration-v1.ts.
16
+ export const REVIEW_AUTHORITY_APPLICABILITY = {
17
+ CURRENT_TARGET: "current_target",
18
+ UNRELATED: "unrelated",
19
+ AMBIGUOUS: "ambiguous",
20
+ CORRUPTED: "corrupted",
21
+ NOT_EVALUATED: "not_evaluated",
22
+ } ;
23
+
24
+
25
+ export const REVIEW_REPLAYABILITY = {
26
+ NOT_REPLAYABLE: "not_replayable",
27
+ EXACT_REPLAY_SAFE: "exact_replay_safe",
28
+ STATUS_REQUIRED: "status_required",
29
+ MANUAL_ACTION_REQUIRED: "manual_action_required",
30
+ } ;
31
+
32
+
33
+ export const REVIEW_MUTATION_OUTCOME = {
34
+ NOT_STARTED: "not_started",
35
+ UNKNOWN: "unknown",
36
+ COMMITTED: "committed",
37
+ } ;
38
+
39
+
40
+ export const REVIEW_PROJECTION = {
41
+ STAGED: "staged",
42
+ WORKSPACE: "workspace",
43
+ } ;
44
+
45
+
46
+ export const REVIEW_PROJECTION_KIND = {
47
+ CURRENT_CHANGES: "current-changes",
48
+ BASE_DIFF: "base-diff",
49
+ BASE_WORKSPACE_OVERLAY: "base-workspace-overlay",
50
+ EXACT_REVISION: "exact-revision",
51
+ FIX_DIFF: "fix-diff",
52
+ } ;
53
+
54
+
55
+ export const REVIEW_AUTHORITY_VERSION = {
56
+ COMPACT_V2: "compact-v2",
57
+ LEGACY_V1: "legacy-v1",
58
+ } ;
59
+
60
+
61
+ // Widened by protocol v2: `correction_required` and `validating` are new
62
+ // start/status states introduced by the negotiated correction lifecycle.
63
+ export const REVIEW_START_STATE = {
64
+ UNREVIEWED: "unreviewed",
65
+ REVIEWING: "reviewing",
66
+ JUDGES_CONFIRMED: "judges_confirmed",
67
+ FINDINGS_FROZEN: "findings_frozen",
68
+ EVIDENCE_CLASSIFIED: "evidence_classified",
69
+ FIX_REQUIRED: "fix_required",
70
+ FIXING: "fixing",
71
+ FIX_VALIDATING: "fix_validating",
72
+ CORRECTION_REQUIRED: "correction_required",
73
+ VALIDATING: "validating",
74
+ READY_FINAL_VERIFICATION: "ready_final_verification",
75
+ FINAL_VERIFYING: "final_verifying",
76
+ APPROVED: "approved",
77
+ ESCALATED: "escalated",
78
+ INVALIDATED: "invalidated",
79
+ } ;
80
+
81
+
82
+ const START_ACTIONS = ["created", "resumed", "reuse-receipt", "blocked-scope-action"] ;
83
+ const RISK_LEVELS = ["low", "medium", "high"] ;
84
+ const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] ;
85
+ const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] ;
86
+ const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] ;
87
+ const STATUS_ACTIONS = ["start", "finalize", "validate", "recover", "retry_final_verification", "maintainer_action", "select_lineage", "repair_authority", "reconcile_finalize", "stop"] ;
88
+ export const REVIEW_STATUS_ACTION_DISPOSITION = {
89
+ SCOPE_CHANGED: "scope_changed",
90
+ INVALIDATED: "invalidated",
91
+ ESCALATED: "escalated",
92
+ FINAL_VERIFICATION_RETRY: "final_verification_retry",
93
+ } ;
94
+
95
+ const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] ;
96
+ const REQUIRED_OPERATIONS = Object.freeze(Object.values(REVIEW_INTEGRATION_OPERATION));
97
+ const REQUIRED_GATES = Object.freeze(["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] );
98
+ const REQUIRED_PROJECTIONS = Object.freeze(Object.values(REVIEW_PROJECTION));
99
+ const REQUIRED_SCHEMAS = Object.freeze([
100
+ "gentle-ai.review-admitted-result/v2",
101
+ "gentle-ai.review-artifact-subject/v2",
102
+ "gentle-ai.review-authority-repair-assessment/v1",
103
+ "gentle-ai.review-authority-status/v1",
104
+ "gentle-ai.review-gate-request/v1",
105
+ "gentle-ai.review-integration.capabilities/v2",
106
+ "gentle-ai.review-integration.consent/v2",
107
+ "gentle-ai.review-integration.failure/v2",
108
+ "gentle-ai.review-final-verification-incident/v1",
109
+ "gentle-ai.review-integration.operation/v2",
110
+ "gentle-ai.review-integration.projection/v1",
111
+ "gentle-ai.review-integration.repair/v2",
112
+ "gentle-ai.review-integration.start/v3",
113
+ "gentle-ai.review-integration.status/v3",
114
+ "gentle-ai.review-receipt/v1",
115
+ "gentle-ai.review-receipt/v2",
116
+ "gentle-ai.review-result-artifact/v2",
117
+ "gentle-ai.review-targeted-validation-request/v1",
118
+ "gentle-ai.review-verification-evidence/v2",
119
+ "https://gentle-ai.dev/schema/review/refuter/v1",
120
+ "https://gentle-ai.dev/schema/review/reviewer/v1",
121
+ "https://gentle-ai.dev/schema/review/validator/v1",
122
+ ] );
123
+ const OPTIONAL_FEATURE_NAMES = Object.freeze([
124
+ "base_ref_workspace_overlay",
125
+ "bounded_process_waits",
126
+ "classified_authority_repair",
127
+ "exact_gate_receipt_discovery",
128
+ "native_frozen_candidate_context",
129
+ "native_low_risk_verification",
130
+ "native_next_transition",
131
+ "one_shot_final_verification_retry",
132
+ "opaque_repository_context",
133
+ "outcome_bound_verification_evidence",
134
+ "provider_artifact_admission",
135
+ "provider_bound_native_git_context",
136
+ "provider_targeted_validation_request",
137
+ "recovered_correction_evidence",
138
+ "risk_reasons",
139
+ "scope_change_diagnostics",
140
+ "validating_result_reopen",
141
+ ] );
142
+ const FEATURE_NAMES = Object.freeze([
143
+ "base_ref_workspace_overlay",
144
+ "bounded_process_waits",
145
+ "classified_authority_repair",
146
+ "compact_v2_authority",
147
+ "exact_gate_receipt_discovery",
148
+ "exact_receipt_replay",
149
+ "five_delivery_gates",
150
+ "immutable_snapshot",
151
+ "legacy_v1_target_scoped_read_only",
152
+ "native_frozen_candidate_context",
153
+ "native_low_risk_verification",
154
+ "native_next_transition",
155
+ "one_shot_final_verification_retry",
156
+ "opaque_repository_context",
157
+ "outcome_bound_verification_evidence",
158
+ "provider_artifact_admission",
159
+ "provider_bound_native_git_context",
160
+ "provider_targeted_validation_request",
161
+ "recovered_correction_evidence",
162
+ "repository_independent_capabilities",
163
+ "restart_safe_projection",
164
+ "risk_reasons",
165
+ "scope_change_diagnostics",
166
+ "sdd_receipt_binding",
167
+ "target_scoped_status",
168
+ "uniform_failure_envelope",
169
+ "validating_result_reopen",
170
+ ] );
171
+ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) => !(OPTIONAL_FEATURE_NAMES ).includes(name)));
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+
181
+
182
+
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+
209
+
210
+
211
+
212
+
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+
221
+
222
+
223
+
224
+
225
+
226
+
227
+
228
+
229
+
230
+
231
+
232
+
233
+
234
+
235
+
236
+
237
+
238
+
239
+
240
+
241
+
242
+
243
+
244
+
245
+
246
+
247
+
248
+
249
+
250
+
251
+
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+
263
+
264
+
265
+
266
+
267
+
268
+
269
+
270
+
271
+
272
+
273
+
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+
314
+
315
+
316
+
317
+
318
+
319
+
320
+
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+
355
+
356
+
357
+
358
+
359
+
360
+
361
+
362
+
363
+
364
+
365
+
366
+
367
+
368
+
369
+
370
+
371
+
372
+
373
+
374
+
375
+
376
+
377
+
378
+
379
+
380
+
381
+
382
+
383
+
384
+
385
+
386
+
387
+
388
+
389
+
390
+
391
+
392
+
393
+
394
+
395
+
396
+
397
+
398
+
399
+
400
+
401
+
402
+
403
+
404
+
405
+
406
+
407
+ const FAILURE_REQUIRED_INPUTS = [
408
+ "lineage_id",
409
+ "change",
410
+ "expected_binding_revision",
411
+ "predecessor_lineage_id",
412
+ "expected_predecessor_revision",
413
+ "successor_lineage_id",
414
+ "disposition",
415
+ "reason",
416
+ "actor",
417
+ "incident",
418
+ "maintainer_authorization",
419
+ "base_ref",
420
+ ] ;
421
+
422
+ const FAILURE_NEXT_ACTIONS = ["correct_request", "retry", "retry_with_bounded_backoff", "review.status", "review.finalize", "review.repair", "review.bind_sdd", "explicit-maintainer-action", "stop"] ;
423
+
424
+ // Known cause_category values: the vendored failure.schema.json enum plus
425
+ // "incomplete_store_entry", which the v2.1.8 emitter produces beyond that enum.
426
+ // cause_category is diagnostic metadata (nothing routes on it), so unknown
427
+ // snake_case values are tolerated for forward compatibility.
428
+ const FAILURE_CAUSE_CATEGORIES = ["inventory_io_or_layout", "lock_ambiguous", "reset_residue", "record_or_graph_invalid", "inventory_incomplete", "incomplete_store_entry"] ;
429
+
430
+
431
+
432
+
433
+
434
+
435
+
436
+
437
+
438
+
439
+
440
+
441
+
442
+
443
+
444
+
445
+
446
+
447
+
448
+
449
+
450
+
451
+
452
+
453
+
454
+
455
+
456
+
457
+
458
+
459
+
460
+
461
+
462
+
463
+
464
+
465
+
466
+
467
+
468
+
469
+
470
+
471
+
472
+
473
+
474
+
475
+
476
+
477
+
478
+
479
+
480
+
481
+
482
+
483
+
484
+
485
+
486
+
487
+
488
+
489
+
490
+
491
+
492
+
493
+
494
+
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+
518
+
519
+
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+
528
+
529
+
530
+
531
+
532
+
533
+
534
+
535
+
536
+
537
+
538
+
539
+
540
+
541
+
542
+
543
+
544
+
545
+
546
+
547
+
548
+
549
+
550
+
551
+
552
+
553
+
554
+ // ---------------------------------------------------------------------------
555
+ // Primitives — ported verbatim from lib/review-integration-v1.ts. exactRecord's
556
+ // exact-key discipline (allowAdditional = false by default) is the single
557
+ // highest-risk thing to port faithfully: losing it means Pi silently accepts
558
+ // malformed v2 payloads.
559
+ // ---------------------------------------------------------------------------
560
+
561
+ function record(value , label ) {
562
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${label} must be an object`);
563
+ return value ;
564
+ }
565
+
566
+ function exactRecord(value , label , required , optional = [], allowAdditional = false) {
567
+ const body = record(value, label);
568
+ for (const key of required) {
569
+ if (!Object.hasOwn(body, key)) throw new TypeError(`${label}.${key} is required`);
570
+ }
571
+ const allowed = new Set([...required, ...optional]);
572
+ if (!allowAdditional) for (const key of Object.keys(body)) if (!allowed.has(key)) throw new TypeError(`${label}.${key} is not allowed`);
573
+ return body;
574
+ }
575
+
576
+ function text(value , label , options = {}) {
577
+ const minimum = options.minimum ?? 0;
578
+ if (typeof value !== "string" || value.length < minimum || (options.maximum !== undefined && value.length > options.maximum) || (options.pattern !== undefined && !options.pattern.test(value))) {
579
+ throw new TypeError(`${label} is invalid`);
580
+ }
581
+ return value;
582
+ }
583
+
584
+ function nonempty(value , label ) {
585
+ return text(value, label, { minimum: 1 });
586
+ }
587
+
588
+ function boolean(value , label ) {
589
+ if (typeof value !== "boolean") throw new TypeError(`${label} must be a boolean`);
590
+ return value;
591
+ }
592
+
593
+ function integer(value , label , minimum = 0, maximum ) {
594
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || (maximum !== undefined && value > maximum)) {
595
+ throw new TypeError(`${label} must be an integer in range`);
596
+ }
597
+ return value;
598
+ }
599
+
600
+ function enumeration (value , values , label ) {
601
+ if (typeof value !== "string" || !values.includes(value )) throw new TypeError(`${label} is unsupported`);
602
+ return value ;
603
+ }
604
+
605
+ function canonicalJson(value ) {
606
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
607
+ if (typeof value === "object" && value !== null) {
608
+ const body = value ;
609
+ return `{${Object.keys(body).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(body[key])}`).join(",")}}`;
610
+ }
611
+ return JSON.stringify(value);
612
+ }
613
+
614
+ function array (value , label , decodeItem , options = {}) {
615
+ if (!Array.isArray(value) || value.length < (options.minimum ?? 0) || (options.maximum !== undefined && value.length > options.maximum)) {
616
+ throw new TypeError(`${label} has an invalid length`);
617
+ }
618
+ const decoded = value.map((entry, index) => decodeItem(entry, `${label}[${index}]`));
619
+ if (options.unique && new Set(decoded.map(canonicalJson)).size !== decoded.length) throw new TypeError(`${label} must not contain duplicates`);
620
+ return decoded;
621
+ }
622
+
623
+ function stringArray(value , label , options = {}) {
624
+ return array(value, label, (entry, itemLabel) => text(entry, itemLabel, { minimum: 1, pattern: options.pattern }), options);
625
+ }
626
+
627
+ function enumArray (value , values , label , options = {}) {
628
+ return array(value, label, (entry, itemLabel) => enumeration(entry, values, itemLabel), options);
629
+ }
630
+
631
+ function sha256(value , label ) {
632
+ return text(value, label, { pattern: /^sha256:[0-9a-f]{64}$/ });
633
+ }
634
+
635
+ function gitTree(value , label ) {
636
+ return text(value, label, { pattern: /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/ });
637
+ }
638
+
639
+ function lineage(value , label ) {
640
+ return text(value, label, { pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ });
641
+ }
642
+
643
+ function safePath(value , label ) {
644
+ return text(value, label, { minimum: 1, pattern: /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$)).+$/ });
645
+ }
646
+
647
+ // v2 constant identity: every v2 payload pins one `const` schema, so unlike
648
+ // v1's requireVersionedIdentity there is no minor-driven revision to resolve.
649
+ function requireIdentity(value , schema , operation ) {
650
+ if (value.schema !== schema) throw new TypeError(`schema must be ${schema}`);
651
+ if (value.contract !== REVIEW_INTEGRATION_CONTRACT) throw new TypeError(`contract must be ${REVIEW_INTEGRATION_CONTRACT}`);
652
+ if (operation !== undefined && value.operation !== operation) throw new TypeError(`operation must be ${operation}`);
653
+ }
654
+
655
+ function assertExactSet(actual , expected , label ) {
656
+ if (actual.length !== expected.length || expected.some((value) => !actual.includes(value))) throw new TypeError(`${label} does not match the required integration surface`);
657
+ }
658
+
659
+ // An advertised surface is a superset promise, not an exact manifest — see
660
+ // lib/review-integration-v1.mjs's identical comment for the v2.2.0 lesson this
661
+ // codifies: demanding an exact match rejects a compatible provider release.
662
+ function assertSupersetOf(actual , required , label ) {
663
+ const advertised = new Set(actual);
664
+ const missing = required.filter((value) => !advertised.has(value));
665
+ if (missing.length > 0) throw new TypeError(`${label} omits the required integration surface: ${missing.join(", ")}`);
666
+ }
667
+
668
+ // ---------------------------------------------------------------------------
669
+ // Capabilities/v2
670
+ // ---------------------------------------------------------------------------
671
+
672
+ function decodeFeature(value , label ) {
673
+ const feature = exactRecord(value, label, ["name", "supported", "requires"]);
674
+ return {
675
+ name: enumeration(feature.name, FEATURE_NAMES, `${label}.name`),
676
+ supported: boolean(feature.supported, `${label}.supported`),
677
+ requires: stringArray(feature.requires, `${label}.requires`, { unique: true }),
678
+ };
679
+ }
680
+
681
+ function decodeOptionalFeature(value , label ) {
682
+ const feature = exactRecord(value, label, ["name", "supported", "requires"]);
683
+ return {
684
+ name: nonempty(feature.name, `${label}.name`),
685
+ supported: boolean(feature.supported, `${label}.supported`),
686
+ requires: stringArray(feature.requires, `${label}.requires`, { unique: true }),
687
+ };
688
+ }
689
+
690
+ export function decodeReviewCapabilitiesV2(value , verifiedExecutableDigest ) {
691
+ const requiredFields = ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"] ;
692
+ const body = exactRecord(value, "capabilities", requiredFields, ["bootstrap"]);
693
+ requireIdentity(body, "gentle-ai.review-integration.capabilities/v2");
694
+
695
+ const protocol = exactRecord(body.protocol, "capabilities.protocol", ["major", "minor"]);
696
+ if (protocol.major !== 2 || protocol.minor !== 0) throw new TypeError("incompatible review integration protocol");
697
+
698
+ const packageIdentity = exactRecord(body.package, "capabilities.package", ["name", "version", "release_channel"]);
699
+ if (packageIdentity.name !== "gentle-ai") throw new TypeError("capabilities package identity mismatch");
700
+ const packageVersion = nonempty(packageIdentity.version, "capabilities.package.version");
701
+ enumeration(packageIdentity.release_channel, ["development", "prerelease", "stable"] , "capabilities.package.release_channel");
702
+
703
+ const build = exactRecord(body.build, "capabilities.build", ["id", "go_version", "module_version", "vcs", "vcs_revision", "vcs_time", "vcs_modified"]);
704
+ const buildId = sha256(build.id, "capabilities.build.id");
705
+ nonempty(build.go_version, "capabilities.build.go_version");
706
+ for (const field of ["module_version", "vcs", "vcs_revision", "vcs_time"] ) text(build[field], `capabilities.build.${field}`);
707
+ enumeration(build.vcs_modified, ["true", "false", "unknown"] , "capabilities.build.vcs_modified");
708
+
709
+ const executable = exactRecord(body.executable, "capabilities.executable", ["sha256", "evidence", "verification"]);
710
+ const selfReportedDigest = sha256(executable.sha256, "capabilities.executable.sha256");
711
+ if (executable.evidence !== "self-reported" || executable.verification !== "compare-with-published-manifest") throw new TypeError("capabilities executable evidence is incompatible");
712
+ const normalizedVerifiedDigest = sha256(verifiedExecutableDigest.startsWith("sha256:") ? verifiedExecutableDigest : `sha256:${verifiedExecutableDigest}`, "verified executable digest");
713
+ if (selfReportedDigest !== normalizedVerifiedDigest) throw new TypeError("review provider executable digest mismatch");
714
+
715
+ const advertisedOperations = stringArray(body.operations, "capabilities.operations", { minimum: REQUIRED_OPERATIONS.length, unique: true });
716
+ const gates = enumArray(body.gates, REQUIRED_GATES, "capabilities.gates", { minimum: 5, maximum: 5, unique: true });
717
+ const projections = enumArray(body.projections, REQUIRED_PROJECTIONS, "capabilities.projections", { minimum: 2, maximum: 2, unique: true });
718
+ const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: REQUIRED_SCHEMAS.length, unique: true });
719
+ assertSupersetOf(advertisedOperations, REQUIRED_OPERATIONS, "capabilities operations");
720
+ assertExactSet(gates, REQUIRED_GATES, "capabilities gates");
721
+ assertExactSet(projections, REQUIRED_PROJECTIONS, "capabilities projections");
722
+ assertSupersetOf(advertisedSchemas, REQUIRED_SCHEMAS, "capabilities schemas");
723
+
724
+ const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
725
+ const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: 10 });
726
+ const optional = array(features.optional, "capabilities.features.optional", (entry, label) => decodeOptionalFeature(entry, label), { minimum: 17, unique: true });
727
+ const mandatoryNames = mandatory.map((feature) => feature.name);
728
+ const optionalNames = optional.map((feature) => feature.name);
729
+ assertExactSet(mandatoryNames, REQUIRED_MANDATORY_FEATURES, "mandatory capabilities");
730
+ if (new Set(optionalNames).size !== optionalNames.length) throw new TypeError("optional capabilities contain duplicate names");
731
+ if (optionalNames.some((name) => mandatoryNames.includes(name ))) throw new TypeError("mandatory and optional capabilities overlap");
732
+ if (mandatory.some((feature) => !feature.supported)) throw new TypeError("mandatory capability is unsupported");
733
+
734
+ const compatibility = exactRecord(body.compatibility, "capabilities.compatibility", ["minimum_protocol_major", "maximum_protocol_major", "additive_minor_policy", "unknown_mandatory", "unknown_optional", "modes", "legacy_window"]);
735
+ if (compatibility.minimum_protocol_major !== 2 || compatibility.maximum_protocol_major !== 2 || compatibility.additive_minor_policy !== "optional-fields-only" || compatibility.unknown_mandatory !== "reject" || compatibility.unknown_optional !== "ignore") {
736
+ throw new TypeError("incompatible capability evolution policy");
737
+ }
738
+ const modes = enumArray(compatibility.modes, Object.values(REVIEW_AUTHORITY_VERSION), "capabilities.compatibility.modes", { minimum: 2, maximum: 2 });
739
+ if (modes[0] !== REVIEW_AUTHORITY_VERSION.COMPACT_V2 || modes[1] !== REVIEW_AUTHORITY_VERSION.LEGACY_V1) throw new TypeError("capabilities compatibility modes are out of order");
740
+ const legacyWindow = exactRecord(compatibility.legacy_window, "capabilities.compatibility.legacy_window", ["mode", "state", "read_only", "deprecation_started", "removal", "minimum_compatibility_releases"]);
741
+ if (legacyWindow.mode !== REVIEW_AUTHORITY_VERSION.LEGACY_V1) throw new TypeError("capabilities legacy window mode is incompatible");
742
+ enumeration(legacyWindow.state, ["pre-fence", "active", "deprecated", "expired"] , "capabilities.compatibility.legacy_window.state");
743
+ boolean(legacyWindow.read_only, "capabilities.compatibility.legacy_window.read_only");
744
+ boolean(legacyWindow.deprecation_started, "capabilities.compatibility.legacy_window.deprecation_started");
745
+ nonempty(legacyWindow.removal, "capabilities.compatibility.legacy_window.removal");
746
+ integer(legacyWindow.minimum_compatibility_releases, "capabilities.compatibility.legacy_window.minimum_compatibility_releases", 1);
747
+
748
+ if (body.bootstrap !== undefined) {
749
+ const bootstrap = exactRecord(body.bootstrap, "capabilities.bootstrap", ["command", "target_selector_variants", "required_feature", "unsupported_outcome", "parent_only"]);
750
+ if (bootstrap.command !== "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --next-transition") throw new TypeError("capabilities.bootstrap.command is unsupported");
751
+ array(bootstrap.target_selector_variants, "capabilities.bootstrap.target_selector_variants", (entry, label) => {
752
+ const selector = exactRecord(entry, label, ["target_type", "arguments"]);
753
+ enumeration(selector.target_type, ["staged", "base_ref", "workspace_overlay_base_ref", "workspace_overlay_base_tree"] , `${label}.target_type`);
754
+ stringArray(selector.arguments, `${label}.arguments`, { minimum: 2 });
755
+ return selector;
756
+ }, { minimum: 4, maximum: 4 });
757
+ if (bootstrap.required_feature !== "native_next_transition") throw new TypeError("capabilities.bootstrap.required_feature is unsupported");
758
+ if (bootstrap.unsupported_outcome !== "unsupported-capability") throw new TypeError("capabilities.bootstrap.unsupported_outcome is unsupported");
759
+ if (bootstrap.parent_only !== true) throw new TypeError("capabilities.bootstrap.parent_only must be true");
760
+ }
761
+
762
+ return {
763
+ contract: REVIEW_INTEGRATION_CONTRACT,
764
+ packageVersion,
765
+ buildId,
766
+ executableDigest: selfReportedDigest,
767
+ operations: new Set(REQUIRED_OPERATIONS),
768
+ gates: new Set(gates),
769
+ projections: new Set(projections),
770
+ schemas: new Set(REQUIRED_SCHEMAS),
771
+ mandatoryFeatures: new Set(mandatoryNames),
772
+ optionalFeatures: new Set(optional.filter((feature) => feature.supported && (FEATURE_NAMES ).includes(feature.name)).map((feature) => feature.name)),
773
+ raw: body,
774
+ };
775
+ }
776
+
777
+ // ---------------------------------------------------------------------------
778
+ // artifact-subject/v2
779
+ // ---------------------------------------------------------------------------
780
+
781
+ function decodeArtifactSubject(value , label ) {
782
+ const body = exactRecord(value, label, ["schema", "subject_hash", "lineage_id", "authority_revision", "target_identity", "base_tree", "candidate_tree", "changed_path_manifest_sha256", "lens", "selected_order"], ["correction_target_identity"]);
783
+ if (body.schema !== "gentle-ai.review-artifact-subject/v2") throw new TypeError(`${label}.schema must be gentle-ai.review-artifact-subject/v2`);
784
+ return {
785
+ schema: "gentle-ai.review-artifact-subject/v2",
786
+ subjectHash: sha256(body.subject_hash, `${label}.subject_hash`),
787
+ lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
788
+ authorityRevision: sha256(body.authority_revision, `${label}.authority_revision`),
789
+ targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
790
+ baseTree: gitTree(body.base_tree, `${label}.base_tree`),
791
+ candidateTree: gitTree(body.candidate_tree, `${label}.candidate_tree`),
792
+ changedPathManifestSha256: sha256(body.changed_path_manifest_sha256, `${label}.changed_path_manifest_sha256`),
793
+ lens: enumeration(body.lens, REVIEW_LENSES, `${label}.lens`),
794
+ selectedOrder: integer(body.selected_order, `${label}.selected_order`, 0, 3),
795
+ ...(body.correction_target_identity === undefined ? {} : { correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`) }),
796
+ };
797
+ }
798
+
799
+ export function decodeReviewArtifactSubjectV2(value ) {
800
+ return decodeArtifactSubject(value, "artifact_subject");
801
+ }
802
+
803
+ function decodeChangedPathEntry(value , label ) {
804
+ const body = exactRecord(value, label, ["path", "status", "old_mode", "new_mode", "deleted", "type_changed", "mode_only", "intended_untracked"]);
805
+ return {
806
+ path: nonempty(body.path, `${label}.path`),
807
+ status: enumeration(body.status, ["A", "D", "M", "T"] , `${label}.status`),
808
+ oldMode: text(body.old_mode, `${label}.old_mode`, { pattern: /^[0-7]{6}$/ }),
809
+ newMode: text(body.new_mode, `${label}.new_mode`, { pattern: /^[0-7]{6}$/ }),
810
+ deleted: boolean(body.deleted, `${label}.deleted`),
811
+ typeChanged: boolean(body.type_changed, `${label}.type_changed`),
812
+ modeOnly: boolean(body.mode_only, `${label}.mode_only`),
813
+ intendedUntracked: boolean(body.intended_untracked, `${label}.intended_untracked`),
814
+ };
815
+ }
816
+
817
+ // ---------------------------------------------------------------------------
818
+ // start/v3
819
+ // ---------------------------------------------------------------------------
820
+
821
+ export function decodeReviewStartV3(value ) {
822
+ const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] ;
823
+ const body = exactRecord(value, "start", [
824
+ "schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
825
+ "selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
826
+ ], [...overlayFields, "changed_path_manifest", "repository_context"]);
827
+ requireIdentity(body, "gentle-ai.review-integration.start/v3", REVIEW_INTEGRATION_OPERATION.START);
828
+
829
+ // dependentRequired binds base_tree<->candidate_tree bidirectionally, and
830
+ // separately binds target_mode<->target_identity bidirectionally with both
831
+ // requiring base_tree+candidate_tree. The two pairs are independent: a
832
+ // selected_lenses START can carry base_tree/candidate_tree without ever
833
+ // carrying target_mode/target_identity.
834
+ if ((body.base_tree === undefined) !== (body.candidate_tree === undefined)) throw new TypeError("start.base_tree and start.candidate_tree must appear together");
835
+ const baseTree = body.base_tree === undefined ? undefined : gitTree(body.base_tree, "start.base_tree");
836
+ const candidateTree = body.candidate_tree === undefined ? undefined : gitTree(body.candidate_tree, "start.candidate_tree");
837
+
838
+ if ((body.target_mode === undefined) !== (body.target_identity === undefined)) throw new TypeError("start.target_mode and start.target_identity must appear together");
839
+ if (body.target_mode !== undefined && (baseTree === undefined || candidateTree === undefined)) throw new TypeError("start.target_mode and start.target_identity require base_tree and candidate_tree");
840
+ const targetMode = body.target_mode === undefined ? undefined : enumeration(body.target_mode, ["base-workspace-overlay"] , "start.target_mode");
841
+ const targetIdentity = body.target_identity === undefined ? undefined : sha256(body.target_identity, "start.target_identity");
842
+
843
+ if (body.changed_path_manifest !== undefined && (baseTree === undefined || candidateTree === undefined)) {
844
+ throw new TypeError("start.changed_path_manifest requires base_tree and candidate_tree");
845
+ }
846
+
847
+ const action = enumeration(body.action, START_ACTIONS, "start.action");
848
+ const state = enumeration(body.state, Object.values(REVIEW_START_STATE), "start.state");
849
+ const selectedLenses = enumArray(body.selected_lenses, REVIEW_LENSES, "start.selected_lenses", { maximum: 4, unique: true });
850
+
851
+ if (selectedLenses.length >= 1 && (body.base_tree === undefined || body.candidate_tree === undefined || body.changed_path_manifest === undefined)) {
852
+ throw new TypeError("start with selected_lenses requires base_tree, candidate_tree, and changed_path_manifest");
853
+ }
854
+
855
+ const requiresRepositoryContext = (action === "created" || action === "resumed") && state === REVIEW_START_STATE.REVIEWING;
856
+ if (requiresRepositoryContext && body.repository_context === undefined) throw new TypeError("start.repository_context is required when action is created/resumed and state is reviewing");
857
+ if (!requiresRepositoryContext && body.repository_context !== undefined) throw new TypeError("start.repository_context is only valid when action is created/resumed and state is reviewing");
858
+
859
+ let repositoryContext ;
860
+ if (body.repository_context !== undefined) {
861
+ const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"]);
862
+ if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
863
+ repositoryContext = {
864
+ capability: "review.opaque_repository_context",
865
+ handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
866
+ revision: sha256(source.revision, "start.repository_context.revision"),
867
+ targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
868
+ };
869
+ }
870
+
871
+ const riskReasons = array(body.risk_reasons, "start.risk_reasons", (entry, label) => {
872
+ const reason = exactRecord(entry, label, ["code"], ["signal", "path", "old_mode", "new_mode"]);
873
+ return {
874
+ code: enumeration(reason.code, RISK_REASON_CODES, `${label}.code`),
875
+ ...(reason.signal === undefined ? {} : { signal: enumeration(reason.signal, RISK_SIGNALS, `${label}.signal`) }),
876
+ ...(reason.path === undefined ? {} : { path: nonempty(reason.path, `${label}.path`) }),
877
+ ...(reason.old_mode === undefined ? {} : { oldMode: text(reason.old_mode, `${label}.old_mode`, { pattern: /^[0-7]{6}$/ }) }),
878
+ ...(reason.new_mode === undefined ? {} : { newMode: text(reason.new_mode, `${label}.new_mode`, { pattern: /^[0-7]{6}$/ }) }),
879
+ };
880
+ }, { minimum: 1, unique: true });
881
+
882
+ const artifactSubjects = array(body.artifact_subjects, "start.artifact_subjects", (entry, label) => decodeArtifactSubject(entry, label), { maximum: 4 });
883
+
884
+ return {
885
+ contract: REVIEW_INTEGRATION_CONTRACT,
886
+ action,
887
+ lensesRequired: boolean(body.lenses_required, "start.lenses_required"),
888
+ lineageId: nonempty(body.lineage_id, "start.lineage_id"),
889
+ state,
890
+ riskLevel: enumeration(body.risk_level, RISK_LEVELS, "start.risk_level"),
891
+ selectedLenses,
892
+ projection: enumeration(body.projection, REQUIRED_PROJECTIONS, "start.projection"),
893
+ changedFiles: integer(body.changed_files, "start.changed_files"),
894
+ changedLines: integer(body.changed_lines, "start.changed_lines"),
895
+ correctionBudget: integer(body.correction_budget, "start.correction_budget", 0, 200),
896
+ riskReasons,
897
+ artifactSubjects,
898
+ ...(targetMode === undefined ? {} : { targetMode }),
899
+ ...(targetIdentity === undefined ? {} : { targetIdentity }),
900
+ ...(baseTree === undefined ? {} : { baseTree }),
901
+ ...(candidateTree === undefined ? {} : { candidateTree }),
902
+ ...(body.changed_path_manifest === undefined ? {} : { changedPathManifest: array(body.changed_path_manifest, "start.changed_path_manifest", decodeChangedPathEntry, { unique: true }) }),
903
+ ...(repositoryContext === undefined ? {} : { repositoryContext }),
904
+ raw: body,
905
+ };
906
+ }
907
+
908
+ // ---------------------------------------------------------------------------
909
+ // projection/v1 — reused verbatim; the v2 capabilities schema still advertises
910
+ // gentle-ai.review-integration.projection/v1, so renaming this decoder would
911
+ // be the same kind of lie the module rename is meant to remove.
912
+ // ---------------------------------------------------------------------------
913
+
914
+ export function decodeReviewProjectionV1(value ) {
915
+ const projection = exactRecord(value, "status.projection", ["schema", "kind", "projection", "base_tree", "initial_review_tree", "current_candidate_tree", "paths_digest", "paths", "intended_untracked", "intended_untracked_proof", "initial_snapshot_identity", "current_snapshot_identity"]);
916
+ if (projection.schema !== "gentle-ai.review-integration.projection/v1") throw new TypeError("status.projection schema is incompatible");
917
+ return {
918
+ schema: "gentle-ai.review-integration.projection/v1",
919
+ kind: enumeration(projection.kind, Object.values(REVIEW_PROJECTION_KIND), "status.projection.kind"),
920
+ projection: enumeration(projection.projection, REQUIRED_PROJECTIONS, "status.projection.projection"),
921
+ baseTree: gitTree(projection.base_tree, "status.projection.base_tree"),
922
+ initialReviewTree: gitTree(projection.initial_review_tree, "status.projection.initial_review_tree"),
923
+ currentCandidateTree: gitTree(projection.current_candidate_tree, "status.projection.current_candidate_tree"),
924
+ pathsDigest: sha256(projection.paths_digest, "status.projection.paths_digest"),
925
+ paths: array(projection.paths, "status.projection.paths", safePath, { unique: true }),
926
+ intendedUntracked: array(projection.intended_untracked, "status.projection.intended_untracked", safePath, { unique: true }),
927
+ intendedUntrackedProof: sha256(projection.intended_untracked_proof, "status.projection.intended_untracked_proof"),
928
+ initialSnapshotIdentity: sha256(projection.initial_snapshot_identity, "status.projection.initial_snapshot_identity"),
929
+ currentSnapshotIdentity: sha256(projection.current_snapshot_identity, "status.projection.current_snapshot_identity"),
930
+ };
931
+ }
932
+
933
+ // ---------------------------------------------------------------------------
934
+ // authority-repair-assessment/v1 — net-new. Consumed as status.repair AND
935
+ // repair.assessment.
936
+ // ---------------------------------------------------------------------------
937
+
938
+ function decodeAuthorityRepairAssessmentCounts(value , label ) {
939
+ const body = exactRecord(value, label, ["lineages", "compact_lineages", "legacy_lineages", "events", "bytes", "eligible_candidates", "unsupported_lineages", "conflicts"]);
940
+ return {
941
+ lineages: integer(body.lineages, `${label}.lineages`, 0, 256),
942
+ compactLineages: integer(body.compact_lineages, `${label}.compact_lineages`, 0, 256),
943
+ legacyLineages: integer(body.legacy_lineages, `${label}.legacy_lineages`, 0, 256),
944
+ events: integer(body.events, `${label}.events`, 0, 1024),
945
+ bytes: integer(body.bytes, `${label}.bytes`, 0, 8_388_608),
946
+ eligibleCandidates: integer(body.eligible_candidates, `${label}.eligible_candidates`, 0, 256),
947
+ unsupportedLineages: integer(body.unsupported_lineages, `${label}.unsupported_lineages`, 0, 1024),
948
+ conflicts: integer(body.conflicts, `${label}.conflicts`, 0, 1024),
949
+ };
950
+ }
951
+
952
+ export function decodeAuthorityRepairAssessmentV1(value ) {
953
+ const label = "assessment";
954
+ const body = exactRecord(value, label, ["schema", "status", "counts", "supported_operations", "authorization_schema"], ["class", "cause", "disposition", "repository_binding", "candidate"]);
955
+ if (body.schema !== "gentle-ai.review-authority-repair-assessment/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-authority-repair-assessment/v1`);
956
+ const status = enumeration(body.status, ["eligible", "unsupported", "ambiguous", "conflicting", "truncated"] , `${label}.status`);
957
+
958
+ const eligibleFields = ["class", "cause", "disposition", "repository_binding", "candidate"] ;
959
+ const eligiblePresent = eligibleFields.filter((field) => body[field] !== undefined);
960
+ if (status === "eligible") {
961
+ if (eligiblePresent.length !== eligibleFields.length) throw new TypeError(`${label} eligible status requires class, cause, disposition, repository_binding, and candidate`);
962
+ } else if (eligiblePresent.length > 0) {
963
+ throw new TypeError(`${label} non-eligible status cannot expose class, cause, disposition, repository_binding, or candidate`);
964
+ }
965
+ if (body.class !== undefined && body.class !== "legacy_v1_historical_alias") throw new TypeError(`${label}.class is unsupported`);
966
+ if (body.cause !== undefined && body.cause !== "unsupported_historical_v1_operation_alias") throw new TypeError(`${label}.cause is unsupported`);
967
+ if (body.disposition !== undefined && body.disposition !== "quarantine-approved-historical-alias") throw new TypeError(`${label}.disposition is unsupported`);
968
+
969
+ let candidate ;
970
+ if (body.candidate !== undefined) {
971
+ const source = exactRecord(body.candidate, `${label}.candidate`, ["lineage_id", "revision", "chain_identity", "event_count", "alias_event_count", "operations"]);
972
+ candidate = {
973
+ lineageId: lineage(source.lineage_id, `${label}.candidate.lineage_id`),
974
+ revision: sha256(source.revision, `${label}.candidate.revision`),
975
+ chainIdentity: sha256(source.chain_identity, `${label}.candidate.chain_identity`),
976
+ eventCount: integer(source.event_count, `${label}.candidate.event_count`, 2, 1024),
977
+ aliasEventCount: integer(source.alias_event_count, `${label}.candidate.alias_event_count`, 1, 1024),
978
+ operations: enumArray(source.operations, ["review/complete-fix", "review/validate-fix"] , `${label}.candidate.operations`, { minimum: 1, maximum: 2, unique: true }),
979
+ };
980
+ }
981
+
982
+ const counts = decodeAuthorityRepairAssessmentCounts(body.counts, `${label}.counts`);
983
+ if (status === "eligible" && (counts.eligibleCandidates !== 1 || counts.unsupportedLineages !== 0 || counts.conflicts !== 0)) {
984
+ throw new TypeError(`${label}.counts is incompatible with eligible status`);
985
+ }
986
+
987
+ const supportedOperations = array(body.supported_operations, `${label}.supported_operations`, (entry, entryLabel) => enumeration(entry, ["review/complete-fix", "review/validate-fix"] , entryLabel), { minimum: 2, maximum: 2 });
988
+ if (supportedOperations[0] !== "review/complete-fix" || supportedOperations[1] !== "review/validate-fix") throw new TypeError(`${label}.supported_operations is out of order`);
989
+ if (body.authorization_schema !== "gentle-ai.review-repair-authorization/v1") throw new TypeError(`${label}.authorization_schema must be gentle-ai.review-repair-authorization/v1`);
990
+
991
+ return {
992
+ schema: "gentle-ai.review-authority-repair-assessment/v1",
993
+ status,
994
+ ...(body.class === undefined ? {} : { class: "legacy_v1_historical_alias" }),
995
+ ...(body.cause === undefined ? {} : { cause: "unsupported_historical_v1_operation_alias" }),
996
+ ...(body.disposition === undefined ? {} : { disposition: "quarantine-approved-historical-alias" }),
997
+ ...(body.repository_binding === undefined ? {} : { repositoryBinding: sha256(body.repository_binding, `${label}.repository_binding`) }),
998
+ ...(candidate === undefined ? {} : { candidate }),
999
+ counts,
1000
+ supportedOperations: supportedOperations ,
1001
+ authorizationSchema: "gentle-ai.review-repair-authorization/v1",
1002
+ };
1003
+ }
1004
+
1005
+ // ---------------------------------------------------------------------------
1006
+ // targeted-validation-request/v1 — reused for status.validation_request,
1007
+ // operation.result.validation_request, and next_transition collect inputs.
1008
+ // ---------------------------------------------------------------------------
1009
+
1010
+ function decodeTargetedValidationRequestV1(value , label ) {
1011
+ const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "fix_finding_ids", "projection", "correction_candidate_tree", "correction_target_identity", "correction_paths", "correction_paths_digest"]);
1012
+ if (body.schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1013
+ return {
1014
+ schema: "gentle-ai.review-targeted-validation-request/v1",
1015
+ requestHash: sha256(body.request_hash, `${label}.request_hash`),
1016
+ lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
1017
+ expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
1018
+ targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1019
+ fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
1020
+ projection: enumeration(body.projection, REQUIRED_PROJECTIONS, `${label}.projection`),
1021
+ correctionCandidateTree: gitTree(body.correction_candidate_tree, `${label}.correction_candidate_tree`),
1022
+ correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
1023
+ correctionPaths: stringArray(body.correction_paths, `${label}.correction_paths`, { minimum: 1, unique: true }),
1024
+ correctionPathsDigest: sha256(body.correction_paths_digest, `${label}.correction_paths_digest`),
1025
+ };
1026
+ }
1027
+
1028
+ // ---------------------------------------------------------------------------
1029
+ // next-transition/v3 — net-new; unlike v1's decodeNextTransition (void),
1030
+ // this decoder returns a typed value so callers can read the manifest-bound
1031
+ // collect inputs and the execute binding.
1032
+ // ---------------------------------------------------------------------------
1033
+
1034
+ const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"] ;
1035
+
1036
+ function decodeTransitionArguments(value , label ) {
1037
+ return array(value, label, (entry, entryLabel) => {
1038
+ const argument = exactRecord(entry, entryLabel, ["name", "value"], ["token"]);
1039
+ const name = text(argument.name, `${entryLabel}.name`, { minimum: 1, pattern: /^[a-z0-9_-]+$/ });
1040
+ const argumentValue = text(argument.value, `${entryLabel}.value`, { minimum: 1 });
1041
+ const token = argument.token === undefined ? undefined : text(argument.token, `${entryLabel}.token`, { minimum: 1 });
1042
+ return { name, value: argumentValue, ...(token === undefined ? {} : { token }) };
1043
+ });
1044
+ }
1045
+
1046
+ function decodeCollectInput(value , label ) {
1047
+ const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "validation_request"]);
1048
+ const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1049
+ const schema = nonempty(input.schema, `${label}.schema`);
1050
+ const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
1051
+ const argumentsList = decodeTransitionArguments(input.arguments, `${label}.arguments`);
1052
+
1053
+ if (captureOperation === "external.run_targeted_validation") {
1054
+ if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1055
+ if (schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1056
+ } else if (input.validation_request !== undefined) {
1057
+ throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation`);
1058
+ }
1059
+
1060
+ if (captureOperation === "review.capture-result") {
1061
+ if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined || input.changed_path_manifest === undefined) {
1062
+ throw new TypeError(`${label} requires artifact_subject, base_tree, candidate_tree, and changed_path_manifest`);
1063
+ }
1064
+ if (schema !== "https://gentle-ai.dev/schema/review/reviewer/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/reviewer/v1`);
1065
+ } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
1066
+ throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
1067
+ }
1068
+
1069
+ return {
1070
+ name,
1071
+ schema,
1072
+ captureOperation,
1073
+ arguments: argumentsList,
1074
+ ...(input.artifact_subject === undefined ? {} : { artifactSubject: decodeArtifactSubject(input.artifact_subject, `${label}.artifact_subject`) }),
1075
+ ...(input.base_tree === undefined ? {} : { baseTree: gitTree(input.base_tree, `${label}.base_tree`) }),
1076
+ ...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
1077
+ ...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
1078
+ ...(input.validation_request === undefined ? {} : { validationRequest: decodeTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`) }),
1079
+ };
1080
+ }
1081
+
1082
+ export function decodeReviewNextTransitionV3(value ) {
1083
+ const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect"]);
1084
+ const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
1085
+ const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1086
+
1087
+ if (kind === "execute") {
1088
+ // `command` is an optional, ready-to-paste rendering of `arguments` (the
1089
+ // exact same binding as a single shell-ready string) — observed live
1090
+ // against a real v2.2.2 review run but absent from the mirrored fixture,
1091
+ // which only exercises the `collect` variant of this envelope. Carried
1092
+ // through untyped-but-validated rather than dropped, matching how this
1093
+ // module already treats every other provider-owned convenience field.
1094
+ // status-v2.schema.json $defs.transition_execution declares optional
1095
+ // `command`, `selector_arguments`, AND `artifacts`. Declaring only the
1096
+ // first rejected two real transitions -- captured_results_ready carries
1097
+ // artifacts, approved_receipt_ready carries selector_arguments -- while
1098
+ // every native call had already succeeded and authority had advanced.
1099
+ const execute = exactRecord(transition.execute, "next_transition.execute", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
1100
+ const operation = enumeration(execute.operation, NEXT_TRANSITION_OPERATIONS, "next_transition.execute.operation");
1101
+ const argumentsList = decodeTransitionArguments(execute.arguments, "next_transition.execute.arguments");
1102
+ const preconditions = decodeTransitionArguments(execute.preconditions, "next_transition.execute.preconditions");
1103
+ // The schema gives `binding` no declared properties and no required list:
1104
+ // it is an OPEN object. Closing it here made Pi stricter than the
1105
+ // contract it implements and rejected the provider's repository_context.
1106
+ // target_identity stays required because Pi reads it.
1107
+ const binding = exactRecord(execute.binding, "next_transition.execute.binding", ["target_identity"], ["lineage_id", "revision"], true);
1108
+ const targetIdentity = sha256(binding.target_identity, "next_transition.execute.binding.target_identity");
1109
+ const lineageId = binding.lineage_id === undefined ? undefined : lineage(binding.lineage_id, "next_transition.execute.binding.lineage_id");
1110
+ const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
1111
+ const command = execute.command === undefined ? undefined : nonempty(execute.command, "next_transition.execute.command");
1112
+ if (transition.collect !== undefined) throw new TypeError("next_transition.collect is incompatible with execute");
1113
+ return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) } };
1114
+ }
1115
+ if (kind === "collect") {
1116
+ const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
1117
+ const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label), { minimum: 1 });
1118
+ if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
1119
+ return { kind, reasonCode, collect: { inputs } };
1120
+ }
1121
+ if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
1122
+ return { kind, reasonCode };
1123
+ }
1124
+
1125
+ // ---------------------------------------------------------------------------
1126
+ // eligibility — decoded for validation, discarded (not exposed on the typed
1127
+ // status/finalize shapes), matching lib/review-integration-v1.mjs's pattern.
1128
+ // Unlike v1's decoder, this one accepts disposition+binding for BOTH
1129
+ // review.recover and review.retry_final_verification, matching the mirrored
1130
+ // status-v2.schema.json action_eligibility definition v2 also $refs.
1131
+ // ---------------------------------------------------------------------------
1132
+
1133
+ const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.finalize", "review.validate", "review.recover", "review.repair", "review.retry_final_verification"] ;
1134
+ const FORBIDDEN_ACTIONS = ["review.abandon", "review.finalize", "review.invalidate", "review.quarantine-legacy", "review.reclaim", "review.reconcile-authority", "review.reconcile-authority-batch", "review.recover", "review.repair", "review.retry_final_verification", "review.start", "review.validate"] ;
1135
+
1136
+ function decodeEligibility(value , label ) {
1137
+ const eligibility = exactRecord(value, label, ["allowed_actions", "forbidden_actions"]);
1138
+ array(eligibility.allowed_actions, `${label}.allowed_actions`, (entry, entryLabel) => {
1139
+ const action = exactRecord(entry, entryLabel, ["action", "reason_code", "required_inputs"], ["disposition", "binding"]);
1140
+ const selected = enumeration(action.action, ELIGIBLE_ACTIONS, `${entryLabel}.action`);
1141
+ text(action.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1142
+ array(action.required_inputs, `${entryLabel}.required_inputs`, (input, inputLabel) => text(input, inputLabel, { minimum: 1, pattern: /^[a-z0-9_]+$/ }), { unique: true });
1143
+ if (selected === "review.recover" || selected === "review.retry_final_verification") {
1144
+ const binding = exactRecord(action.binding, `${entryLabel}.binding`, ["lineage_id", "revision", "target_identity"]);
1145
+ enumeration(action.disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), `${entryLabel}.disposition`);
1146
+ lineage(binding.lineage_id, `${entryLabel}.binding.lineage_id`);
1147
+ sha256(binding.revision, `${entryLabel}.binding.revision`);
1148
+ sha256(binding.target_identity, `${entryLabel}.binding.target_identity`);
1149
+ } else if (action.disposition !== undefined || action.binding !== undefined) {
1150
+ throw new TypeError(`${entryLabel} recovery fields require review.recover or review.retry_final_verification`);
1151
+ }
1152
+ return action;
1153
+ }, { minimum: 1, maximum: 1 });
1154
+ array(eligibility.forbidden_actions, `${label}.forbidden_actions`, (entry, entryLabel) => {
1155
+ const action = exactRecord(entry, entryLabel, ["action", "reason_code"]);
1156
+ enumeration(action.action, FORBIDDEN_ACTIONS, `${entryLabel}.action`);
1157
+ text(action.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1158
+ return action;
1159
+ });
1160
+ }
1161
+
1162
+ function decodeFinalVerificationRetry(value , label ) {
1163
+ const body = exactRecord(value, label, ["incident_schema", "incident_class", "validating_revision", "target_identity", "failed_evidence_hash", "finalize_request_digest"], ["failed_evidence_record_digest"]);
1164
+ if (body.incident_schema !== "gentle-ai.review-final-verification-incident/v1") throw new TypeError(`${label}.incident_schema is unsupported`);
1165
+ if (body.incident_class !== "procedural_tooling_failure") throw new TypeError(`${label}.incident_class is unsupported`);
1166
+ return {
1167
+ incidentSchema: "gentle-ai.review-final-verification-incident/v1",
1168
+ incidentClass: "procedural_tooling_failure",
1169
+ validatingRevision: sha256(body.validating_revision, `${label}.validating_revision`),
1170
+ targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1171
+ failedEvidenceHash: sha256(body.failed_evidence_hash, `${label}.failed_evidence_hash`),
1172
+ ...(body.failed_evidence_record_digest === undefined ? {} : { failedEvidenceRecordDigest: sha256(body.failed_evidence_record_digest, `${label}.failed_evidence_record_digest`) }),
1173
+ finalizeRequestDigest: sha256(body.finalize_request_digest, `${label}.finalize_request_digest`),
1174
+ };
1175
+ }
1176
+
1177
+ // ---------------------------------------------------------------------------
1178
+ // status/v3
1179
+ // ---------------------------------------------------------------------------
1180
+
1181
+ export function decodeReviewStatusV3(value ) {
1182
+ const body = exactRecord(value, "status", [
1183
+ "schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1184
+ ], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry"]);
1185
+ requireIdentity(body, "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1186
+
1187
+ const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] , "status.applicability");
1188
+ const receiptBody = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
1189
+ const receiptStatus = enumeration(receiptBody.status, RECEIPT_STATUSES, "status.receipt.status");
1190
+ const receipt = { status: receiptStatus, ...(receiptBody.identity === undefined ? {} : { identity: sha256(receiptBody.identity, "status.receipt.identity") }) };
1191
+
1192
+ let authority ;
1193
+ if (body.authority !== undefined) {
1194
+ const source = exactRecord(body.authority, "status.authority", ["version", "lineage_id", "state", "generation", "revision"]);
1195
+ authority = {
1196
+ version: enumeration(source.version, Object.values(REVIEW_AUTHORITY_VERSION), "status.authority.version"),
1197
+ lineageId: lineage(source.lineage_id, "status.authority.lineage_id"),
1198
+ state: nonempty(source.state, "status.authority.state"),
1199
+ generation: integer(source.generation, "status.authority.generation", 1),
1200
+ revision: sha256(source.revision, "status.authority.revision"),
1201
+ };
1202
+ }
1203
+ if (applicability === REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET && authority === undefined) throw new TypeError("current_target status requires authority");
1204
+ if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET && (authority !== undefined || body.frozen !== undefined || body.authority_target_identity !== undefined)) {
1205
+ throw new TypeError("non-current status cannot expose authority, frozen, or authority_target_identity");
1206
+ }
1207
+
1208
+ let frozen ;
1209
+ if (body.frozen !== undefined) {
1210
+ const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"]);
1211
+ frozen = {
1212
+ tier: enumeration(source.tier, RISK_LEVELS, "status.frozen.tier"),
1213
+ originalChangedLines: integer(source.original_changed_lines, "status.frozen.original_changed_lines"),
1214
+ correctionBudget: integer(source.correction_budget, "status.frozen.correction_budget", 0, 200),
1215
+ };
1216
+ }
1217
+ if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
1218
+ if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && (frozen !== undefined || body.authority_target_identity !== undefined)) throw new TypeError("legacy status cannot expose frozen metadata or authority_target_identity");
1219
+ if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && receiptStatus !== "expected_missing" && receiptStatus !== "present") throw new TypeError("legacy status receipt is incompatible");
1220
+
1221
+ const action = enumeration(body.action, STATUS_ACTIONS, "status.action");
1222
+ const actionDisposition = body.action_disposition === undefined ? undefined : enumeration(body.action_disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), "status.action_disposition");
1223
+ if ((action === "recover" || action === "retry_final_verification") && actionDisposition === undefined) throw new TypeError(`${action} status requires action_disposition`);
1224
+ if (action !== "recover" && action !== "retry_final_verification" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover or retry_final_verification action");
1225
+ if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
1226
+ const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
1227
+ const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
1228
+
1229
+ let reconciliation ;
1230
+ if (action === "reconcile_finalize") {
1231
+ if (body.reconciliation === undefined) throw new TypeError("reconcile_finalize status requires reconciliation");
1232
+ const source = exactRecord(body.reconciliation, "status.reconciliation", ["required"]);
1233
+ if (source.required !== true) throw new TypeError("status.reconciliation.required must be true");
1234
+ if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET) throw new TypeError("reconcile_finalize status requires current_target applicability");
1235
+ if (replayability !== REVIEW_REPLAYABILITY.STATUS_REQUIRED) throw new TypeError("reconcile_finalize status requires status_required replayability");
1236
+ reconciliation = { required: true };
1237
+ } else if (body.reconciliation !== undefined) {
1238
+ throw new TypeError("status.reconciliation is only valid for the reconcile_finalize action");
1239
+ }
1240
+
1241
+ let validationRequest ;
1242
+ if (body.validation_request !== undefined) {
1243
+ if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.state !== "correction_required") {
1244
+ throw new TypeError("status.validation_request requires current_target applicability and correction_required authority state");
1245
+ }
1246
+ validationRequest = decodeTargetedValidationRequestV1(body.validation_request, "status.validation_request");
1247
+ }
1248
+
1249
+ let finalVerificationRetry ;
1250
+ if (action === "retry_final_verification") {
1251
+ if (body.final_verification_retry === undefined) throw new TypeError("retry_final_verification status requires final_verification_retry");
1252
+ if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.version !== REVIEW_AUTHORITY_VERSION.COMPACT_V2 || authority?.state !== "escalated") {
1253
+ throw new TypeError("retry_final_verification status requires current_target compact-v2 escalated authority");
1254
+ }
1255
+ if (actionDisposition !== REVIEW_STATUS_ACTION_DISPOSITION.FINAL_VERIFICATION_RETRY) throw new TypeError("retry_final_verification status requires final_verification_retry disposition");
1256
+ finalVerificationRetry = decodeFinalVerificationRetry(body.final_verification_retry, "status.final_verification_retry");
1257
+ } else if (body.final_verification_retry !== undefined) {
1258
+ throw new TypeError("status.final_verification_retry is only valid for the retry_final_verification action");
1259
+ }
1260
+
1261
+ return {
1262
+ contract: REVIEW_INTEGRATION_CONTRACT,
1263
+ applicability,
1264
+ ...(authority === undefined ? {} : { authority }),
1265
+ receipt,
1266
+ action,
1267
+ ...(actionDisposition === undefined ? {} : { actionDisposition }),
1268
+ replayability,
1269
+ ...(frozen === undefined ? {} : { frozen }),
1270
+ ...(reconciliation === undefined ? {} : { reconciliation }),
1271
+ targetIdentity: sha256(body.target_identity, "status.target_identity"),
1272
+ ...(body.authority_target_identity === undefined ? {} : { authorityTargetIdentity: sha256(body.authority_target_identity, "status.authority_target_identity") }),
1273
+ projection: decodeReviewProjectionV1(body.projection),
1274
+ repair: decodeAuthorityRepairAssessmentV1(body.repair),
1275
+ candidates: stringArray(body.candidates, "status.candidates", { unique: true, pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ }),
1276
+ ...(nextTransition === undefined ? {} : { nextTransition }),
1277
+ ...(validationRequest === undefined ? {} : { validationRequest }),
1278
+ ...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
1279
+ raw: body,
1280
+ };
1281
+ }
1282
+
1283
+ // ---------------------------------------------------------------------------
1284
+ // consent/v2 — net-new. The typed per-candidate blocking consent question
1285
+ // emitted by negotiated v2 START when the caller declared --consent relay.
1286
+ // ---------------------------------------------------------------------------
1287
+
1288
+ function decodeConsentChoice(value , label , answer ) {
1289
+ const body = exactRecord(value, label, ["answer", "label", "effect", "invocation"]);
1290
+ if (body.answer !== answer) throw new TypeError(`${label}.answer must be ${answer}`);
1291
+ return {
1292
+ answer,
1293
+ label: nonempty(body.label, `${label}.label`),
1294
+ effect: nonempty(body.effect, `${label}.effect`),
1295
+ invocation: text(body.invocation, `${label}.invocation`, { pattern: new RegExp(`^gentle-ai review start --contract gentle-ai\\.review-integration/v2 .* --consent ${answer}$`) }),
1296
+ };
1297
+ }
1298
+
1299
+ export function decodeReviewConsentV2(value ) {
1300
+ const body = exactRecord(value, "consent", [
1301
+ "schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
1302
+ ]);
1303
+ requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
1304
+ if (body.action !== "consent_required") throw new TypeError("consent.action must be consent_required");
1305
+ if (body.blocking !== true) throw new TypeError("consent.blocking must be true");
1306
+
1307
+ const choicesArray = body.choices;
1308
+ if (!Array.isArray(choicesArray) || choicesArray.length !== 2) throw new TypeError("consent.choices must have exactly 2 items");
1309
+ const granted = decodeConsentChoice(choicesArray[0], "consent.choices[0]", "granted");
1310
+ const declined = decodeConsentChoice(choicesArray[1], "consent.choices[1]", "declined");
1311
+
1312
+ const offPathSource = exactRecord(body.off_path, "consent.off_path", ["note", "command"]);
1313
+ if (offPathSource.command !== "gentle-ai review mode disable") throw new TypeError("consent.off_path.command is unsupported");
1314
+
1315
+ return {
1316
+ schema: "gentle-ai.review-integration.consent/v2",
1317
+ contract: REVIEW_INTEGRATION_CONTRACT,
1318
+ operation: "review.start",
1319
+ action: "consent_required",
1320
+ blocking: true,
1321
+ targetIdentity: sha256(body.target_identity, "consent.target_identity"),
1322
+ projection: enumeration(body.projection, REQUIRED_PROJECTIONS, "consent.projection"),
1323
+ riskLevel: enumeration(body.risk_level, ["medium", "high"] , "consent.risk_level"),
1324
+ changedFiles: integer(body.changed_files, "consent.changed_files"),
1325
+ changedLines: integer(body.changed_lines, "consent.changed_lines"),
1326
+ headline: nonempty(body.headline, "consent.headline"),
1327
+ reason: nonempty(body.reason, "consent.reason"),
1328
+ value: nonempty(body.value, "consent.value"),
1329
+ riskEvidence: stringArray(body.risk_evidence, "consent.risk_evidence"),
1330
+ choices: [granted, declined],
1331
+ offPath: { note: nonempty(offPathSource.note, "consent.off_path.note"), command: "gentle-ai review mode disable" },
1332
+ raw: body,
1333
+ };
1334
+ }
1335
+
1336
+ // ---------------------------------------------------------------------------
1337
+ // failure/v2
1338
+ // ---------------------------------------------------------------------------
1339
+
1340
+ function decodeOptionalSha256(value , label ) {
1341
+ if (value === "") return "";
1342
+ return sha256(value, label);
1343
+ }
1344
+
1345
+ function decodeFailureBindingRevision(value , label ) {
1346
+ const body = exactRecord(value, label, ["expected", "current"]);
1347
+ return { expected: decodeOptionalSha256(body.expected, `${label}.expected`), current: decodeOptionalSha256(body.current, `${label}.current`) };
1348
+ }
1349
+
1350
+ function decodeFailureTargetEvidence(value , label ) {
1351
+ const evidence = exactRecord(value, label, ["candidate_tree", "paths_digest"]);
1352
+ return {
1353
+ candidateTree: gitTree(evidence.candidate_tree, `${label}.candidate_tree`),
1354
+ pathsDigest: sha256(evidence.paths_digest, `${label}.paths_digest`),
1355
+ };
1356
+ }
1357
+
1358
+ function decodeFailureScopeChange(value , label ) {
1359
+ const scope = exactRecord(value, label, ["expected", "actual", "differing_path_count", "differing_paths_digest", "predecessor_lineage_id", "predecessor_revision", "recovery_operation", "recovery_required_inputs"]);
1360
+ if (scope.recovery_operation !== "review.recover") throw new TypeError(`${label}.recovery_operation is unsupported`);
1361
+ const recoveryInputs = stringArray(scope.recovery_required_inputs, `${label}.recovery_required_inputs`, { minimum: 6, maximum: 6 });
1362
+ const expectedRecoveryInputs = ["predecessor_lineage_id", "expected_predecessor_revision", "successor_lineage_id", "disposition", "reason", "actor"];
1363
+ if (recoveryInputs.some((input, index) => input !== expectedRecoveryInputs[index])) throw new TypeError(`${label}.recovery_required_inputs is unsupported`);
1364
+ return {
1365
+ expected: decodeFailureTargetEvidence(scope.expected, `${label}.expected`),
1366
+ actual: decodeFailureTargetEvidence(scope.actual, `${label}.actual`),
1367
+ differingPathCount: integer(scope.differing_path_count, `${label}.differing_path_count`, 0, 1_000_000),
1368
+ differingPathsDigest: sha256(scope.differing_paths_digest, `${label}.differing_paths_digest`),
1369
+ predecessorLineageId: lineage(scope.predecessor_lineage_id, `${label}.predecessor_lineage_id`),
1370
+ predecessorRevision: sha256(scope.predecessor_revision, `${label}.predecessor_revision`),
1371
+ recoveryOperation: "review.recover",
1372
+ recoveryRequiredInputs: recoveryInputs,
1373
+ };
1374
+ }
1375
+
1376
+ // The mirrored gate_context is `oneOf` [scope_change, binding_revision]. v1's
1377
+ // decoder only ever implemented scope_change; this decoder implements both
1378
+ // branches so a legitimate binding_revision failure payload is not rejected.
1379
+ function decodeFailureContext(value , label ) {
1380
+ const context = exactRecord(value, label, [], ["scope_change", "binding_revision"]);
1381
+ const hasScope = context.scope_change !== undefined;
1382
+ const hasBinding = context.binding_revision !== undefined;
1383
+ if (hasScope === hasBinding) throw new TypeError(`${label} requires exactly one of scope_change or binding_revision`);
1384
+ return {
1385
+ ...(hasScope ? { scopeChange: decodeFailureScopeChange(context.scope_change, `${label}.scope_change`) } : {}),
1386
+ ...(hasBinding ? { bindingRevision: decodeFailureBindingRevision(context.binding_revision, `${label}.binding_revision`) } : {}),
1387
+ };
1388
+ }
1389
+
1390
+ export function decodeReviewFailureV2(value ) {
1391
+ const body = exactRecord(value, "failure", [
1392
+ "schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
1393
+ ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
1394
+ requireIdentity(body, "gentle-ai.review-integration.failure/v2");
1395
+ const operation = enumeration(body.operation, REQUIRED_OPERATIONS, "failure.operation");
1396
+
1397
+ if (body.progress_identity !== undefined) {
1398
+ if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
1399
+ if (operation !== REVIEW_INTEGRATION_OPERATION.REPAIR) throw new TypeError("failure.progress_identity requires operation review.repair");
1400
+ }
1401
+ if (body.request_digest !== undefined && operation === REVIEW_INTEGRATION_OPERATION.REPAIR && body.progress_identity === undefined) {
1402
+ throw new TypeError("failure.request_digest with review.repair requires progress_identity");
1403
+ }
1404
+
1405
+ return {
1406
+ schema: "gentle-ai.review-integration.failure/v2",
1407
+ contract: REVIEW_INTEGRATION_CONTRACT,
1408
+ operation,
1409
+ phase: enumeration(body.phase, ["preflight", "pre_native", "native_running", "native_committed", "reconciliation"] , "failure.phase"),
1410
+ code: text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }),
1411
+ message: text(body.message, "failure.message", { minimum: 1, maximum: 240, pattern: /^[^\r\n]+$/ }),
1412
+ mutationOutcome: enumeration(body.mutation_outcome, Object.values(REVIEW_MUTATION_OUTCOME), "failure.mutation_outcome"),
1413
+ authorityApplicability: enumeration(body.authority_applicability, Object.values(REVIEW_AUTHORITY_APPLICABILITY), "failure.authority_applicability"),
1414
+ retrySafe: boolean(body.retry_safe, "failure.retry_safe"),
1415
+ replayability: enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "failure.replayability"),
1416
+ ...(body.lineage_id === undefined ? {} : { lineageId: lineage(body.lineage_id, "failure.lineage_id") }),
1417
+ ...(body.request_digest === undefined ? {} : { requestDigest: sha256(body.request_digest, "failure.request_digest") }),
1418
+ ...(body.progress_identity === undefined ? {} : { progressIdentity: sha256(body.progress_identity, "failure.progress_identity") }),
1419
+ requiredInputs: enumArray(body.required_inputs, FAILURE_REQUIRED_INPUTS, "failure.required_inputs", { unique: true }),
1420
+ nextAction: enumeration(body.next_action, FAILURE_NEXT_ACTIONS, "failure.next_action"),
1421
+ ...(body.cause_category === undefined ? {} : { causeCategory: text(body.cause_category, "failure.cause_category", { minimum: 1, pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }) }),
1422
+ ...(body.cause === undefined ? {} : { cause: text(body.cause, "failure.cause", { minimum: 1, maximum: 4000, pattern: /^[^\r\n]+$/ }) }),
1423
+ ...(body.context === undefined ? {} : { context: decodeFailureContext(body.context, "failure.context") }),
1424
+ raw: body,
1425
+ };
1426
+ }
1427
+
1428
+ // ---------------------------------------------------------------------------
1429
+ // operation/v2
1430
+ // ---------------------------------------------------------------------------
1431
+
1432
+ export function decodeReviewOperationV2(value ) {
1433
+ const body = exactRecord(value, "operation", ["schema", "contract", "operation", "result"]);
1434
+ requireIdentity(body, "gentle-ai.review-integration.operation/v2");
1435
+ const operation = enumeration(body.operation, ["review.finalize", "review.validate", "review.bind_sdd", "review.retry_final_verification"] , "operation.operation");
1436
+ let result ;
1437
+ if (operation === REVIEW_INTEGRATION_OPERATION.FINALIZE) {
1438
+ result = exactRecord(body.result, "operation.result", ["operation", "lineage_id", "state", "action", "store_revision"], ["eligibility", "next_transition", "validation_request", "escalation"]);
1439
+ if (result.operation !== "review/finalize") throw new TypeError("operation.result does not match review.finalize");
1440
+ nonempty(result.lineage_id, "operation.result.lineage_id");
1441
+ nonempty(result.state, "operation.result.state");
1442
+ nonempty(result.action, "operation.result.action");
1443
+ sha256(result.store_revision, "operation.result.store_revision");
1444
+ if (result.eligibility !== undefined) decodeEligibility(result.eligibility, "operation.result.eligibility");
1445
+ if (result.next_transition !== undefined) decodeReviewNextTransitionV3(result.next_transition);
1446
+ if (result.validation_request !== undefined) {
1447
+ decodeTargetedValidationRequestV1(result.validation_request, "operation.result.validation_request");
1448
+ if (result.state !== "correction_required") throw new TypeError("operation.result.validation_request requires state correction_required");
1449
+ }
1450
+ if (result.escalation !== undefined) nonempty(result.escalation, "operation.result.escalation");
1451
+ } else if (operation === REVIEW_INTEGRATION_OPERATION.VALIDATE) {
1452
+ result = exactRecord(body.result, "operation.result", ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
1453
+ if (result.schema !== "gentle-ai.review-gate-result/v1") throw new TypeError("operation.result does not match review.validate");
1454
+ enumeration(result.result, ["allow", "scope-changed", "invalidated", "escalated"] , "operation.result.result");
1455
+ boolean(result.allowed, "operation.result.allowed");
1456
+ nonempty(result.action, "operation.result.action");
1457
+ nonempty(result.reason, "operation.result.reason");
1458
+ record(result.context, "operation.result.context");
1459
+ if (result.delivery !== undefined && result.delivery !== "disabled/unmanaged") throw new TypeError("operation.result.delivery is unsupported");
1460
+ } else if (operation === REVIEW_INTEGRATION_OPERATION.BIND_SDD) {
1461
+ result = exactRecord(body.result, "operation.result", ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
1462
+ if (result.schema !== "gentle-ai.sdd-review-binding/v1") throw new TypeError("operation.result does not match review.bind_sdd");
1463
+ sha256(result.revision, "operation.result.revision");
1464
+ nonempty(result.change, "operation.result.change");
1465
+ nonempty(result.lineage, "operation.result.lineage");
1466
+ sha256(result.authority_revision, "operation.result.authority_revision");
1467
+ sha256(result.receipt_hash, "operation.result.receipt_hash");
1468
+ record(result.gate_context, "operation.result.gate_context");
1469
+ } else {
1470
+ result = exactRecord(body.result, "operation.result", ["operation", "predecessor_lineage_id", "predecessor_revision", "lineage_id", "state", "store_revision", "target_identity", "incident_digest", "recovery_disposition"]);
1471
+ if (result.operation !== "review.retry_final_verification") throw new TypeError("operation.result does not match review.retry_final_verification");
1472
+ lineage(result.predecessor_lineage_id, "operation.result.predecessor_lineage_id");
1473
+ sha256(result.predecessor_revision, "operation.result.predecessor_revision");
1474
+ lineage(result.lineage_id, "operation.result.lineage_id");
1475
+ if (result.state !== "validating") throw new TypeError("operation.result.state must be validating");
1476
+ sha256(result.store_revision, "operation.result.store_revision");
1477
+ sha256(result.target_identity, "operation.result.target_identity");
1478
+ sha256(result.incident_digest, "operation.result.incident_digest");
1479
+ if (result.recovery_disposition !== "final_verification_retry") throw new TypeError("operation.result.recovery_disposition must be final_verification_retry");
1480
+ }
1481
+ return { contract: REVIEW_INTEGRATION_CONTRACT, operation, result, raw: body };
1482
+ }
1483
+
1484
+ // ---------------------------------------------------------------------------
1485
+ // repair/v2 — net-new. Encodes the two conditional allOf invariants from v1
1486
+ // repair.schema.json:53-157: execute mode requires execution (and forbids
1487
+ // provider_inputs / non-empty required_inputs); an eligible preflight
1488
+ // assessment requires provider_inputs and required_inputs exactly
1489
+ // [actor, reason, maintainer_authorization] in that order.
1490
+ // ---------------------------------------------------------------------------
1491
+
1492
+ export function decodeReviewRepairV2(value ) {
1493
+ const body = exactRecord(value, "repair", ["schema", "contract", "operation", "mode", "assessment", "required_inputs"], ["provider_inputs", "execution"]);
1494
+ requireIdentity(body, "gentle-ai.review-integration.repair/v2", "review.repair");
1495
+ const mode = enumeration(body.mode, ["preflight", "execute"] , "repair.mode");
1496
+ const assessment = decodeAuthorityRepairAssessmentV1(body.assessment);
1497
+ const requiredInputs = enumArray(body.required_inputs, ["actor", "reason", "maintainer_authorization"] , "repair.required_inputs", { maximum: 3, unique: true });
1498
+
1499
+ if (mode === "execute") {
1500
+ if (body.execution === undefined) throw new TypeError("repair execute mode requires execution");
1501
+ if (body.provider_inputs !== undefined) throw new TypeError("repair execute mode cannot expose provider_inputs");
1502
+ if (requiredInputs.length !== 0) throw new TypeError("repair execute mode requires an empty required_inputs");
1503
+ } else if (body.execution !== undefined) {
1504
+ throw new TypeError("repair.execution is only valid for execute mode");
1505
+ }
1506
+
1507
+ let providerInputs ;
1508
+ if (mode === "preflight" && assessment.status === "eligible") {
1509
+ if (body.provider_inputs === undefined) throw new TypeError("eligible preflight repair requires provider_inputs");
1510
+ if (requiredInputs.length !== 3 || requiredInputs[0] !== "actor" || requiredInputs[1] !== "reason" || requiredInputs[2] !== "maintainer_authorization") {
1511
+ throw new TypeError("eligible preflight repair requires required_inputs exactly [actor, reason, maintainer_authorization] in order");
1512
+ }
1513
+ const source = exactRecord(body.provider_inputs, "repair.provider_inputs", ["class", "lineage_id", "expected_revision", "cause", "disposition", "repository_binding", "authorization_schema"]);
1514
+ if (source.class !== "legacy_v1_historical_alias") throw new TypeError("repair.provider_inputs.class is unsupported");
1515
+ if (source.cause !== "unsupported_historical_v1_operation_alias") throw new TypeError("repair.provider_inputs.cause is unsupported");
1516
+ if (source.disposition !== "quarantine-approved-historical-alias") throw new TypeError("repair.provider_inputs.disposition is unsupported");
1517
+ if (source.authorization_schema !== "gentle-ai.review-repair-authorization/v1") throw new TypeError("repair.provider_inputs.authorization_schema is unsupported");
1518
+ providerInputs = {
1519
+ class: "legacy_v1_historical_alias",
1520
+ lineageId: lineage(source.lineage_id, "repair.provider_inputs.lineage_id"),
1521
+ expectedRevision: sha256(source.expected_revision, "repair.provider_inputs.expected_revision"),
1522
+ cause: "unsupported_historical_v1_operation_alias",
1523
+ disposition: "quarantine-approved-historical-alias",
1524
+ repositoryBinding: sha256(source.repository_binding, "repair.provider_inputs.repository_binding"),
1525
+ authorizationSchema: "gentle-ai.review-repair-authorization/v1",
1526
+ };
1527
+ } else if (mode === "preflight") {
1528
+ if (body.provider_inputs !== undefined) throw new TypeError("repair.provider_inputs is only valid for an eligible preflight assessment");
1529
+ if (requiredInputs.length !== 0) throw new TypeError("non-eligible preflight repair requires an empty required_inputs");
1530
+ }
1531
+
1532
+ let execution ;
1533
+ if (body.execution !== undefined) {
1534
+ const source = exactRecord(body.execution, "repair.execution", ["status", "class", "lineage_id", "revision", "chain_identity", "cause", "disposition", "assessment_digest", "request_digest", "record_identity"]);
1535
+ if (source.status !== "committed") throw new TypeError("repair.execution.status must be committed");
1536
+ if (source.class !== "legacy_v1_historical_alias") throw new TypeError("repair.execution.class is unsupported");
1537
+ if (source.cause !== "unsupported_historical_v1_operation_alias") throw new TypeError("repair.execution.cause is unsupported");
1538
+ if (source.disposition !== "quarantine-approved-historical-alias") throw new TypeError("repair.execution.disposition is unsupported");
1539
+ execution = {
1540
+ status: "committed",
1541
+ class: "legacy_v1_historical_alias",
1542
+ lineageId: lineage(source.lineage_id, "repair.execution.lineage_id"),
1543
+ revision: sha256(source.revision, "repair.execution.revision"),
1544
+ chainIdentity: sha256(source.chain_identity, "repair.execution.chain_identity"),
1545
+ cause: "unsupported_historical_v1_operation_alias",
1546
+ disposition: "quarantine-approved-historical-alias",
1547
+ assessmentDigest: sha256(source.assessment_digest, "repair.execution.assessment_digest"),
1548
+ requestDigest: sha256(source.request_digest, "repair.execution.request_digest"),
1549
+ recordIdentity: sha256(source.record_identity, "repair.execution.record_identity"),
1550
+ };
1551
+ }
1552
+
1553
+ return {
1554
+ schema: "gentle-ai.review-integration.repair/v2",
1555
+ contract: REVIEW_INTEGRATION_CONTRACT,
1556
+ operation: "review.repair",
1557
+ mode,
1558
+ assessment,
1559
+ ...(providerInputs === undefined ? {} : { providerInputs }),
1560
+ requiredInputs,
1561
+ ...(execution === undefined ? {} : { execution }),
1562
+ raw: body,
1563
+ };
1564
+ }