gentle-pi 1.2.0 → 2.1.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 (111) hide show
  1. package/README.md +32 -10
  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 +647 -58
  65. package/lib/gentle-ai-binary.ts +65 -18
  66. package/lib/native-review-cli.ts +824 -56
  67. package/lib/review-candidate-view.ts +135 -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 +1567 -0
  72. package/package.json +15 -11
  73. package/runtime/gentle-ai-binary.mjs +65 -18
  74. package/runtime/native-review-cli.mjs +808 -40
  75. package/runtime/review-integration-v2.mjs +1568 -0
  76. package/scripts/build-git-commit-transaction-runner.mjs +1 -1
  77. package/scripts/gentle-ai-installer.mjs +464 -76
  78. package/scripts/install-gentle-ai.mjs +7 -3
  79. package/scripts/test-packed-runner.mjs +3 -3
  80. package/scripts/verify-package-files.mjs +216 -64
  81. package/skills/_shared/review-ledger-contract.md +3 -1
  82. package/skills/release/SKILL.md +38 -11
  83. package/tests/devbinary/native-review-parity.devtest.ts +348 -0
  84. package/tests/gentle-ai-binary.test.ts +77 -3
  85. package/tests/gentle-ai-installer.test.ts +561 -22
  86. package/tests/gentle-ai.test.ts +2 -2
  87. package/tests/native-binary-gate.test.ts +145 -0
  88. package/tests/native-review-capability-contract.test.ts +87 -0
  89. package/tests/native-review-cli.test.ts +44 -1
  90. package/tests/native-review-consent.test.ts +128 -0
  91. package/tests/native-review-parity-runtime.test.ts +85 -29
  92. package/tests/native-review-parity.test.ts +782 -0
  93. package/tests/native-risk-evidence-parity.test.ts +91 -0
  94. package/tests/package-manifest.test.ts +80 -8
  95. package/tests/review-actor-tool-deny.test.ts +20 -5
  96. package/tests/review-candidate-view.test.ts +374 -1
  97. package/tests/review-compact-contract.test.ts +5 -0
  98. package/tests/review-consent-latch.test.ts +67 -0
  99. package/tests/review-controller-native-recovery.test.ts +180 -5
  100. package/tests/review-controller-native-routing.test.ts +632 -18
  101. package/tests/review-controller-workspace-root.test.ts +26 -6
  102. package/tests/review-controller.test.ts +1 -1
  103. package/tests/review-correction-lifecycle.test.ts +174 -0
  104. package/tests/review-integration-v2.test.ts +548 -0
  105. package/tests/runtime-harness.mjs +211 -12
  106. package/tests/support/native-binary-gate.ts +86 -0
  107. package/tests/verify-package-files.test.ts +119 -0
  108. package/lib/review-integration-v1.ts +0 -795
  109. package/runtime/review-integration-v1.mjs +0 -796
  110. package/tests/native-review-integration-v1.test.ts +0 -102
  111. package/tests/review-integration-v1.test.ts +0 -530
@@ -0,0 +1,548 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import {
6
+ REVIEW_INTEGRATION_CONTRACT,
7
+ REVIEW_START_STATE,
8
+ decodeAuthorityRepairAssessmentV1,
9
+ decodeReviewArtifactSubjectV2,
10
+ decodeReviewCapabilitiesV2,
11
+ decodeReviewConsentV2,
12
+ decodeReviewFailureV2,
13
+ decodeReviewNextTransitionV3,
14
+ decodeReviewOperationV2,
15
+ decodeReviewProjectionV1,
16
+ decodeReviewRepairV2,
17
+ decodeReviewStartV3,
18
+ decodeReviewStatusV3,
19
+ } from "../lib/review-integration-v2.ts";
20
+
21
+ const fixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
22
+ const fixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(fixtureRoot, name), "utf8")) as T;
23
+ const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
24
+ const digest = `sha256:${"a".repeat(64)}`;
25
+
26
+ type JsonObject = Record<string, unknown>;
27
+ type Decoder = (value: unknown) => unknown;
28
+
29
+ function clone<T>(value: T): T {
30
+ return structuredClone(value);
31
+ }
32
+
33
+ function assertRequired(decoder: Decoder, source: JsonObject, fields: readonly string[]): void {
34
+ for (const field of fields) {
35
+ const candidate = clone(source);
36
+ delete candidate[field];
37
+ assert.throws(() => decoder(candidate), new RegExp(`${field}.*required|required.*${field}`), field);
38
+ }
39
+ }
40
+
41
+ function assertNestedRequired(decoder: Decoder, source: JsonObject, path: readonly string[], fields: readonly string[]): void {
42
+ for (const field of fields) {
43
+ const candidate = clone(source);
44
+ let target = candidate;
45
+ for (const segment of path) target = target[segment] as JsonObject;
46
+ delete target[field];
47
+ assert.throws(() => decoder(candidate), /required/, `${path.join(".")}.${field}`);
48
+ }
49
+ }
50
+
51
+ function assertAdditionalProperty(decoder: Decoder, source: JsonObject, path: readonly string[] = []): void {
52
+ const candidate = clone(source);
53
+ let target = candidate;
54
+ for (const segment of path) target = target[segment] as JsonObject;
55
+ target.unadvertised = true;
56
+ assert.throws(() => decoder(candidate), /not allowed/, path.length === 0 ? "top-level" : path.join("."));
57
+ }
58
+
59
+ test("every published review integration v2 fixture decodes", () => {
60
+ assert.equal(decodeReviewCapabilitiesV2(fixture("capabilities.fixture.json"), executableDigest).contract, REVIEW_INTEGRATION_CONTRACT);
61
+ assert.equal(decodeReviewStartV3(fixture("start.fixture.json")).riskLevel, "high");
62
+ assert.equal(decodeReviewStatusV3(fixture("status.fixture.json")).contract, REVIEW_INTEGRATION_CONTRACT);
63
+ assert.equal(decodeReviewConsentV2(fixture("consent.fixture.json")).action, "consent_required");
64
+ });
65
+
66
+ test("capabilities enforce every required top-level and nested property", () => {
67
+ const source = fixture<JsonObject>("capabilities.fixture.json");
68
+ const decode: Decoder = (value) => decodeReviewCapabilitiesV2(value, executableDigest);
69
+ assertRequired(decode, source, ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"]);
70
+ assertNestedRequired(decode, source, ["protocol"], ["major", "minor"]);
71
+ assertNestedRequired(decode, source, ["package"], ["name", "version", "release_channel"]);
72
+ assertNestedRequired(decode, source, ["build"], ["id", "go_version", "module_version", "vcs", "vcs_revision", "vcs_time", "vcs_modified"]);
73
+ assertNestedRequired(decode, source, ["executable"], ["sha256", "evidence", "verification"]);
74
+ assertNestedRequired(decode, source, ["features"], ["mandatory", "optional"]);
75
+ assertNestedRequired(decode, source, ["compatibility"], ["minimum_protocol_major", "maximum_protocol_major", "additive_minor_policy", "unknown_mandatory", "unknown_optional", "modes", "legacy_window"]);
76
+ });
77
+
78
+ test("capabilities reject additional properties at every exact object boundary", () => {
79
+ const source = fixture<JsonObject>("capabilities.fixture.json");
80
+ const decode: Decoder = (value) => decodeReviewCapabilitiesV2(value, executableDigest);
81
+ for (const path of [[], ["protocol"], ["package"], ["build"], ["executable"], ["features"], ["compatibility"], ["compatibility", "legacy_window"]] as const) {
82
+ assertAdditionalProperty(decode, source, path);
83
+ }
84
+ });
85
+
86
+ test("capabilities reject an incompatible protocol identity", () => {
87
+ const source = fixture<JsonObject>("capabilities.fixture.json");
88
+ const decode: Decoder = (value) => decodeReviewCapabilitiesV2(value, executableDigest);
89
+ const wrongMajor = clone(source);
90
+ (wrongMajor.protocol as JsonObject).major = 1;
91
+ assert.throws(() => decode(wrongMajor), /incompatible/);
92
+ const wrongMinor = clone(source);
93
+ (wrongMinor.protocol as JsonObject).minor = 1;
94
+ assert.throws(() => decode(wrongMinor), /incompatible/);
95
+ assert.throws(() => decode({ ...clone(source), schema: "gentle-ai.review-integration.capabilities/v1" }), /schema/);
96
+ });
97
+
98
+ test("START enforces required, exact, and enum-bounded payloads", () => {
99
+ const source = fixture<JsonObject>("start.fixture.json");
100
+ assertRequired(decodeReviewStartV3, source, ["schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level", "selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects"]);
101
+ assertAdditionalProperty(decodeReviewStartV3, source);
102
+ for (const state of Object.values(REVIEW_START_STATE)) {
103
+ const candidate = clone(source);
104
+ candidate.state = state;
105
+ if (state === "reviewing") continue; // fixture is action=created, state=reviewing already requires repository_context which is present
106
+ assert.throws(() => decodeReviewStartV3(candidate), /repository_context|state/);
107
+ }
108
+ });
109
+
110
+ test("status enforces required properties and rejects additional keys", () => {
111
+ const source = fixture<JsonObject>("status.fixture.json");
112
+ assertRequired(decodeReviewStatusV3, source, ["schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "repair", "candidates"]);
113
+ assertAdditionalProperty(decodeReviewStatusV3, source);
114
+ });
115
+
116
+ test("projection enforces every required property and rejects additional keys", () => {
117
+ const source = (fixture<JsonObject>("status.fixture.json").projection as JsonObject);
118
+ assertRequired(decodeReviewProjectionV1, source, ["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"]);
119
+ assertAdditionalProperty(decodeReviewProjectionV1, source);
120
+ });
121
+
122
+ test("failure enforces exact keys, enums, and identifiers", () => {
123
+ const source: JsonObject = {
124
+ schema: "gentle-ai.review-integration.failure/v2",
125
+ contract: REVIEW_INTEGRATION_CONTRACT,
126
+ operation: "review.finalize",
127
+ phase: "pre_native",
128
+ code: "gate_scope_changed",
129
+ message: "the target scope changed since START",
130
+ mutation_outcome: "not_started",
131
+ authority_applicability: "current_target",
132
+ retry_safe: true,
133
+ replayability: "manual_action_required",
134
+ required_inputs: [],
135
+ next_action: "explicit-maintainer-action",
136
+ };
137
+ assert.equal(decodeReviewFailureV2(source).code, "gate_scope_changed");
138
+ assertRequired(decodeReviewFailureV2, source, ["schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action"]);
139
+ assertAdditionalProperty(decodeReviewFailureV2, source);
140
+ const badCode = clone(source);
141
+ badCode.code = "Bad-Code";
142
+ assert.throws(() => decodeReviewFailureV2(badCode), /code/);
143
+ });
144
+
145
+ function finalizeEnvelope(): JsonObject {
146
+ return {
147
+ schema: "gentle-ai.review-integration.operation/v2",
148
+ contract: REVIEW_INTEGRATION_CONTRACT,
149
+ operation: "review.finalize",
150
+ result: {
151
+ operation: "review/finalize",
152
+ lineage_id: "review-fixture",
153
+ state: "approved",
154
+ action: "publish",
155
+ store_revision: digest,
156
+ },
157
+ };
158
+ }
159
+
160
+ test("operation envelopes strictly bind the outer operation to one exact result variant", () => {
161
+ const envelope = finalizeEnvelope();
162
+ assert.equal(decodeReviewOperationV2(envelope).operation, "review.finalize");
163
+ assertAdditionalProperty(decodeReviewOperationV2, envelope);
164
+ assertNestedRequired(decodeReviewOperationV2, envelope, ["result"], ["operation", "lineage_id", "state", "action", "store_revision"]);
165
+ });
166
+
167
+ test("net-new decoders: consent, next-transition, and artifact-subject reject malformed payloads", () => {
168
+ const consentSource = fixture<JsonObject>("consent.fixture.json");
169
+ assertRequired(decodeReviewConsentV2, consentSource, ["schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path"]);
170
+ assertAdditionalProperty(decodeReviewConsentV2, consentSource);
171
+
172
+ const transitionSource = ((fixture<JsonObject>("status.fixture.json").next_transition) as JsonObject);
173
+ assertRequired(decodeReviewNextTransitionV3, transitionSource, ["kind", "reason_code"]);
174
+ assertAdditionalProperty(decodeReviewNextTransitionV3, transitionSource);
175
+
176
+ const artifactSubjectSource = (((fixture<JsonObject>("start.fixture.json").artifact_subjects) as JsonObject[])[0]);
177
+ assertRequired(decodeReviewArtifactSubjectV2, artifactSubjectSource, ["schema", "subject_hash", "lineage_id", "authority_revision", "target_identity", "base_tree", "candidate_tree", "changed_path_manifest_sha256", "lens", "selected_order"]);
178
+ assertAdditionalProperty(decodeReviewArtifactSubjectV2, artifactSubjectSource);
179
+ });
180
+
181
+ function repairAssessment(status: "eligible" | "unsupported" = "unsupported"): JsonObject {
182
+ if (status === "unsupported") {
183
+ return {
184
+ schema: "gentle-ai.review-authority-repair-assessment/v1",
185
+ status: "unsupported",
186
+ counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
187
+ supported_operations: ["review/complete-fix", "review/validate-fix"],
188
+ authorization_schema: "gentle-ai.review-repair-authorization/v1",
189
+ };
190
+ }
191
+ return {
192
+ schema: "gentle-ai.review-authority-repair-assessment/v1",
193
+ status: "eligible",
194
+ class: "legacy_v1_historical_alias",
195
+ cause: "unsupported_historical_v1_operation_alias",
196
+ disposition: "quarantine-approved-historical-alias",
197
+ repository_binding: digest,
198
+ candidate: {
199
+ lineage_id: "review-legacy-fixture",
200
+ revision: digest,
201
+ chain_identity: digest,
202
+ event_count: 3,
203
+ alias_event_count: 1,
204
+ operations: ["review/complete-fix"],
205
+ },
206
+ counts: { lineages: 1, compact_lineages: 0, legacy_lineages: 1, events: 3, bytes: 128, eligible_candidates: 1, unsupported_lineages: 0, conflicts: 0 },
207
+ supported_operations: ["review/complete-fix", "review/validate-fix"],
208
+ authorization_schema: "gentle-ai.review-repair-authorization/v1",
209
+ };
210
+ }
211
+
212
+ test("authority repair assessment decodes eligible and unsupported statuses", () => {
213
+ assert.equal(decodeAuthorityRepairAssessmentV1(repairAssessment("unsupported")).status, "unsupported");
214
+ assert.equal(decodeAuthorityRepairAssessmentV1(repairAssessment("eligible")).status, "eligible");
215
+ const missingClass = repairAssessment("eligible");
216
+ delete missingClass.class;
217
+ assert.throws(() => decodeAuthorityRepairAssessmentV1(missingClass), /requires class/);
218
+ });
219
+
220
+ test("repair execute-without-execution is rejected", () => {
221
+ const executeMissingExecution: JsonObject = {
222
+ schema: "gentle-ai.review-integration.repair/v2",
223
+ contract: REVIEW_INTEGRATION_CONTRACT,
224
+ operation: "review.repair",
225
+ mode: "execute",
226
+ assessment: repairAssessment("unsupported"),
227
+ required_inputs: [],
228
+ };
229
+ assert.throws(() => decodeReviewRepairV2(executeMissingExecution), /execution/);
230
+ });
231
+
232
+ test("capabilities enforce the exact mandatory feature set while accepting a superset of advertised operations", () => {
233
+ const source = fixture<JsonObject>("capabilities.fixture.json");
234
+ const decode = (value: unknown) => decodeReviewCapabilitiesV2(value, executableDigest);
235
+ const decoded = decode(source) as { mandatoryFeatures: ReadonlySet<string>; operations: ReadonlySet<string> };
236
+ assert.equal(decoded.mandatoryFeatures.has("compact_v2_authority"), true);
237
+ assert.equal(decoded.operations.has("review.repair"), true);
238
+
239
+ const extraMandatory = clone(source);
240
+ ((extraMandatory.features as JsonObject).mandatory as JsonObject[]).push({ name: "risk_reasons", supported: true, requires: [] });
241
+ assert.throws(() => decode(extraMandatory), /mandatory/);
242
+
243
+ // operations is a superset promise: an extra advertised operation beyond the
244
+ // required set must not be rejected.
245
+ const extraOperation = clone(source);
246
+ (extraOperation.operations as string[]).push("review.future_operation");
247
+ assert.doesNotThrow(() => decode(extraOperation));
248
+ });
249
+
250
+ test("START independently binds base/candidate tree and the target-mode overlay pair", () => {
251
+ const source = fixture<JsonObject>("start.fixture.json");
252
+ const decoded = decodeReviewStartV3(source);
253
+ assert.equal(decoded.baseTree, source.base_tree);
254
+ assert.equal(decoded.candidateTree, source.candidate_tree);
255
+ assert.equal(decoded.targetMode, undefined, "the fixture never sets target_mode");
256
+
257
+ const partialOverlay = clone(source);
258
+ delete partialOverlay.target_identity;
259
+ (partialOverlay as JsonObject).target_mode = "base-workspace-overlay";
260
+ assert.throws(() => decodeReviewStartV3(partialOverlay), /target_mode.*target_identity|together/);
261
+
262
+ const droppedManifest = clone(source);
263
+ delete droppedManifest.changed_path_manifest;
264
+ assert.throws(() => decodeReviewStartV3(droppedManifest), /selected_lenses|changed_path_manifest/);
265
+
266
+ const manifestWithoutTrees = clone(source);
267
+ delete manifestWithoutTrees.base_tree;
268
+ delete manifestWithoutTrees.candidate_tree;
269
+ delete manifestWithoutTrees.selected_lenses;
270
+ (manifestWithoutTrees as JsonObject).selected_lenses = [];
271
+ assert.throws(() => decodeReviewStartV3(manifestWithoutTrees), /changed_path_manifest/);
272
+ });
273
+
274
+ test("status enforces authority/frozen/receipt conditionals and decodes the required repair field", () => {
275
+ const current = fixture<JsonObject>("status.fixture.json");
276
+ const decoded = decodeReviewStatusV3(current);
277
+ assert.equal(decoded.repair.status, "unsupported");
278
+ assert.equal(decoded.repair.counts.eligibleCandidates, 0);
279
+
280
+ const missingAuthority = clone(current);
281
+ delete missingAuthority.authority;
282
+ assert.throws(() => decodeReviewStatusV3(missingAuthority), /requires authority/);
283
+
284
+ const compactMissingFrozen = clone(current);
285
+ delete compactMissingFrozen.frozen;
286
+ assert.throws(() => decodeReviewStatusV3(compactMissingFrozen), /requires frozen/);
287
+
288
+ const legacyWithFrozen = clone(current);
289
+ (legacyWithFrozen.authority as JsonObject).version = "legacy-v1";
290
+ (legacyWithFrozen.receipt as JsonObject).status = "expected_missing";
291
+ assert.throws(() => decodeReviewStatusV3(legacyWithFrozen), /legacy status cannot expose frozen/);
292
+ });
293
+
294
+ test("status validation_request and retry_final_verification require their exact authority preconditions", () => {
295
+ const current = fixture<JsonObject>("status.fixture.json");
296
+ const withValidationRequest = clone(current);
297
+ withValidationRequest.validation_request = {
298
+ schema: "gentle-ai.review-targeted-validation-request/v1",
299
+ request_hash: digest,
300
+ lineage_id: "review-status-fixture",
301
+ expected_revision: digest,
302
+ target_identity: (current.target_identity as string),
303
+ fix_finding_ids: ["finding-1"],
304
+ projection: "workspace",
305
+ correction_candidate_tree: "a".repeat(40),
306
+ correction_target_identity: digest,
307
+ correction_paths: ["tracked.txt"],
308
+ correction_paths_digest: digest,
309
+ };
310
+ // authority.state is "reviewing" in the fixture, not correction_required.
311
+ assert.throws(() => decodeReviewStatusV3(withValidationRequest), /correction_required/);
312
+
313
+ const retryWithoutIncident = clone(current);
314
+ retryWithoutIncident.action = "retry_final_verification";
315
+ retryWithoutIncident.action_disposition = "final_verification_retry";
316
+ (retryWithoutIncident.authority as JsonObject).state = "escalated";
317
+ assert.throws(() => decodeReviewStatusV3(retryWithoutIncident), /requires final_verification_retry/);
318
+ });
319
+
320
+ test("operation envelopes decode validate, bind_sdd, and retry_final_verification result variants", () => {
321
+ const validateEnvelope: JsonObject = {
322
+ schema: "gentle-ai.review-integration.operation/v2",
323
+ contract: REVIEW_INTEGRATION_CONTRACT,
324
+ operation: "review.validate",
325
+ result: { schema: "gentle-ai.review-gate-result/v1", result: "allow", allowed: true, action: "publish", reason: "receipt matches", context: { gate: "pre-pr" } },
326
+ };
327
+ assert.equal(decodeReviewOperationV2(validateEnvelope).operation, "review.validate");
328
+
329
+ const bindSddEnvelope: JsonObject = {
330
+ schema: "gentle-ai.review-integration.operation/v2",
331
+ contract: REVIEW_INTEGRATION_CONTRACT,
332
+ operation: "review.bind_sdd",
333
+ result: { schema: "gentle-ai.sdd-review-binding/v1", revision: digest, change: "migrate-review-integration-v2", lineage: "review-fixture", authority_revision: digest, receipt_hash: digest, gate_context: { gate: "post-apply" } },
334
+ };
335
+ assert.equal(decodeReviewOperationV2(bindSddEnvelope).operation, "review.bind_sdd");
336
+
337
+ const retryEnvelope: JsonObject = {
338
+ schema: "gentle-ai.review-integration.operation/v2",
339
+ contract: REVIEW_INTEGRATION_CONTRACT,
340
+ operation: "review.retry_final_verification",
341
+ result: {
342
+ operation: "review.retry_final_verification",
343
+ predecessor_lineage_id: "review-predecessor",
344
+ predecessor_revision: digest,
345
+ lineage_id: "review-successor",
346
+ state: "validating",
347
+ store_revision: digest,
348
+ target_identity: digest,
349
+ incident_digest: digest,
350
+ recovery_disposition: "final_verification_retry",
351
+ },
352
+ };
353
+ assert.equal(decodeReviewOperationV2(retryEnvelope).operation, "review.retry_final_verification");
354
+
355
+ const crossedResult = clone(validateEnvelope);
356
+ crossedResult.result = bindSddEnvelope.result;
357
+ assert.throws(() => decodeReviewOperationV2(crossedResult), /does not match|not allowed|required/);
358
+ });
359
+
360
+ test("failure context accepts scope_change or binding_revision but rejects both or neither", () => {
361
+ const base: JsonObject = {
362
+ schema: "gentle-ai.review-integration.failure/v2",
363
+ contract: REVIEW_INTEGRATION_CONTRACT,
364
+ operation: "review.finalize",
365
+ phase: "pre_native",
366
+ code: "gate_scope_changed",
367
+ message: "the target scope changed since START",
368
+ mutation_outcome: "not_started",
369
+ authority_applicability: "current_target",
370
+ retry_safe: true,
371
+ replayability: "manual_action_required",
372
+ required_inputs: ["predecessor_lineage_id", "expected_predecessor_revision", "successor_lineage_id", "disposition", "reason", "actor"],
373
+ next_action: "explicit-maintainer-action",
374
+ context: {
375
+ binding_revision: { expected: digest, current: "" },
376
+ },
377
+ };
378
+ const decoded = decodeReviewFailureV2(base);
379
+ assert.equal(decoded.context?.bindingRevision?.current, "");
380
+
381
+ const neither = clone(base);
382
+ neither.context = {};
383
+ assert.throws(() => decodeReviewFailureV2(neither), /exactly one/);
384
+
385
+ const both = clone(base);
386
+ both.context = { binding_revision: { expected: digest, current: digest }, scope_change: (both.context as JsonObject) };
387
+ assert.throws(() => decodeReviewFailureV2(both), /exactly one|not allowed/);
388
+ });
389
+
390
+ test("consent rejects a swapped choice order, invalid answer domain, or invocation outside the frozen target", () => {
391
+ const source = fixture<JsonObject>("consent.fixture.json");
392
+ const swapped = clone(source);
393
+ swapped.choices = [(source.choices as JsonObject[])[1], (source.choices as JsonObject[])[0]];
394
+ assert.throws(() => decodeReviewConsentV2(swapped), /answer/);
395
+
396
+ const badAnswer = clone(source);
397
+ (badAnswer.choices as JsonObject[])[0].answer = "yes";
398
+ assert.throws(() => decodeReviewConsentV2(badAnswer), /answer/);
399
+
400
+ const badInvocation = clone(source);
401
+ (badInvocation.choices as JsonObject[])[0].invocation = "gentle-ai review start --contract gentle-ai.review-integration/v1 --consent granted";
402
+ assert.throws(() => decodeReviewConsentV2(badInvocation), /invocation/);
403
+
404
+ const differentTarget = clone(source);
405
+ (differentTarget.choices as JsonObject[])[0].invocation = String((differentTarget.choices as JsonObject[])[0].invocation)
406
+ .replace(String(differentTarget.target_identity), `sha256:${"b".repeat(64)}`);
407
+ assert.throws(() => decodeReviewConsentV2(differentTarget), /target|invocation/);
408
+ });
409
+
410
+ test("next_transition decodes an execute variant and rejects a stop that carries a transition", () => {
411
+ const execute: JsonObject = {
412
+ kind: "execute",
413
+ reason_code: "fresh_target_ready",
414
+ execute: {
415
+ operation: "review.start",
416
+ arguments: [{ name: "lineage", value: "review-fixture", token: "--lineage=review-fixture" }],
417
+ preconditions: [{ name: "clean", value: "true" }],
418
+ binding: { target_identity: digest },
419
+ },
420
+ };
421
+ const decoded = decodeReviewNextTransitionV3(execute);
422
+ assert.equal(decoded.execute?.operation, "review.start");
423
+ assert.equal(decoded.execute?.arguments[0]?.token, "--lineage=review-fixture");
424
+
425
+ const stopWithExecute = clone(execute);
426
+ stopWithExecute.kind = "stop";
427
+ assert.throws(() => decodeReviewNextTransitionV3(stopWithExecute), /stop cannot carry/);
428
+ });
429
+
430
+ test("repair decodes a committed execute result and rejects a non-eligible preflight carrying provider_inputs", () => {
431
+ const executed: JsonObject = {
432
+ schema: "gentle-ai.review-integration.repair/v2",
433
+ contract: REVIEW_INTEGRATION_CONTRACT,
434
+ operation: "review.repair",
435
+ mode: "execute",
436
+ assessment: repairAssessment("eligible"),
437
+ required_inputs: [],
438
+ execution: {
439
+ status: "committed",
440
+ class: "legacy_v1_historical_alias",
441
+ lineage_id: "review-legacy-fixture",
442
+ revision: digest,
443
+ chain_identity: digest,
444
+ cause: "unsupported_historical_v1_operation_alias",
445
+ disposition: "quarantine-approved-historical-alias",
446
+ assessment_digest: digest,
447
+ request_digest: digest,
448
+ record_identity: digest,
449
+ },
450
+ };
451
+ assert.equal(decodeReviewRepairV2(executed).execution?.status, "committed");
452
+
453
+ const nonEligibleWithInputs: JsonObject = {
454
+ schema: "gentle-ai.review-integration.repair/v2",
455
+ contract: REVIEW_INTEGRATION_CONTRACT,
456
+ operation: "review.repair",
457
+ mode: "preflight",
458
+ assessment: repairAssessment("unsupported"),
459
+ provider_inputs: {
460
+ class: "legacy_v1_historical_alias",
461
+ lineage_id: "review-legacy-fixture",
462
+ expected_revision: digest,
463
+ cause: "unsupported_historical_v1_operation_alias",
464
+ disposition: "quarantine-approved-historical-alias",
465
+ repository_binding: digest,
466
+ authorization_schema: "gentle-ai.review-repair-authorization/v1",
467
+ },
468
+ required_inputs: [],
469
+ };
470
+ assert.throws(() => decodeReviewRepairV2(nonEligibleWithInputs), /provider_inputs is only valid/);
471
+ });
472
+
473
+ test("artifact-subject accepts an optional correction_target_identity", () => {
474
+ const source = ((fixture<JsonObject>("start.fixture.json").artifact_subjects) as JsonObject[])[0];
475
+ const withCorrection = clone(source);
476
+ withCorrection.correction_target_identity = digest;
477
+ const decoded = decodeReviewArtifactSubjectV2(withCorrection);
478
+ assert.equal(decoded.correctionTargetIdentity, digest);
479
+ assert.equal(decodeReviewArtifactSubjectV2(source).correctionTargetIdentity, undefined);
480
+ });
481
+
482
+ test("repair eligible preflight with wrong required_inputs order is rejected", () => {
483
+ const wrongOrder: JsonObject = {
484
+ schema: "gentle-ai.review-integration.repair/v2",
485
+ contract: REVIEW_INTEGRATION_CONTRACT,
486
+ operation: "review.repair",
487
+ mode: "preflight",
488
+ assessment: repairAssessment("eligible"),
489
+ provider_inputs: {
490
+ class: "legacy_v1_historical_alias",
491
+ lineage_id: "review-legacy-fixture",
492
+ expected_revision: digest,
493
+ cause: "unsupported_historical_v1_operation_alias",
494
+ disposition: "quarantine-approved-historical-alias",
495
+ repository_binding: digest,
496
+ authorization_schema: "gentle-ai.review-repair-authorization/v1",
497
+ },
498
+ required_inputs: ["reason", "actor", "maintainer_authorization"],
499
+ };
500
+ assert.throws(() => decodeReviewRepairV2(wrongOrder), /required_inputs/);
501
+ });
502
+
503
+ // Three decode defects the after-bench found by driving the real v2.2.2 binary.
504
+ // Every native call exited 0 and authority advanced; Pi threw locally on the
505
+ // RESPONSE. The mirrored fixture never caught them because it only exercises
506
+ // the `collect` transition kind, so no `execute` payload was ever decoded.
507
+ //
508
+ // The authority here is contracts/review-integration/v1/schemas/status-v2.schema.json
509
+ // $defs.transition_execution, which declares optional `command`,
510
+ // `selector_arguments`, and `artifacts`, and a `binding` with NO declared
511
+ // properties and no required list -- an OPEN object. Pi had closed execute to
512
+ // `command` alone and binding to three keys, making it stricter than the
513
+ // contract it implements.
514
+
515
+ test("next_transition.execute accepts the optional selector_arguments and artifacts the schema declares", () => {
516
+ const base = {
517
+ kind: "execute" as const,
518
+ reason_code: "captured_results_ready",
519
+ execute: {
520
+ operation: "review.finalize",
521
+ command: "gentle-ai review finalize --lineage=review-96f29cbd865e77a9 --captured-results=true",
522
+ arguments: [{ name: "lineage", value: "review-96f29cbd865e77a9", token: "--lineage=review-96f29cbd865e77a9" }],
523
+ preconditions: [{ name: "target_identity", value: "sha256:" + "9".repeat(64) }],
524
+ binding: { target_identity: "sha256:" + "9".repeat(64) },
525
+ },
526
+ };
527
+
528
+ assert.doesNotThrow(() => decodeReviewNextTransitionV3(base));
529
+ assert.doesNotThrow(() => decodeReviewNextTransitionV3({ ...base, execute: { ...base.execute, artifacts: [{ name: "receipt", path: "review-receipt.json" }] } }));
530
+ assert.doesNotThrow(() => decodeReviewNextTransitionV3({ ...base, execute: { ...base.execute, selector_arguments: [{ name: "projection", value: "workspace", token: "--projection=workspace" }] } }));
531
+ });
532
+
533
+ test("next_transition.execute.binding stays open, as its schema declares no properties", () => {
534
+ const withContext = {
535
+ kind: "execute" as const,
536
+ reason_code: "approved_receipt_ready",
537
+ execute: {
538
+ operation: "review.validate",
539
+ arguments: [{ name: "gate", value: "pre-commit", token: "--gate=pre-commit" }],
540
+ preconditions: [{ name: "target_identity", value: "sha256:" + "8".repeat(64) }],
541
+ // The provider sends this. The schema constrains nothing here, so Pi
542
+ // closing the object was stricter than the contract.
543
+ binding: { target_identity: "sha256:" + "8".repeat(64), repository_context: "rctx1_" + "c".repeat(64) },
544
+ },
545
+ };
546
+
547
+ assert.doesNotThrow(() => decodeReviewNextTransitionV3(withContext));
548
+ });