gentle-pi 2.1.2 → 2.3.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.
- package/README.md +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import {
|
|
6
|
+
REVIEW_ACKNOWLEDGED_SCHEMA,
|
|
7
|
+
decodeReviewAcknowledgedV1,
|
|
8
|
+
decodeReviewCapabilitiesV2,
|
|
9
|
+
decodeReviewConsentV2,
|
|
10
|
+
decodeReviewConsentV3,
|
|
11
|
+
decodeReviewFailureV2,
|
|
12
|
+
decodeReviewLastEventClosureV1,
|
|
13
|
+
decodeReviewNextTransitionV3,
|
|
14
|
+
decodeReviewRepairV2,
|
|
15
|
+
decodeReviewResultArtifactV2,
|
|
16
|
+
decodeReviewStartV3,
|
|
17
|
+
decodeReviewStartV4,
|
|
18
|
+
decodeReviewStatusV3,
|
|
19
|
+
} from "../lib/review-integration-v2.ts";
|
|
20
|
+
|
|
21
|
+
const DEV_FIXTURES = join(process.cwd(), "tests", "fixtures", "devbinary");
|
|
22
|
+
const V2_FIXTURES = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
|
|
23
|
+
const CAPTURED_DIGEST = "ffc91d8fa79c869aba9aa3d1ec80edebb5b1744e5a06fef75d4c8b73c0e46bc1";
|
|
24
|
+
|
|
25
|
+
type JsonObject = Record<string, unknown>;
|
|
26
|
+
const clone = <T>(value: T): T => structuredClone(value);
|
|
27
|
+
const sha = (fill: string): string => `sha256:${fill.repeat(64)}`;
|
|
28
|
+
|
|
29
|
+
function fixture(root: string, name: string): unknown {
|
|
30
|
+
return JSON.parse(readFileSync(join(root, name), "utf8"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function currentStatusFixture(name: string): Record<string, unknown> {
|
|
34
|
+
const body = structuredClone(fixture(DEV_FIXTURES, name)) as Record<string, unknown>;
|
|
35
|
+
if (body.action === "finalize") {
|
|
36
|
+
body.action = "stop";
|
|
37
|
+
const transition = body.next_transition as Record<string, unknown> | undefined;
|
|
38
|
+
if (transition?.kind === "execute") {
|
|
39
|
+
delete body.next_transition;
|
|
40
|
+
delete body.forecast;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return body;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function initialIntendedUntrackedStatusV6(): JsonObject {
|
|
47
|
+
const body = currentStatusFixture("status-v5.captured.json");
|
|
48
|
+
const projection = body.projection as JsonObject;
|
|
49
|
+
const schema = "gentle-ai.review-intended-untracked-selection/v1";
|
|
50
|
+
body.schema = "gentle-ai.review-integration.status/v6";
|
|
51
|
+
body.forecast = {
|
|
52
|
+
horizon: "partial", steps: [{ step: 1, kind: "collect", reason_code: "intended_untracked_selection_required", description: "initial intended-untracked selection required" }],
|
|
53
|
+
};
|
|
54
|
+
body.next_transition = {
|
|
55
|
+
kind: "collect",
|
|
56
|
+
reason_code: "intended_untracked_selection_required",
|
|
57
|
+
collect: { inputs: [{
|
|
58
|
+
name: "intended_untracked_selection", schema, capture_operation: "external.select_intended_untracked",
|
|
59
|
+
arguments: [
|
|
60
|
+
["target_identity", body.target_identity], ["projection", "workspace"], ["base_tree", projection.base_tree],
|
|
61
|
+
["candidate_tree", projection.current_candidate_tree], ["eligible_paths_json", '["docs/selected.md"]'], ["expected_untracked_inventory", sha("e")],
|
|
62
|
+
].map(([name, value]) => ({ name, value })),
|
|
63
|
+
submission: {
|
|
64
|
+
operation_token: "status",
|
|
65
|
+
argument_tokens: ["--contract=gentle-ai.review-integration/v2", "--next-transition=true", "--agent=pi", "--projection=workspace", "--intended-untracked-selection={{value}}"],
|
|
66
|
+
value: { slot: "intended_untracked_selection", domain: "schema_bound_json", schema, substitution_location: 4 },
|
|
67
|
+
},
|
|
68
|
+
}] },
|
|
69
|
+
};
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test("captured capabilities retain their exact schema identity", () => {
|
|
74
|
+
const capabilities = decodeReviewCapabilitiesV2(
|
|
75
|
+
fixture(DEV_FIXTURES, "capabilities-v2.2.captured.json"),
|
|
76
|
+
CAPTURED_DIGEST,
|
|
77
|
+
);
|
|
78
|
+
assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
|
|
79
|
+
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v5"), true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("historical v3 FINALIZE status remains rejected without rewriting fixture bytes", () => {
|
|
83
|
+
assert.throws(
|
|
84
|
+
() => decodeReviewStatusV3(fixture(V2_FIXTURES, "status.fixture.json")),
|
|
85
|
+
/receipt|finalize|status/i,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("v5 STATUS rejects malformed receipt status, identity, and extra fields", () => {
|
|
90
|
+
const unsupported = currentStatusFixture("status-v5.captured.json");
|
|
91
|
+
(unsupported.receipt as JsonObject).status = "retired";
|
|
92
|
+
assert.throws(() => decodeReviewStatusV3(unsupported), /status\.receipt\.status/);
|
|
93
|
+
|
|
94
|
+
const invalidIdentity = currentStatusFixture("status-v5.captured.json");
|
|
95
|
+
(invalidIdentity.receipt as JsonObject).identity = "not-a-sha256";
|
|
96
|
+
assert.throws(() => decodeReviewStatusV3(invalidIdentity), /status\.receipt\.identity/);
|
|
97
|
+
|
|
98
|
+
const extra = currentStatusFixture("status-v5.captured.json");
|
|
99
|
+
(extra.receipt as JsonObject).unexpected = true;
|
|
100
|
+
assert.throws(() => decodeReviewStatusV3(extra), /status\.receipt\.unexpected is not allowed/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("legacy-v1 v5 STATUS accepts only its two compatible receipt states", () => {
|
|
104
|
+
const legacyStatus = (status: string, identity?: string): JsonObject => {
|
|
105
|
+
const body = currentStatusFixture("status-v5-capture-result-submission.captured.json");
|
|
106
|
+
(body.authority as JsonObject).version = "legacy-v1";
|
|
107
|
+
body.receipt = { status, ...(identity === undefined ? {} : { identity }) };
|
|
108
|
+
delete body.frozen;
|
|
109
|
+
delete body.authority_target_identity;
|
|
110
|
+
return body;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
assert.deepEqual(decodeReviewStatusV3(legacyStatus("expected_missing")).receipt, { status: "expected_missing" });
|
|
114
|
+
assert.deepEqual(decodeReviewStatusV3(legacyStatus("present", sha("a"))).receipt, { status: "present", identity: sha("a") });
|
|
115
|
+
for (const status of ["publication_pending", "not_applicable"]) {
|
|
116
|
+
assert.throws(() => decodeReviewStatusV3(legacyStatus(status)), /legacy status receipt is incompatible/);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("captured terminal closure decodes without a compatibility status projection", () => {
|
|
121
|
+
const closure = decodeReviewLastEventClosureV1(
|
|
122
|
+
fixture(DEV_FIXTURES, "last-event-capture-result-approved.captured.json"),
|
|
123
|
+
);
|
|
124
|
+
assert.equal(closure.operation, "review/capture-result");
|
|
125
|
+
assert.equal(closure.state, "approved");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("derived and pinned capabilities remain separately schema-pinned", () => {
|
|
129
|
+
const derived = decodeReviewCapabilitiesV2(fixture(DEV_FIXTURES, "capabilities-v2.1.derived.json"), CAPTURED_DIGEST);
|
|
130
|
+
assert.equal(derived.schemas.has("gentle-ai.review-integration.consent/v3"), true);
|
|
131
|
+
const pinned = decodeReviewCapabilitiesV2(fixture(V2_FIXTURES, "capabilities.fixture.json"), "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
|
|
132
|
+
assert.equal(pinned.schemas.has("gentle-ai.review-integration.consent/v2"), true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("capabilities reject a protocol minor that disagrees with their exact identity", () => {
|
|
136
|
+
const body = fixture(DEV_FIXTURES, "capabilities-v2.2.captured.json") as Record<string, unknown>;
|
|
137
|
+
const mutated = structuredClone(body) as Record<string, unknown>;
|
|
138
|
+
(mutated.protocol as Record<string, unknown>).minor = 1;
|
|
139
|
+
assert.throws(() => decodeReviewCapabilitiesV2(mutated, CAPTURED_DIGEST), /protocol/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("captured v5 STATUS preserves its provider forecast and collect binding", () => {
|
|
143
|
+
const decoded = decodeReviewStatusV3(currentStatusFixture("status-v5.captured.json"));
|
|
144
|
+
assert.equal(decoded.forecast?.horizon, "partial");
|
|
145
|
+
assert.equal(decoded.nextTransition?.kind, "collect");
|
|
146
|
+
assert.equal(decoded.nextTransition?.collect?.inputs[0]?.captureOperation, "external.select_base_ref");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("v3 status identity rejects v5-only forecast fields", () => {
|
|
150
|
+
const body = currentStatusFixture("status-v5.captured.json");
|
|
151
|
+
body.schema = "gentle-ai.review-integration.status/v3";
|
|
152
|
+
delete body.receipt;
|
|
153
|
+
assert.throws(() => decodeReviewStatusV3(body), /forecast/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("captured v5 STATUS retains its opaque repository context reference", () => {
|
|
157
|
+
const decoded = decodeReviewStatusV3(currentStatusFixture("status-v5-repository-context.captured.json"));
|
|
158
|
+
assert.match(decoded.repositoryContext?.handle ?? "", /^rctx1_[0-9a-f]{64}$/);
|
|
159
|
+
assert.equal(decoded.repositoryContext?.targetIdentity, decoded.targetIdentity);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("the singular v5 capture-result submission normalizes to one typed value", () => {
|
|
163
|
+
const decoded = decodeReviewStatusV3(currentStatusFixture("status-v5-capture-result-submission.captured.json"));
|
|
164
|
+
const input = decoded.nextTransition?.collect?.inputs[0];
|
|
165
|
+
assert.equal(input?.captureOperation, "review.capture-result");
|
|
166
|
+
assert.deepEqual(input?.submission?.values, [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", schema: "https://gentle-ai.dev/schema/review/reviewer/v1", substitutionLocation: 7 }]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("the singular capture-result form rejects competing legacy values", () => {
|
|
170
|
+
const body = currentStatusFixture("status-v5-capture-result-submission.captured.json");
|
|
171
|
+
const input = (((body.next_transition as Record<string, unknown>).collect as Record<string, unknown>).inputs as Record<string, unknown>[])[0]!;
|
|
172
|
+
const submission = input.submission as Record<string, unknown>;
|
|
173
|
+
submission.values = [submission.value];
|
|
174
|
+
assert.throws(() => decodeReviewStatusV3(body), /value/);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("consent/v3 accepts the Pi runtime only when its exact agent binding matches", () => {
|
|
178
|
+
const captured = fixture(DEV_FIXTURES, "consent-v3.captured.json") as Record<string, unknown>;
|
|
179
|
+
const piConsent = decodeReviewConsentV3({ ...captured, agent: "pi" }, "pi");
|
|
180
|
+
assert.equal(piConsent.agent, "pi");
|
|
181
|
+
assert.throws(() => decodeReviewConsentV3(captured, "pi"), /consent\.agent/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("consent identities remain cross-decoder incompatible", () => {
|
|
185
|
+
const v3 = fixture(DEV_FIXTURES, "consent-v3.captured.json");
|
|
186
|
+
const v2 = fixture(V2_FIXTURES, "consent.fixture.json");
|
|
187
|
+
assert.throws(() => decodeReviewConsentV2(v3), /agent|schema/);
|
|
188
|
+
assert.throws(() => decodeReviewConsentV3(v2), /agent|schema/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("captured granted START keeps its repository-context event binding", () => {
|
|
192
|
+
const start = decodeReviewStartV3(fixture(DEV_FIXTURES, "start-v3-consent-granted.captured.json"));
|
|
193
|
+
assert.equal(start.state, "reviewing");
|
|
194
|
+
assert.match(start.repositoryContext?.eventId ?? "", /^sha256:[0-9a-f]{64}$/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("result artifacts retain mutually exclusive reference and path locators", () => {
|
|
198
|
+
const reference = decodeReviewResultArtifactV2(fixture(DEV_FIXTURES, "result-artifact-v2.captured.json"));
|
|
199
|
+
const path = decodeReviewResultArtifactV2(fixture(DEV_FIXTURES, "result-artifact-v2-path.captured.json"));
|
|
200
|
+
assert.match(reference.reference ?? "", /^rart1_[0-9a-f]{64}$/);
|
|
201
|
+
assert.equal(reference.path, undefined);
|
|
202
|
+
assert.equal(path.reference, undefined);
|
|
203
|
+
assert.equal(path.sha256, reference.sha256);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("result artifacts reject a second locator", () => {
|
|
207
|
+
const body = structuredClone(fixture(DEV_FIXTURES, "result-artifact-v2.captured.json")) as Record<string, unknown>;
|
|
208
|
+
body.path = "/store/reviewer-results/00-review-reliability.json";
|
|
209
|
+
assert.throws(() => decodeReviewResultArtifactV2(body), /exactly one/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("nonuniform role capture closures retain their exact upstream operation identities", () => {
|
|
213
|
+
assert.equal(decodeReviewLastEventClosureV1(fixture(DEV_FIXTURES, "last-event-capture-refuter-approved.captured.json")).operation, "review.capture-refuter");
|
|
214
|
+
assert.equal(decodeReviewLastEventClosureV1(fixture(DEV_FIXTURES, "last-event-capture-validation-approved.captured.json")).operation, "review/capture-validation");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("the derived capabilities/v2.1 payload decodes with protocol minor 1", () => {
|
|
218
|
+
const capabilities = decodeReviewCapabilitiesV2(fixture(DEV_FIXTURES, "capabilities-v2.1.derived.json"), CAPTURED_DIGEST);
|
|
219
|
+
assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
|
|
220
|
+
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v3"), true);
|
|
221
|
+
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v3"), true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("a status/v3 payload never carries a top-level repository context", () => {
|
|
225
|
+
const body = currentStatusFixture("status-v5-repository-context.captured.json");
|
|
226
|
+
body.schema = "gentle-ai.review-integration.status/v3";
|
|
227
|
+
delete body.receipt;
|
|
228
|
+
delete body.forecast;
|
|
229
|
+
assert.throws(() => decodeReviewStatusV3(body), /repository_context/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a v5 forecast requires the next transition and a coherent horizon", () => {
|
|
233
|
+
const orphanForecast = currentStatusFixture("status-v5.captured.json");
|
|
234
|
+
delete orphanForecast.next_transition;
|
|
235
|
+
assert.throws(() => decodeReviewStatusV3(orphanForecast), /forecast/);
|
|
236
|
+
const wrongHorizon = currentStatusFixture("status-v5.captured.json");
|
|
237
|
+
(wrongHorizon.forecast as JsonObject).horizon = "terminal";
|
|
238
|
+
assert.throws(() => decodeReviewStatusV3(wrongHorizon), /horizon/);
|
|
239
|
+
const badStep = currentStatusFixture("status-v5.captured.json");
|
|
240
|
+
((badStep.forecast as JsonObject).steps as JsonObject[])[0]!.step = 2;
|
|
241
|
+
assert.throws(() => decodeReviewStatusV3(badStep), /step/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("a v5 provider role task input decodes and is confined to external.run_provider_role", () => {
|
|
245
|
+
const body = currentStatusFixture("status-v5.captured.json");
|
|
246
|
+
const transition = body.next_transition as JsonObject;
|
|
247
|
+
const input = ((transition.collect as JsonObject).inputs as JsonObject[])[0]!;
|
|
248
|
+
input.name = "refuter_batch";
|
|
249
|
+
input.schema = "https://gentle-ai.dev/schema/review/refuter/v1";
|
|
250
|
+
input.capture_operation = "external.run_provider_role";
|
|
251
|
+
input.provider_task = { agent: "review-refuter", role: "refuter", prompt: "GENTLE_AI_REVIEW_BINDING {}" };
|
|
252
|
+
const decoded = decodeReviewStatusV3(body);
|
|
253
|
+
assert.deepEqual(decoded.nextTransition?.collect?.inputs[0]?.providerTask, { agent: "review-refuter", role: "refuter", prompt: "GENTLE_AI_REVIEW_BINDING {}" });
|
|
254
|
+
const missing = clone(body);
|
|
255
|
+
delete ((((missing.next_transition as JsonObject).collect as JsonObject).inputs as JsonObject[])[0]!).provider_task;
|
|
256
|
+
assert.throws(() => decodeReviewStatusV3(missing), /provider_task/);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("the v3 identity keeps rejecting the singular capture-result value form", () => {
|
|
260
|
+
const body = currentStatusFixture("status-v5-capture-result-submission.captured.json");
|
|
261
|
+
body.schema = "gentle-ai.review-integration.status/v3";
|
|
262
|
+
delete body.receipt;
|
|
263
|
+
delete body.forecast;
|
|
264
|
+
delete body.repository_context;
|
|
265
|
+
assert.throws(() => decodeReviewStatusV3(body), /values is required/);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("the v3 next transition keeps rejecting every v5-only surface", () => {
|
|
269
|
+
const v5 = currentStatusFixture("status-v5.captured.json");
|
|
270
|
+
v5.schema = "gentle-ai.review-integration.status/v3";
|
|
271
|
+
delete v5.receipt;
|
|
272
|
+
delete v5.forecast;
|
|
273
|
+
const transition = v5.next_transition as JsonObject;
|
|
274
|
+
transition.correction_request = { schema: "gentle-ai.review-correction-plan-request/v1", request_hash: sha("a") };
|
|
275
|
+
assert.throws(() => decodeReviewStatusV3(v5), /not allowed|correction_request/);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("consent/v3 keeps every v2 semantic guard and rejects agents outside the fixed runtime contract", () => {
|
|
279
|
+
const base = fixture(DEV_FIXTURES, "consent-v3.captured.json") as JsonObject;
|
|
280
|
+
for (const agent of ["kilocode", "future-runtime", ""] as const) {
|
|
281
|
+
assert.throws(() => decodeReviewConsentV3({ ...clone(base), agent }), /agent/);
|
|
282
|
+
}
|
|
283
|
+
const swapped = clone(base);
|
|
284
|
+
swapped.choices = [...(swapped.choices as JsonObject[])].reverse();
|
|
285
|
+
assert.throws(() => decodeReviewConsentV3(swapped), /answer/);
|
|
286
|
+
const wrongTarget = clone(base);
|
|
287
|
+
wrongTarget.target_identity = sha("d");
|
|
288
|
+
assert.throws(() => decodeReviewConsentV3(wrongTarget), /target|invocation/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("start/v3 repository context event fields stay optional and exact", () => {
|
|
292
|
+
const pinned = decodeReviewStartV3(fixture(V2_FIXTURES, "start.fixture.json"));
|
|
293
|
+
assert.equal(pinned.repositoryContext?.eventId, undefined);
|
|
294
|
+
assert.equal(pinned.repositoryContext?.outcome, undefined);
|
|
295
|
+
const badEvent = clone(fixture(DEV_FIXTURES, "start-v3-consent-granted.captured.json") as JsonObject);
|
|
296
|
+
(badEvent.repository_context as JsonObject).event_id = "not-a-digest";
|
|
297
|
+
assert.throws(() => decodeReviewStartV3(badEvent), /event_id/);
|
|
298
|
+
const badOutcome = clone(fixture(DEV_FIXTURES, "start-v3-consent-granted.captured.json") as JsonObject);
|
|
299
|
+
(badOutcome.repository_context as JsonObject).outcome = "unheard-of";
|
|
300
|
+
assert.throws(() => decodeReviewStartV3(badOutcome), /outcome/);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("a result artifact rejects unknown keys and weakened bindings", () => {
|
|
304
|
+
const base = fixture(DEV_FIXTURES, "result-artifact-v2.captured.json") as JsonObject;
|
|
305
|
+
const extra = clone(base);
|
|
306
|
+
extra.unadvertised = true;
|
|
307
|
+
assert.throws(() => decodeReviewResultArtifactV2(extra), /not allowed/);
|
|
308
|
+
const wrongCapability = clone(base);
|
|
309
|
+
wrongCapability.capability = "review.result_artifact";
|
|
310
|
+
assert.throws(() => decodeReviewResultArtifactV2(wrongCapability), /capability/);
|
|
311
|
+
const foreignLocator = clone(base);
|
|
312
|
+
foreignLocator.reference = `rref1_${"b".repeat(64)}`;
|
|
313
|
+
assert.throws(() => decodeReviewResultArtifactV2(foreignLocator), /reference/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
function reviewingStartV4(action: "created" | "replayed" = "created"): JsonObject {
|
|
317
|
+
const body = clone(fixture(DEV_FIXTURES, "start-v3-consent-granted.captured.json") as JsonObject);
|
|
318
|
+
body.schema = "gentle-ai.review-integration.start/v4";
|
|
319
|
+
body.action = action;
|
|
320
|
+
const targetIdentity = (body.repository_context as JsonObject).target_identity as string;
|
|
321
|
+
const baseTree = body.base_tree as string;
|
|
322
|
+
body.next_transition = {
|
|
323
|
+
kind: "execute",
|
|
324
|
+
reason_code: "review_status_required",
|
|
325
|
+
execute: {
|
|
326
|
+
operation: "review.status",
|
|
327
|
+
arguments: [
|
|
328
|
+
{ name: "contract", value: "gentle-ai.review-integration/v2", token: "--contract=gentle-ai.review-integration/v2" },
|
|
329
|
+
{ name: "next-transition", value: "true", token: "--next-transition=true" },
|
|
330
|
+
{ name: "lineage", value: body.lineage_id, token: `--lineage=${body.lineage_id}` },
|
|
331
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
332
|
+
{ name: "base-ref", value: baseTree, token: `--base-ref=${baseTree}` },
|
|
333
|
+
{ name: "committed-only", value: "true", token: "--committed-only=true" },
|
|
334
|
+
],
|
|
335
|
+
preconditions: [],
|
|
336
|
+
binding: { target_identity: targetIdentity },
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
const execute = (body.next_transition as JsonObject).execute as JsonObject;
|
|
340
|
+
execute.selector_arguments = (execute.arguments as JsonObject[]).slice(-2).map(clone);
|
|
341
|
+
return body;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
test("capabilities/v2.3 retains status/v5 while v2.4 requires status/v6", () => {
|
|
345
|
+
const v23 = clone(fixture(DEV_FIXTURES, "capabilities-v2.2.captured.json") as JsonObject);
|
|
346
|
+
v23.schema = "gentle-ai.review-integration.capabilities/v2.3";
|
|
347
|
+
(v23.protocol as JsonObject).minor = 3;
|
|
348
|
+
v23.schemas = (v23.schemas as string[]).map((schema) => schema
|
|
349
|
+
.replace("capabilities/v2.2", "capabilities/v2.3")
|
|
350
|
+
.replace("start/v3", "start/v4"));
|
|
351
|
+
// The v2.3 provider contract retired these three mandatory features; the
|
|
352
|
+
// pinned v2.5.0-rc.3 runtime no longer advertises them.
|
|
353
|
+
const features = v23.features as JsonObject;
|
|
354
|
+
features.mandatory = (features.mandatory as JsonObject[]).filter((feature) =>
|
|
355
|
+
!["exact_receipt_replay", "five_delivery_gates", "sdd_receipt_binding"].includes(feature.name as string));
|
|
356
|
+
const decoded = decodeReviewCapabilitiesV2(v23, CAPTURED_DIGEST);
|
|
357
|
+
assert.equal(decoded.schemas.has("gentle-ai.review-integration.start/v4"), true);
|
|
358
|
+
assert.equal(decoded.schemas.has("gentle-ai.review-integration.start/v3"), false);
|
|
359
|
+
assert.equal(decoded.schemas.has("gentle-ai.review-integration.status/v5"), true);
|
|
360
|
+
|
|
361
|
+
const v24 = clone(v23);
|
|
362
|
+
v24.schema = "gentle-ai.review-integration.capabilities/v2.4";
|
|
363
|
+
(v24.protocol as JsonObject).minor = 4;
|
|
364
|
+
v24.schemas = [
|
|
365
|
+
...(v24.schemas as string[]).map((schema) => schema
|
|
366
|
+
.replace("capabilities/v2.3", "capabilities/v2.4")
|
|
367
|
+
.replace("status/v5", "status/v6")),
|
|
368
|
+
"gentle-ai.review-intended-untracked-selection/v1",
|
|
369
|
+
];
|
|
370
|
+
const v24Decoded = decodeReviewCapabilitiesV2(v24, CAPTURED_DIGEST);
|
|
371
|
+
assert.equal(v24Decoded.schemas.has("gentle-ai.review-integration.status/v6"), true);
|
|
372
|
+
assert.equal(v24Decoded.schemas.has("gentle-ai.review-integration.status/v5"), false);
|
|
373
|
+
|
|
374
|
+
const missingStatusV6 = clone(v24);
|
|
375
|
+
missingStatusV6.schemas = (missingStatusV6.schemas as string[]).map((schema) => schema.replace("status/v6", "status/v5"));
|
|
376
|
+
assert.throws(() => decodeReviewCapabilitiesV2(missingStatusV6, CAPTURED_DIGEST), /status\/v6/);
|
|
377
|
+
|
|
378
|
+
// Distinguishing case for the v2.3 retired-schema filter: the real rc.3
|
|
379
|
+
// provider no longer advertises these three identities, so a v2.3
|
|
380
|
+
// advertisement WITHOUT them must still negotiate. A no-op filter would
|
|
381
|
+
// keep requiring them and fail the superset assertion here.
|
|
382
|
+
const retiredSchemas = ["gentle-ai.review-final-verification-incident/v1", "gentle-ai.review-receipt/v2", "gentle-ai.review-verification-evidence/v2"];
|
|
383
|
+
const retiredRemoved = clone(v23);
|
|
384
|
+
retiredRemoved.schemas = (retiredRemoved.schemas as string[]).filter((schema) => !retiredSchemas.includes(schema));
|
|
385
|
+
assert.equal((v23.schemas as string[]).length - (retiredRemoved.schemas as string[]).length, 3, "fixture must actually carry and then remove the three retired identities");
|
|
386
|
+
const relaxed = decodeReviewCapabilitiesV2(retiredRemoved, CAPTURED_DIGEST);
|
|
387
|
+
for (const schema of retiredSchemas) assert.equal(relaxed.schemas.has(schema), false);
|
|
388
|
+
|
|
389
|
+
// Earlier minors keep the full common requirement: the same removal under
|
|
390
|
+
// the v2.2 identity must fail closed.
|
|
391
|
+
const v22Retired = clone(fixture(DEV_FIXTURES, "capabilities-v2.2.captured.json") as JsonObject);
|
|
392
|
+
v22Retired.schemas = (v22Retired.schemas as string[]).filter((schema) => !retiredSchemas.includes(schema));
|
|
393
|
+
assert.throws(() => decodeReviewCapabilitiesV2(v22Retired, CAPTURED_DIGEST), /schema/);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("status/v6 decodes and enforces the intended-untracked selection submission", () => {
|
|
397
|
+
const decoded = decodeReviewStatusV3(initialIntendedUntrackedStatusV6());
|
|
398
|
+
const input = decoded.nextTransition?.collect?.inputs[0];
|
|
399
|
+
assert.deepEqual([input?.schema, input?.captureOperation, input?.arguments, input?.submission], [
|
|
400
|
+
"gentle-ai.review-intended-untracked-selection/v1", "external.select_intended_untracked", [
|
|
401
|
+
{ name: "target_identity", value: decoded.targetIdentity }, { name: "projection", value: "workspace" },
|
|
402
|
+
{ name: "base_tree", value: decoded.projection.baseTree }, { name: "candidate_tree", value: decoded.projection.currentCandidateTree },
|
|
403
|
+
{ name: "eligible_paths_json", value: '["docs/selected.md"]' }, { name: "expected_untracked_inventory", value: sha("e") },
|
|
404
|
+
], {
|
|
405
|
+
operationToken: "status",
|
|
406
|
+
argumentTokens: ["--contract=gentle-ai.review-integration/v2", "--next-transition=true", "--agent=pi", "--projection=workspace", "--intended-untracked-selection={{value}}"],
|
|
407
|
+
values: [{ slot: "intended_untracked_selection", domain: "schema_bound_json", schema: "gentle-ai.review-intended-untracked-selection/v1", substitutionLocation: 4 }],
|
|
408
|
+
},
|
|
409
|
+
]);
|
|
410
|
+
const submission = (body: JsonObject) => (((body.next_transition as JsonObject).collect as JsonObject).inputs as JsonObject[])[0]!.submission as JsonObject;
|
|
411
|
+
const cases: Array<[string, (value: JsonObject) => void, RegExp]> = [
|
|
412
|
+
["operation", (value) => { value.operation_token = "start"; }, /operation/],
|
|
413
|
+
["competing values", (value) => { value.values = [clone(value.value)]; }, /value/],
|
|
414
|
+
["slot", (value) => { delete (value.value as JsonObject).slot; }, /slot/],
|
|
415
|
+
["schema", (value) => { (value.value as JsonObject).schema = "gentle-ai.review-intended-untracked-selection/v2"; }, /schema/],
|
|
416
|
+
["placeholder", (value) => { (value.argument_tokens as string[])[4] = "--intended-untracked-selection={{selection}}"; }, /value|substitution/],
|
|
417
|
+
["location", (value) => { (value.value as JsonObject).substitution_location = 3; }, /substitution/],
|
|
418
|
+
];
|
|
419
|
+
for (const [name, mutate, pattern] of cases) {
|
|
420
|
+
const body = initialIntendedUntrackedStatusV6();
|
|
421
|
+
mutate(submission(body));
|
|
422
|
+
assert.throws(() => decodeReviewStatusV3(body), pattern, name);
|
|
423
|
+
}
|
|
424
|
+
const v5 = initialIntendedUntrackedStatusV6();
|
|
425
|
+
v5.schema = "gentle-ai.review-integration.status/v5";
|
|
426
|
+
assert.throws(() => decodeReviewStatusV3(v5), /submission/);
|
|
427
|
+
|
|
428
|
+
const collectInput = (body: JsonObject) => (((body.next_transition as JsonObject).collect as JsonObject).inputs as JsonObject[])[0]!;
|
|
429
|
+
const artifactSubject = { schema: "gentle-ai.review-artifact-subject/v2", subject_hash: sha("a"), lineage_id: "review-test", authority_revision: sha("b"), target_identity: sha("c"), base_tree: "a".repeat(40), candidate_tree: "b".repeat(40), changed_path_manifest_sha256: sha("d"), lens: "review-risk", selected_order: 0 };
|
|
430
|
+
const incompatibleCaptures: ReadonlyArray<readonly [string, JsonObject]> = [
|
|
431
|
+
["review.capture-result", { schema: "https://gentle-ai.dev/schema/review/reviewer/v1", artifact_subject: artifactSubject, base_tree: "a".repeat(40), candidate_tree: "b".repeat(40), changed_path_manifest: [{ path: "x.ts", status: "M", old_mode: "100644", new_mode: "100644", deleted: false, type_changed: false, mode_only: false, intended_untracked: false }] }],
|
|
432
|
+
["review.capture-correction-plan", { schema: "gentle-ai.review-correction-plan/v1" }],
|
|
433
|
+
];
|
|
434
|
+
for (const [captureOperation, fields] of incompatibleCaptures) {
|
|
435
|
+
const body = initialIntendedUntrackedStatusV6();
|
|
436
|
+
Object.assign(collectInput(body), { capture_operation: captureOperation, ...fields });
|
|
437
|
+
assert.throws(() => decodeReviewStatusV3(body), /operation_token/, captureOperation);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const mismatchedBindings: ReadonlyArray<readonly [string, string]> = [
|
|
441
|
+
["name", "renamed_selection"],
|
|
442
|
+
["schema", "gentle-ai.review-intended-untracked-selection/v0"],
|
|
443
|
+
];
|
|
444
|
+
for (const [field, value] of mismatchedBindings) {
|
|
445
|
+
const body = initialIntendedUntrackedStatusV6();
|
|
446
|
+
collectInput(body)[field] = value;
|
|
447
|
+
assert.throws(() => decodeReviewStatusV3(body), /intended_untracked_selection binding/, field);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("START/v4 accepts only its reviewing status continuation and preserves v3 strictness", () => {
|
|
452
|
+
for (const action of ["created", "replayed"] as const) {
|
|
453
|
+
const start = reviewingStartV4(action);
|
|
454
|
+
const decoded = decodeReviewStartV4(start);
|
|
455
|
+
assert.equal(decoded.nextTransition?.execute?.operation, "review.status");
|
|
456
|
+
assert.deepEqual(decoded.nextTransition?.execute?.arguments, [
|
|
457
|
+
{ name: "contract", value: "gentle-ai.review-integration/v2", token: "--contract=gentle-ai.review-integration/v2" },
|
|
458
|
+
{ name: "next-transition", value: "true", token: "--next-transition=true" },
|
|
459
|
+
{ name: "lineage", value: decoded.lineageId, token: `--lineage=${decoded.lineageId}` },
|
|
460
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
461
|
+
{ name: "base-ref", value: decoded.baseTree, token: `--base-ref=${decoded.baseTree}` },
|
|
462
|
+
{ name: "committed-only", value: "true", token: "--committed-only=true" },
|
|
463
|
+
]);
|
|
464
|
+
assert.deepEqual(decoded.nextTransition?.execute?.selectorArguments, [
|
|
465
|
+
{ name: "base-ref", value: decoded.baseTree, token: `--base-ref=${decoded.baseTree}` },
|
|
466
|
+
{ name: "committed-only", value: "true", token: "--committed-only=true" },
|
|
467
|
+
]);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const v3 = reviewingStartV4();
|
|
471
|
+
v3.schema = "gentle-ai.review-integration.start/v3";
|
|
472
|
+
assert.throws(() => decodeReviewStartV3(v3), /next_transition is not allowed/);
|
|
473
|
+
|
|
474
|
+
const wrongOperation = reviewingStartV4();
|
|
475
|
+
((wrongOperation.next_transition as JsonObject).execute as JsonObject).operation = "review.start";
|
|
476
|
+
assert.throws(() => decodeReviewStartV4(wrongOperation), /review.status/);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("START/v4 falls back to the frozen overlay target when repository_context is absent", () => {
|
|
480
|
+
// A replayed reviewing START must not carry repository_context, so the
|
|
481
|
+
// binding assert has to derive the frozen target from the overlay identity.
|
|
482
|
+
const start = reviewingStartV4("replayed");
|
|
483
|
+
const execute = (start.next_transition as JsonObject).execute as JsonObject;
|
|
484
|
+
const targetIdentity = (start.repository_context as JsonObject).target_identity as string;
|
|
485
|
+
delete start.repository_context;
|
|
486
|
+
start.target_mode = "base-workspace-overlay";
|
|
487
|
+
start.target_identity = targetIdentity;
|
|
488
|
+
const arguments_ = execute.arguments as JsonObject[];
|
|
489
|
+
const committedOnly = arguments_.find((argument) => argument.name === "committed-only")!;
|
|
490
|
+
committedOnly.name = "workspace-overlay";
|
|
491
|
+
committedOnly.token = "--workspace-overlay=true";
|
|
492
|
+
execute.selector_arguments = arguments_.slice(-2).map(clone);
|
|
493
|
+
const decoded = decodeReviewStartV4(start);
|
|
494
|
+
assert.equal(decoded.targetIdentity, targetIdentity);
|
|
495
|
+
assert.equal(decoded.repositoryContext, undefined);
|
|
496
|
+
assert.equal(decoded.nextTransition?.execute?.binding.targetIdentity, targetIdentity);
|
|
497
|
+
|
|
498
|
+
const unbound = reviewingStartV4("replayed");
|
|
499
|
+
delete unbound.repository_context;
|
|
500
|
+
assert.throws(() => decodeReviewStartV4(unbound), /repository_context is required/);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("START/v4 rejects status vectors that drift from frozen bindings", () => {
|
|
504
|
+
const bindingTarget = reviewingStartV4();
|
|
505
|
+
(((bindingTarget.next_transition as JsonObject).execute as JsonObject).binding as JsonObject).target_identity = sha("d");
|
|
506
|
+
assert.throws(() => decodeReviewStartV4(bindingTarget), /binding/);
|
|
507
|
+
|
|
508
|
+
for (const [name, value] of [["lineage", "review-other"], ["base-ref", "a".repeat(40)]] as const) {
|
|
509
|
+
const start = reviewingStartV4();
|
|
510
|
+
const arguments_ = ((start.next_transition as JsonObject).execute as JsonObject).arguments as JsonObject[];
|
|
511
|
+
arguments_.find((argument) => argument.name === name)!.value = value;
|
|
512
|
+
assert.throws(() => decodeReviewStartV4(start), /binding/);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const [name, value, token] of [["projection", "staged", "--projection=staged"], ["target", sha("d"), `--target=${sha("d")}`]] as const) {
|
|
516
|
+
const start = reviewingStartV4();
|
|
517
|
+
const arguments_ = ((start.next_transition as JsonObject).execute as JsonObject).arguments as JsonObject[];
|
|
518
|
+
arguments_.push({ name, value, token });
|
|
519
|
+
assert.throws(() => decodeReviewStartV4(start), /binding/);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const bindingLineage = reviewingStartV4();
|
|
523
|
+
(((bindingLineage.next_transition as JsonObject).execute as JsonObject).binding as JsonObject).lineage_id = "review-other";
|
|
524
|
+
assert.throws(() => decodeReviewStartV4(bindingLineage), /binding/);
|
|
525
|
+
|
|
526
|
+
const outerTarget = reviewingStartV4();
|
|
527
|
+
outerTarget.target_mode = "base-workspace-overlay";
|
|
528
|
+
outerTarget.target_identity = sha("d");
|
|
529
|
+
assert.throws(() => decodeReviewStartV4(outerTarget), /binding/);
|
|
530
|
+
|
|
531
|
+
const artifactTarget = reviewingStartV4();
|
|
532
|
+
(artifactTarget.artifact_subjects as JsonObject[])[0]!.target_identity = sha("d");
|
|
533
|
+
assert.throws(() => decodeReviewStartV4(artifactTarget), /binding/);
|
|
534
|
+
|
|
535
|
+
const duplicate = reviewingStartV4();
|
|
536
|
+
const arguments_ = ((duplicate.next_transition as JsonObject).execute as JsonObject).arguments as JsonObject[];
|
|
537
|
+
arguments_.push(clone(arguments_.find((argument) => argument.name === "lineage")!));
|
|
538
|
+
assert.throws(() => decodeReviewStartV4(duplicate), /binding/);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("START/v4 rejects incomplete or incoherent provider-owned status selectors", () => {
|
|
542
|
+
const execute = (start: JsonObject) => (start.next_transition as JsonObject).execute as JsonObject;
|
|
543
|
+
const rows = (start: JsonObject, field: "arguments" | "selector_arguments") => execute(start)[field] as JsonObject[];
|
|
544
|
+
for (const change of [
|
|
545
|
+
(start: JsonObject) => delete execute(start).selector_arguments,
|
|
546
|
+
(start: JsonObject) => rows(start, "selector_arguments").splice(1, 1),
|
|
547
|
+
(start: JsonObject) => {
|
|
548
|
+
for (const field of ["arguments", "selector_arguments"] as const) rows(start, field).find((row) => row.name === "committed-only")!.value = "false";
|
|
549
|
+
},
|
|
550
|
+
(start: JsonObject) => rows(start, "selector_arguments")[1]!.token = "--committed-only",
|
|
551
|
+
]) {
|
|
552
|
+
const start = reviewingStartV4();
|
|
553
|
+
change(start);
|
|
554
|
+
assert.throws(() => decodeReviewStartV4(start), /selector|binding/);
|
|
555
|
+
}
|
|
556
|
+
for (const [name, value] of [["contract", "other"], ["next-transition", "false"], ["lineage", "review-other"], ["agent", "other"]] as const) {
|
|
557
|
+
const missing = reviewingStartV4();
|
|
558
|
+
rows(missing, "arguments").splice(rows(missing, "arguments").findIndex((row) => row.name === name), 1);
|
|
559
|
+
assert.throws(() => decodeReviewStartV4(missing), /binding/);
|
|
560
|
+
const wrong = reviewingStartV4();
|
|
561
|
+
rows(wrong, "arguments").find((row) => row.name === name)!.value = value;
|
|
562
|
+
assert.throws(() => decodeReviewStartV4(wrong), /binding/);
|
|
563
|
+
}
|
|
564
|
+
const controlToken = reviewingStartV4(); rows(controlToken, "arguments")[0]!.token = "--contract=other";
|
|
565
|
+
assert.throws(() => decodeReviewStartV4(controlToken), /binding/);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("START/v4 closed approval keeps acknowledgement but forbids a continuation", () => {
|
|
569
|
+
const closed = clone(fixture(DEV_FIXTURES, "start-v3-zero-lens-closed.captured.json") as JsonObject);
|
|
570
|
+
closed.schema = "gentle-ai.review-integration.start/v4";
|
|
571
|
+
closed.acknowledgement = { provider_owned: true };
|
|
572
|
+
assert.deepEqual(decodeReviewStartV4(closed).raw.acknowledgement, { provider_owned: true });
|
|
573
|
+
|
|
574
|
+
closed.next_transition = (reviewingStartV4().next_transition as JsonObject);
|
|
575
|
+
assert.throws(() => decodeReviewStartV4(closed), /closed approved zero-lens START cannot carry next_transition/);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// review-acknowledged/v1 — gentle-ai #3947: the exact acknowledgement burn
|
|
580
|
+
// prints one typed envelope instead of nothing. Provenance:
|
|
581
|
+
// tests/fixtures/devbinary/review-acknowledged.provenance.md.
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
const ACKNOWLEDGED_FIXTURE = "review-acknowledged-v1.captured.json";
|
|
585
|
+
|
|
586
|
+
function acknowledgedFixture(): JsonObject {
|
|
587
|
+
return clone(fixture(DEV_FIXTURES, ACKNOWLEDGED_FIXTURE) as JsonObject);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
test("review-acknowledged/v1 decodes the captured burn envelope exactly", () => {
|
|
591
|
+
const raw = acknowledgedFixture();
|
|
592
|
+
const decoded = decodeReviewAcknowledgedV1(raw);
|
|
593
|
+
assert.deepEqual(decoded, {
|
|
594
|
+
schema: "gentle-ai.review-acknowledged/v1",
|
|
595
|
+
operation: "review/acknowledge-approved",
|
|
596
|
+
action: "acknowledged",
|
|
597
|
+
lineageId: "review-3ec95251db75f626",
|
|
598
|
+
targetIdentity: "sha256:b505dcd8d82395c053c9786935e11e0e235cbdecca4f1f46f98c768ea6248d3d",
|
|
599
|
+
consumedRevision: "sha256:9732b1c3526bfecd3851093239241145c970cc126acea59bfaf14133214b60ee",
|
|
600
|
+
authority: "burned",
|
|
601
|
+
raw,
|
|
602
|
+
});
|
|
603
|
+
assert.equal(decodeReviewAcknowledgedV1(raw, { lineageId: decoded.lineageId, targetIdentity: decoded.targetIdentity, revision: decoded.consumedRevision }).authority, "burned");
|
|
604
|
+
assert.equal(REVIEW_ACKNOWLEDGED_SCHEMA, "gentle-ai.review-acknowledged/v1");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("review-acknowledged/v1 rejects identity drift, foreign fields, and a binding that names another burn", () => {
|
|
608
|
+
const cases: Array<[string, (body: JsonObject) => void, RegExp]> = [
|
|
609
|
+
["schema", (body) => { body.schema = "gentle-ai.review-acknowledged/v2"; }, /schema/],
|
|
610
|
+
["operation", (body) => { body.operation = "review.acknowledge-approved"; }, /operation/],
|
|
611
|
+
["action", (body) => { body.action = "replayed"; }, /action/],
|
|
612
|
+
["authority", (body) => { body.authority = "retained"; }, /authority/],
|
|
613
|
+
["unknown key", (body) => { body.receipt = { status: "created" }; }, /receipt/],
|
|
614
|
+
["missing consumed revision", (body) => { delete body.consumed_revision; }, /consumed_revision/],
|
|
615
|
+
["malformed target", (body) => { body.target_identity = "b505dcd8"; }, /target_identity/],
|
|
616
|
+
["malformed lineage", (body) => { body.lineage_id = "Review_3ec95251"; }, /lineage_id/],
|
|
617
|
+
];
|
|
618
|
+
for (const [name, mutate, pattern] of cases) {
|
|
619
|
+
const body = acknowledgedFixture();
|
|
620
|
+
mutate(body);
|
|
621
|
+
assert.throws(() => decodeReviewAcknowledgedV1(body), pattern, name);
|
|
622
|
+
}
|
|
623
|
+
const raw = acknowledgedFixture();
|
|
624
|
+
assert.throws(() => decodeReviewAcknowledgedV1(raw, { lineageId: "review-other" }), /lineage/);
|
|
625
|
+
assert.throws(() => decodeReviewAcknowledgedV1(raw, { targetIdentity: sha("b") }), /target/);
|
|
626
|
+
assert.throws(() => decodeReviewAcknowledgedV1(raw, { revision: sha("c") }), /revision/);
|
|
627
|
+
assert.throws(() => decodeReviewAcknowledgedV1(null), /object/);
|
|
628
|
+
assert.throws(() => decodeReviewAcknowledgedV1("{}"), /object/);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("review-acknowledged/v1 is disjoint from every prior captured identity in both directions", () => {
|
|
632
|
+
const priorFixtures = readdirSync(DEV_FIXTURES).filter((name) => name.endsWith(".json") && name !== ACKNOWLEDGED_FIXTURE);
|
|
633
|
+
assert.ok(priorFixtures.length >= 15, "the prior captured corpus must be present");
|
|
634
|
+
for (const name of priorFixtures) {
|
|
635
|
+
assert.throws(() => decodeReviewAcknowledgedV1(fixture(DEV_FIXTURES, name)), /schema|object/, `${name} must not decode as an acknowledgement`);
|
|
636
|
+
}
|
|
637
|
+
const acknowledged = acknowledgedFixture();
|
|
638
|
+
const priorDecoders: Array<[string, (value: unknown) => unknown]> = [
|
|
639
|
+
["status/v3", decodeReviewStatusV3],
|
|
640
|
+
["start/v3", decodeReviewStartV3],
|
|
641
|
+
["start/v4", decodeReviewStartV4],
|
|
642
|
+
["capabilities/v2", decodeReviewCapabilitiesV2],
|
|
643
|
+
["consent/v2", decodeReviewConsentV2],
|
|
644
|
+
["consent/v3", decodeReviewConsentV3],
|
|
645
|
+
["last-event-closure/v1", decodeReviewLastEventClosureV1],
|
|
646
|
+
["result-artifact/v2", decodeReviewResultArtifactV2],
|
|
647
|
+
["failure/v2", decodeReviewFailureV2],
|
|
648
|
+
["next-transition/v3", decodeReviewNextTransitionV3],
|
|
649
|
+
["repair/v2", decodeReviewRepairV2],
|
|
650
|
+
];
|
|
651
|
+
for (const [name, decoder] of priorDecoders) {
|
|
652
|
+
assert.throws(() => decoder(clone(acknowledged)), `${name} must reject the acknowledged envelope`);
|
|
653
|
+
}
|
|
654
|
+
});
|