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
@@ -19,11 +19,11 @@ class NativeReviewCliV214 extends NativeReviewCliV214Production {
19
19
  }
20
20
  }
21
21
  import { canonicalJsonV1, domainHashV1 } from "../lib/review-canonical.ts";
22
- import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
22
+ import { CandidateViewRegistry, deriveChangedPathManifest } from "../lib/review-candidate-view.ts";
23
23
  import { inspectLegacyReviewAuthorityV1 } from "../lib/review-legacy-detector.ts";
24
24
  import { resolveRepositoryAuthorityV1 } from "../lib/review-repository.ts";
25
25
  import { NATIVE_REVIEW_REMEDIATION, classifyNativeReviewRemediation } from "../lib/native-review-remediation.ts";
26
- import type { ReviewStatusV1 } from "../lib/review-integration-v1.ts";
26
+ import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
27
27
 
28
28
  interface RegisteredTool {
29
29
  execute: (
@@ -268,18 +268,26 @@ function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
268
268
  };
269
269
  }
270
270
 
271
+ const UNSUPPORTED_REPAIR_ASSESSMENT: AuthorityRepairAssessmentV1 = {
272
+ schema: "gentle-ai.review-authority-repair-assessment/v1",
273
+ status: "unsupported",
274
+ counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 },
275
+ supportedOperations: ["review/complete-fix", "review/validate-fix"],
276
+ authorizationSchema: "gentle-ai.review-repair-authorization/v1",
277
+ };
278
+
271
279
  function targetStatusFixture(options: {
272
280
  applicability?: "current_target" | "unrelated" | "ambiguous" | "corrupted";
273
- action?: ReviewStatusV1["action"];
274
- replayability?: ReviewStatusV1["replayability"];
281
+ action?: ReviewStatusV3["action"];
282
+ replayability?: ReviewStatusV3["replayability"];
275
283
  lineageId?: string;
276
284
  authorityVersion?: "compact-v2" | "legacy-v1";
277
- authorityState?: NonNullable<ReviewStatusV1["authority"]>["state"];
278
- receiptStatus?: ReviewStatusV1["receipt"]["status"];
285
+ authorityState?: NonNullable<ReviewStatusV3["authority"]>["state"];
286
+ receiptStatus?: ReviewStatusV3["receipt"]["status"];
279
287
  baseTree?: string;
280
288
  currentCandidateTree?: string;
281
289
  paths?: readonly string[];
282
- } = {}): ReviewStatusV1 {
290
+ } = {}): ReviewStatusV3 {
283
291
  const applicability = options.applicability ?? "current_target";
284
292
  const action = options.action ?? (applicability === "current_target" ? "finalize" : applicability === "unrelated" ? "start" : applicability === "ambiguous" ? "select_lineage" : "repair_authority");
285
293
  const replayability = options.replayability ?? (action === "reconcile_finalize" ? "status_required" : applicability === "ambiguous" ? "status_required" : applicability === "corrupted" ? "manual_action_required" : "not_replayable");
@@ -305,15 +313,26 @@ function targetStatusFixture(options: {
305
313
  initialSnapshotIdentity: sha,
306
314
  currentSnapshotIdentity: sha,
307
315
  };
316
+ const rawRepair = {
317
+ schema: UNSUPPORTED_REPAIR_ASSESSMENT.schema,
318
+ status: UNSUPPORTED_REPAIR_ASSESSMENT.status,
319
+ counts: {
320
+ lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0,
321
+ eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0,
322
+ },
323
+ supported_operations: UNSUPPORTED_REPAIR_ASSESSMENT.supportedOperations,
324
+ authorization_schema: UNSUPPORTED_REPAIR_ASSESSMENT.authorizationSchema,
325
+ };
308
326
  const raw: Record<string, unknown> = {
309
- schema: "gentle-ai.review-integration.status/v1",
310
- contract: "gentle-ai.review-integration/v1",
327
+ schema: "gentle-ai.review-integration.status/v3",
328
+ contract: "gentle-ai.review-integration/v2",
311
329
  operation: "review.status",
312
330
  applicability,
313
331
  receipt: { status: receiptStatus },
314
332
  action,
315
333
  replayability,
316
334
  target_identity: sha,
335
+ repair: rawRepair,
317
336
  projection: {
318
337
  schema: projection.schema,
319
338
  kind: projection.kind,
@@ -336,7 +355,7 @@ function targetStatusFixture(options: {
336
355
  }
337
356
  if (action === "reconcile_finalize") raw.reconciliation = { required: true };
338
357
  return {
339
- contract: "gentle-ai.review-integration/v1",
358
+ contract: "gentle-ai.review-integration/v2",
340
359
  applicability,
341
360
  ...(applicability === "current_target" ? { authority: { version: authorityVersion, lineageId, state: authorityState, generation: 1, revision: sha } } : {}),
342
361
  receipt: { status: receiptStatus },
@@ -346,11 +365,116 @@ function targetStatusFixture(options: {
346
365
  ...(action === "reconcile_finalize" ? { reconciliation: { required: true as const } } : {}),
347
366
  targetIdentity: sha,
348
367
  projection,
368
+ repair: UNSUPPORTED_REPAIR_ASSESSMENT,
349
369
  candidates: applicability === "ambiguous" ? [lineageId, "other-lineage"] : [],
350
370
  raw,
351
371
  };
352
372
  }
353
373
 
374
+ function bindReviewerManifest(status: ReviewStatusV3, cwd: string, manifestHash = `sha256:${"7".repeat(64)}`): ReviewStatusV3 {
375
+ const manifest = deriveChangedPathManifest(cwd, status.projection.baseTree, status.projection.currentCandidateTree).map((entry) => ({
376
+ ...entry,
377
+ status: entry.status as "A" | "D" | "M" | "T",
378
+ intendedUntracked: status.projection.intendedUntracked.includes(entry.path),
379
+ }));
380
+ const subject = {
381
+ schema: "gentle-ai.review-artifact-subject/v2" as const,
382
+ subjectHash: `sha256:${"8".repeat(64)}`,
383
+ lineageId: status.authority!.lineageId,
384
+ authorityRevision: status.authority!.revision,
385
+ targetIdentity: status.targetIdentity,
386
+ baseTree: status.projection.baseTree,
387
+ candidateTree: status.projection.currentCandidateTree,
388
+ changedPathManifestSha256: manifestHash,
389
+ lens: "review-reliability" as const,
390
+ selectedOrder: 0,
391
+ };
392
+ status.nextTransition = {
393
+ kind: "collect",
394
+ reasonCode: "reviewer_results_required",
395
+ collect: { inputs: [{
396
+ name: "reviewer_result",
397
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
398
+ captureOperation: "review.capture-result",
399
+ arguments: [
400
+ { name: "lineage", value: subject.lineageId, token: `--lineage=${subject.lineageId}` },
401
+ { name: "expected-revision", value: subject.authorityRevision, token: `--expected-revision=${subject.authorityRevision}` },
402
+ { name: "target", value: subject.targetIdentity, token: `--target=${subject.targetIdentity}` },
403
+ { name: "lens", value: subject.lens, token: `--lens=${subject.lens}` },
404
+ { name: "order", value: "0", token: "--order=0" },
405
+ { name: "subject-hash", value: subject.subjectHash, token: `--subject-hash=${subject.subjectHash}` },
406
+ ],
407
+ artifactSubject: subject,
408
+ baseTree: subject.baseTree,
409
+ candidateTree: subject.candidateTree,
410
+ changedPathManifest: manifest,
411
+ }] },
412
+ };
413
+ return status;
414
+ }
415
+
416
+ function bindCorrectionCollection(status: ReviewStatusV3): ReviewStatusV3 {
417
+ status.nextTransition = {
418
+ kind: "collect",
419
+ reasonCode: "verification_evidence_required",
420
+ collect: { inputs: [{
421
+ name: "verification_evidence",
422
+ schema: "gentle-ai.review-verification-evidence/v2",
423
+ captureOperation: "review.capture-evidence",
424
+ arguments: [{ name: "lineage", value: status.authority!.lineageId }],
425
+ }] },
426
+ };
427
+ delete status.validationRequest;
428
+ return status;
429
+ }
430
+
431
+ function bindTargetedValidation(status: ReviewStatusV3, requestHash = `sha256:${"9".repeat(64)}`): ReviewStatusV3 {
432
+ const request = {
433
+ schema: "gentle-ai.review-targeted-validation-request/v1" as const,
434
+ requestHash,
435
+ lineageId: status.authority!.lineageId,
436
+ expectedRevision: status.authority!.revision,
437
+ targetIdentity: status.targetIdentity,
438
+ fixFindingIds: [],
439
+ projection: "workspace" as const,
440
+ correctionCandidateTree: status.projection.currentCandidateTree,
441
+ correctionTargetIdentity: status.targetIdentity,
442
+ correctionPaths: status.projection.paths,
443
+ correctionPathsDigest: status.projection.pathsDigest,
444
+ };
445
+ status.validationRequest = request;
446
+ status.nextTransition = {
447
+ kind: "collect",
448
+ reasonCode: "targeted_validation_required",
449
+ collect: { inputs: [{
450
+ name: "targeted_validation",
451
+ schema: request.schema,
452
+ captureOperation: "external.run_targeted_validation",
453
+ arguments: [{ name: "lineage", value: request.lineageId }],
454
+ validationRequest: request,
455
+ }] },
456
+ };
457
+ return status;
458
+ }
459
+
460
+ function capturedCorrectionEvidence(status: ReviewStatusV3, outcome: "passed" | "verification_failed" | "procedural_tooling_failed", identityDigit: string) {
461
+ return {
462
+ schema: "gentle-ai.review-verification-evidence/v2" as const,
463
+ version: 2 as const,
464
+ lineageId: status.authority!.lineageId,
465
+ authorityRevision: status.authority!.revision,
466
+ targetIdentity: status.targetIdentity,
467
+ candidateTree: status.projection.currentCandidateTree,
468
+ pathsDigest: status.projection.pathsDigest,
469
+ paths: status.projection.paths,
470
+ ledgerIds: [],
471
+ rawPayloadSha256: `sha256:${identityDigit.repeat(64)}`,
472
+ rawPayloadBytes: 8,
473
+ outcome,
474
+ recordDigest: `sha256:${identityDigit.repeat(64)}`,
475
+ };
476
+ }
477
+
354
478
  function findResetRequests(value: unknown): unknown[] {
355
479
  if (Array.isArray(value)) return value.flatMap(findResetRequests);
356
480
  if (!value || typeof value !== "object") return [];
@@ -584,6 +708,7 @@ test("fresh negotiated registries reconstruct the frozen candidate before FINALI
584
708
  status.projection.initialReviewTree = frozen.candidateTree;
585
709
  status.projection.currentCandidateTree = frozen.candidateTree;
586
710
  status.projection.paths = frozen.paths;
711
+ bindReviewerManifest(status, cwd);
587
712
  frozen.cleanup();
588
713
  let finalizedContent = "";
589
714
  const { controller } = runtime(fakeNative({
@@ -602,6 +727,85 @@ test("fresh negotiated registries reconstruct the frozen candidate before FINALI
602
727
  assert.equal((result.details as { result: { state: string } }).result.state, "approved");
603
728
  });
604
729
 
730
+ test("fresh FINALIZE restores the provider manifest binding and rejects mode drift on the production path", async (t) => {
731
+ const cwd = repository(t);
732
+ writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
733
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
734
+ const status = targetStatusFixture({ lineageId: "manifest-lineage", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths });
735
+ bindReviewerManifest(status, cwd);
736
+ const collect = status.nextTransition!.collect!;
737
+ const input = collect.inputs[0]!;
738
+ const changed = input.changedPathManifest![0]!;
739
+ status.nextTransition = { ...status.nextTransition!, collect: { inputs: [{
740
+ ...input,
741
+ changedPathManifest: [{
742
+ ...changed,
743
+ newMode: "100755",
744
+ modeOnly: true,
745
+ }],
746
+ }] } };
747
+ frozen.cleanup();
748
+ let finalizes = 0;
749
+ const { controller } = runtime(fakeNative({
750
+ targetStatus: async () => status,
751
+ finalize: async () => {
752
+ finalizes += 1;
753
+ return { lineageId: "manifest-lineage", state: "approved", action: "approved", storeRevision: "r1" };
754
+ },
755
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
756
+ const result = await controller.execute("manifest-mode-drift", {
757
+ operation: "finalize",
758
+ lineageId: "manifest-lineage",
759
+ input: JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } }),
760
+ }, undefined, undefined, context(cwd));
761
+ assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
762
+ assert.equal(((result.details as { diagnostics?: { code?: string } }).diagnostics?.code), "manifest-mode-drift");
763
+ assert.equal(finalizes, 0, "manifest drift must stop before native FINALIZE");
764
+ });
765
+
766
+ test("fresh FINALIZE rejects inconsistent provider manifest hashes before production dispatch", async (t) => {
767
+ const cwd = repository(t);
768
+ writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
769
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
770
+ const status = bindReviewerManifest(targetStatusFixture({ lineageId: "manifest-hash-lineage", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths }), cwd);
771
+ const first = status.nextTransition!.collect!.inputs[0]!;
772
+ const secondSubjectHash = `sha256:${"a".repeat(64)}`;
773
+ status.nextTransition = { ...status.nextTransition!, collect: { inputs: [first, {
774
+ ...first,
775
+ arguments: first.arguments.map((argument) => argument.name === "lens"
776
+ ? { ...argument, value: "review-risk", token: "--lens=review-risk" }
777
+ : argument.name === "order"
778
+ ? { ...argument, value: "1", token: "--order=1" }
779
+ : argument.name === "subject-hash"
780
+ ? { ...argument, value: secondSubjectHash, token: `--subject-hash=${secondSubjectHash}` }
781
+ : argument),
782
+ artifactSubject: {
783
+ ...first.artifactSubject!,
784
+ subjectHash: secondSubjectHash,
785
+ changedPathManifestSha256: `sha256:${"b".repeat(64)}`,
786
+ lens: "review-risk",
787
+ selectedOrder: 1,
788
+ },
789
+ }] } };
790
+ frozen.cleanup();
791
+ let finalizes = 0;
792
+ const { controller } = runtime(fakeNative({
793
+ targetStatus: async () => status,
794
+ finalize: async () => {
795
+ finalizes += 1;
796
+ return { lineageId: "manifest-hash-lineage", state: "approved", action: "approved", storeRevision: "r1" };
797
+ },
798
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
799
+ const result = await controller.execute("manifest-hash-drift", {
800
+ operation: "finalize",
801
+ lineageId: "manifest-hash-lineage",
802
+ input: JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } }),
803
+ }, undefined, undefined, context(cwd));
804
+ assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
805
+ assert.equal(((result.details as { diagnostics?: { code?: string } }).diagnostics?.code), "manifest-input-divergence");
806
+ assert.equal(finalizes, 0);
807
+ });
808
+
605
809
  test("forecast-only FINALIZE reconstructs the frozen candidate after a fresh process (#176)", async (t) => {
606
810
  const cwd = repository(t);
607
811
  writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
@@ -924,10 +1128,16 @@ test("fresh registry reload ignores raw correction state and follows the native
924
1128
  const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
925
1129
  mkdirSync(dirname(join(cwd, ".git", "gentle-ai", "review-transactions", "v2", "correction-lineage", "review-state.json")), { recursive: true });
926
1130
  writeFileSync(join(cwd, ".git", "gentle-ai", "review-transactions", "v2", "correction-lineage", "review-state.json"), JSON.stringify({ schema: "gentle-ai.review-state-record/v2", state: { schema: "gentle-ai.review-state/v2", lineage_id: "correction-lineage", state: "correction_required", initial_snapshot: { kind: "current-changes", base_tree: frozen.baseTree, candidate_tree: frozen.candidateTree, paths: frozen.paths, paths_digest: "paths" }, current_snapshot: { kind: "current-changes", base_tree: frozen.baseTree, candidate_tree: frozen.candidateTree, paths: frozen.paths, paths_digest: "paths" }, fix_finding_ids: ["RELIABILITY-001"], findings: [{ id: "RELIABILITY-001", severity: "CRITICAL" }] } }));
927
- const status = targetStatusFixture({ lineageId: "correction-lineage", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths });
1131
+ const status = bindCorrectionCollection(targetStatusFixture({ lineageId: "correction-lineage", authorityState: "correction_required", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths }));
1132
+ const validationStatus = bindTargetedValidation(targetStatusFixture({ lineageId: "correction-lineage", authorityState: "validating", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths }));
928
1133
  frozen.cleanup();
929
1134
  let finalizes = 0;
930
- const { controller } = runtime(fakeNative({ finalize: async () => { finalizes += 1; return { lineageId: "correction-lineage", state: "approved", action: "approved", storeRevision: "r2" }; }, targetStatus: async () => status }), undefined, undefined, undefined, new CandidateViewRegistry());
1135
+ let statuses = 0;
1136
+ const { controller } = runtime(fakeNative({
1137
+ finalize: async () => { finalizes += 1; return { lineageId: "correction-lineage", state: "approved", action: "approved", storeRevision: "r2" }; },
1138
+ targetStatus: async () => { statuses += 1; return statuses === 1 ? status : validationStatus; },
1139
+ captureEvidence: async () => capturedCorrectionEvidence(status, "passed", "6"),
1140
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
931
1141
  const required = await controller.execute("correction-validation-request", { operation: "finalize", lineageId: "correction-lineage", input: JSON.stringify({ final_evidence: "focused tests passed", final_verification_passed: true }) }, undefined, undefined, context(cwd));
932
1142
  const request = required.details as { status: string; result: Record<string, unknown> };
933
1143
  assert.equal(request.status, "in-progress");
@@ -938,6 +1148,140 @@ test("fresh registry reload ignores raw correction state and follows the native
938
1148
  assert.equal(finalizes, 0);
939
1149
  });
940
1150
 
1151
+ test("production correction routing captures evidence before validation and enforces all three outcomes", async (t) => {
1152
+ for (const [outcome, expectedKind] of [
1153
+ ["passed", "run-targeted-validation"],
1154
+ ["verification_failed", "recapture-required"],
1155
+ ["procedural_tooling_failed", "terminal-escalation"],
1156
+ ] as const) {
1157
+ await t.test(outcome, async (scenario) => {
1158
+ const cwd = repository(scenario);
1159
+ writeFileSync(join(cwd, "app.ts"), `export const outcome = ${JSON.stringify(outcome)};\n`);
1160
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
1161
+ const beforeCapture = bindCorrectionCollection(targetStatusFixture({
1162
+ lineageId: `correction-${outcome.replaceAll("_", "-")}`,
1163
+ authorityState: "correction_required",
1164
+ baseTree: frozen.baseTree,
1165
+ currentCandidateTree: frozen.candidateTree,
1166
+ paths: frozen.paths,
1167
+ }));
1168
+ const afterCapture = targetStatusFixture({
1169
+ lineageId: beforeCapture.authority!.lineageId,
1170
+ authorityState: outcome === "procedural_tooling_failed" ? "escalated" : outcome === "passed" ? "validating" : "correction_required",
1171
+ action: outcome === "procedural_tooling_failed" ? "stop" : "finalize",
1172
+ baseTree: frozen.baseTree,
1173
+ currentCandidateTree: frozen.candidateTree,
1174
+ paths: frozen.paths,
1175
+ });
1176
+ if (outcome === "passed") bindTargetedValidation(afterCapture);
1177
+ else if (outcome === "verification_failed") bindCorrectionCollection(afterCapture);
1178
+ frozen.cleanup();
1179
+ const calls: string[] = [];
1180
+ let statuses = 0;
1181
+ let finalizes = 0;
1182
+ const { controller } = runtime(fakeNative({
1183
+ targetStatus: async () => {
1184
+ calls.push("status");
1185
+ statuses += 1;
1186
+ return statuses === 1 ? beforeCapture : afterCapture;
1187
+ },
1188
+ captureEvidence: async (request) => {
1189
+ calls.push("capture-evidence");
1190
+ assert.equal(request.outcome, outcome);
1191
+ return capturedCorrectionEvidence(beforeCapture, outcome, outcome === "passed" ? "1" : outcome === "verification_failed" ? "2" : "3");
1192
+ },
1193
+ finalize: async () => {
1194
+ calls.push("finalize");
1195
+ finalizes += 1;
1196
+ return { lineageId: beforeCapture.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r-final" };
1197
+ },
1198
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
1199
+ const validation = outcome === "passed" ? {
1200
+ request_hash: "9".repeat(64), correction_ids: [],
1201
+ original_criteria: { passed: true, evidence: ["acceptance passes"] },
1202
+ correction_regression: { passed: true, evidence: ["regression passes"] },
1203
+ fix_caused_findings: [], follow_ups: [],
1204
+ } : undefined;
1205
+ const result = await controller.execute(`correction-${outcome}`, {
1206
+ operation: "finalize",
1207
+ lineageId: beforeCapture.authority!.lineageId,
1208
+ input: JSON.stringify({ final_evidence: `evidence: ${outcome}`, final_verification_outcome: outcome, ...(validation === undefined ? {} : { validation }) }),
1209
+ }, undefined, undefined, context(cwd));
1210
+ const details = result.details as Record<string, unknown>;
1211
+ assert.deepEqual(calls.slice(0, 3), ["status", "capture-evidence", "status"], "STATUS must collect evidence before targeted validation can run");
1212
+ assert.equal((details.correction_step as { kind?: string } | undefined)?.kind, expectedKind);
1213
+ assert.equal(finalizes, outcome === "passed" ? 1 : 0);
1214
+ if (outcome === "passed") assert.deepEqual(calls, ["status", "capture-evidence", "status", "finalize"]);
1215
+ else assert.deepEqual(calls, ["status", "capture-evidence", "status"]);
1216
+ });
1217
+ }
1218
+ });
1219
+
1220
+ test("production correction routing fails closed when targeted validation is offered before evidence capture", async (t) => {
1221
+ const cwd = repository(t);
1222
+ writeFileSync(join(cwd, "app.ts"), "export const corrected = true;\n");
1223
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
1224
+ const premature = bindTargetedValidation(targetStatusFixture({ lineageId: "premature-validation", authorityState: "validating", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths }));
1225
+ frozen.cleanup();
1226
+ let captures = 0;
1227
+ let finalizes = 0;
1228
+ const { controller } = runtime(fakeNative({
1229
+ targetStatus: async () => premature,
1230
+ captureEvidence: async () => { captures += 1; return capturedCorrectionEvidence(premature, "passed", "4"); },
1231
+ finalize: async () => { finalizes += 1; return { lineageId: "premature-validation", state: "approved", action: "approved", storeRevision: "r1" }; },
1232
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
1233
+ const result = await controller.execute("premature-targeted-validation", {
1234
+ operation: "finalize",
1235
+ lineageId: "premature-validation",
1236
+ input: JSON.stringify({
1237
+ final_evidence: "evidence first",
1238
+ final_verification_outcome: "passed",
1239
+ validation: { request_hash: "9".repeat(64), correction_ids: [], original_criteria: { passed: true, evidence: ["passes"] }, correction_regression: { passed: true, evidence: ["passes"] }, fix_caused_findings: [], follow_ups: [] },
1240
+ }),
1241
+ }, undefined, undefined, context(cwd));
1242
+ assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
1243
+ assert.equal(captures, 0);
1244
+ assert.equal(finalizes, 0);
1245
+ });
1246
+
1247
+ test("production correction routing rejects a second capture that reuses failed evidence identity", async (t) => {
1248
+ const cwd = repository(t);
1249
+ writeFileSync(join(cwd, "app.ts"), "export const corrected = 1;\n");
1250
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
1251
+ const status = bindCorrectionCollection(targetStatusFixture({ lineageId: "distinct-evidence", authorityState: "correction_required", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths }));
1252
+ frozen.cleanup();
1253
+ let captures = 0;
1254
+ let finalizes = 0;
1255
+ const candidateViews = new CandidateViewRegistry();
1256
+ const { controller } = runtime(fakeNative({
1257
+ targetStatus: async () => status,
1258
+ captureEvidence: async () => {
1259
+ captures += 1;
1260
+ return capturedCorrectionEvidence(status, "verification_failed", "5");
1261
+ },
1262
+ finalize: async () => { finalizes += 1; return { lineageId: "distinct-evidence", state: "approved", action: "approved", storeRevision: "r1" }; },
1263
+ }), undefined, undefined, undefined, candidateViews);
1264
+ const request = {
1265
+ operation: "finalize",
1266
+ lineageId: "distinct-evidence",
1267
+ input: JSON.stringify({ final_evidence: "verification failed", final_verification_outcome: "verification_failed" }),
1268
+ };
1269
+ const first = await controller.execute("distinct-first", request, undefined, undefined, context(cwd));
1270
+ assert.equal(((first.details as { correction_step?: { kind?: string } }).correction_step?.kind), "recapture-required");
1271
+ writeFileSync(join(cwd, "app.ts"), "export const corrected = 2;\n");
1272
+ const changed = new CandidateViewRegistry().create({ contributorRoot: cwd });
1273
+ status.projection.currentCandidateTree = changed.candidateTree;
1274
+ status.projection.paths = changed.paths;
1275
+ changed.cleanup();
1276
+ const second = await controller.execute("distinct-second", request, undefined, undefined, context(cwd));
1277
+ assert.equal((second.details as { outcome?: string }).outcome, "native-operation-failed");
1278
+ assert.match(JSON.stringify(second.details), /correction-evidence-replaced/);
1279
+ assert.equal(captures, 2);
1280
+ assert.equal(finalizes, 0);
1281
+ candidateViews.cleanupTerminal("distinct-evidence", "escalated");
1282
+ execFileSync("chmod", ["-R", "u+w", cwd]);
1283
+ });
1284
+
941
1285
  test("native FINALIZE derives and requires the trusted refuter request before invoking native FINALIZE", async (t) => {
942
1286
  const cwd = repository(t);
943
1287
  let finalizes = 0;
@@ -1555,6 +1899,35 @@ test("native allow registers one authorization and bash-time revalidation consum
1555
1899
  assert.equal(validates, 2);
1556
1900
  });
1557
1901
 
1902
+ test("native VALIDATE delivery disabled/unmanaged renders as a successful skipped envelope before the maintainer-exception check, minting no authorization", async (t) => {
1903
+ const cwd = repository(t);
1904
+ let validations = 0;
1905
+ const { controller } = runtime(fakeNative({
1906
+ validate: async () => {
1907
+ validations += 1;
1908
+ return {
1909
+ allowed: false,
1910
+ result: "invalidated",
1911
+ action: "repository-policy",
1912
+ reason: "review-driven development is disabled and no receipt governs this candidate, so delivery follows ordinary repository policy",
1913
+ gateContext: nativeGateContext("native-lineage", "r1", git(cwd, "write-tree")),
1914
+ delivery: "disabled/unmanaged",
1915
+ };
1916
+ },
1917
+ targetStatus: async () => targetStatusFixture({ lineageId: "native-lineage", baseTree: git(cwd, "rev-parse", "HEAD^{tree}"), currentCandidateTree: git(cwd, "write-tree"), paths: [] }),
1918
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
1919
+ const command = "git commit -m native";
1920
+ const validated = await controller.execute("disabled-delivery", { operation: "validate", lineageId: "native-lineage", idempotencyKey: "disabled-delivery", command, input: "{}" }, undefined, undefined, context(cwd));
1921
+ const details = validated.details as Record<string, unknown>;
1922
+ assert.equal(validations, 1);
1923
+ assert.equal(details.status, "skipped");
1924
+ assert.equal(details.outcome, "review-disabled-unmanaged-delivery");
1925
+ assert.equal((details.result as Record<string, unknown>).delivery, "disabled/unmanaged");
1926
+ assert.equal((details.result as Record<string, unknown>).allowed, false);
1927
+ assert.equal(details.maintainer_exception_request, undefined, "a repository-policy delivery skip must never mint a maintainer-exception request");
1928
+ assert.equal(details.authorization, undefined, "a repository-policy delivery skip must never mint an authorization");
1929
+ });
1930
+
1558
1931
  test("fresh candidate registry binds a resumed zero-lens native START through FINALIZE and pre-commit", async (t) => {
1559
1932
  const cwd = repository(t);
1560
1933
  writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
@@ -8,7 +8,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
8
8
  import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
9
9
  import type { NativeReviewCli } from "../lib/native-review-cli.ts";
10
10
  import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
11
- import type { ReviewStatusV1 } from "../lib/review-integration-v1.ts";
11
+ import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
12
12
 
13
13
  interface RegisteredTool {
14
14
  execute: (
@@ -90,11 +90,19 @@ function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
90
90
  };
91
91
  }
92
92
 
93
+ const UNSUPPORTED_REPAIR_ASSESSMENT: AuthorityRepairAssessmentV1 = {
94
+ schema: "gentle-ai.review-authority-repair-assessment/v1",
95
+ status: "unsupported",
96
+ counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 },
97
+ supportedOperations: ["review/complete-fix", "review/validate-fix"],
98
+ authorizationSchema: "gentle-ai.review-repair-authorization/v1",
99
+ };
100
+
93
101
  function targetStatusFixture(options: {
94
102
  applicability?: "current_target" | "unrelated";
95
- action?: ReviewStatusV1["action"];
103
+ action?: ReviewStatusV3["action"];
96
104
  lineageId?: string;
97
- } = {}): ReviewStatusV1 {
105
+ } = {}): ReviewStatusV3 {
98
106
  const applicability = options.applicability ?? "current_target";
99
107
  const action = options.action ?? (applicability === "current_target" ? "finalize" : "start");
100
108
  const lineageId = options.lineageId ?? "native-lineage";
@@ -114,15 +122,26 @@ function targetStatusFixture(options: {
114
122
  initialSnapshotIdentity: sha,
115
123
  currentSnapshotIdentity: sha,
116
124
  };
125
+ const rawRepair = {
126
+ schema: UNSUPPORTED_REPAIR_ASSESSMENT.schema,
127
+ status: UNSUPPORTED_REPAIR_ASSESSMENT.status,
128
+ counts: {
129
+ lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0,
130
+ eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0,
131
+ },
132
+ supported_operations: UNSUPPORTED_REPAIR_ASSESSMENT.supportedOperations,
133
+ authorization_schema: UNSUPPORTED_REPAIR_ASSESSMENT.authorizationSchema,
134
+ };
117
135
  const raw: Record<string, unknown> = {
118
- schema: "gentle-ai.review-integration.status/v1",
119
- contract: "gentle-ai.review-integration/v1",
136
+ schema: "gentle-ai.review-integration.status/v3",
137
+ contract: "gentle-ai.review-integration/v2",
120
138
  operation: "review.status",
121
139
  applicability,
122
140
  receipt: { status: applicability === "current_target" ? "expected_missing" : "not_applicable" },
123
141
  action,
124
142
  replayability: "not_replayable",
125
143
  target_identity: sha,
144
+ repair: rawRepair,
126
145
  projection: {
127
146
  schema: projection.schema,
128
147
  kind: projection.kind,
@@ -144,7 +163,7 @@ function targetStatusFixture(options: {
144
163
  raw.frozen = { tier: "medium", original_changed_lines: 2, correction_budget: 1 };
145
164
  }
146
165
  return {
147
- contract: "gentle-ai.review-integration/v1",
166
+ contract: "gentle-ai.review-integration/v2",
148
167
  applicability,
149
168
  ...(applicability === "current_target" ? { authority: { version: "compact-v2" as const, lineageId, state: "reviewing", generation: 1, revision: sha } } : {}),
150
169
  receipt: { status: applicability === "current_target" ? "expected_missing" : "not_applicable" },
@@ -153,6 +172,7 @@ function targetStatusFixture(options: {
153
172
  ...(applicability === "current_target" ? { frozen: { tier: "medium" as const, originalChangedLines: 2, correctionBudget: 1 } } : {}),
154
173
  targetIdentity: sha,
155
174
  projection,
175
+ repair: UNSUPPORTED_REPAIR_ASSESSMENT,
156
176
  candidates: [],
157
177
  raw,
158
178
  };