gentle-pi 2.2.0 → 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 +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- 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/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- 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/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- 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/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -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/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- 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 +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- 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/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- 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 -530
|
@@ -1,631 +1,654 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import {
|
|
6
|
+
REVIEW_ACKNOWLEDGED_SCHEMA,
|
|
7
|
+
decodeReviewAcknowledgedV1,
|
|
6
8
|
decodeReviewCapabilitiesV2,
|
|
7
9
|
decodeReviewConsentV2,
|
|
8
10
|
decodeReviewConsentV3,
|
|
9
11
|
decodeReviewFailureV2,
|
|
10
|
-
|
|
12
|
+
decodeReviewLastEventClosureV1,
|
|
13
|
+
decodeReviewNextTransitionV3,
|
|
14
|
+
decodeReviewRepairV2,
|
|
11
15
|
decodeReviewResultArtifactV2,
|
|
12
16
|
decodeReviewStartV3,
|
|
17
|
+
decodeReviewStartV4,
|
|
13
18
|
decodeReviewStatusV3,
|
|
14
19
|
} from "../lib/review-integration-v2.ts";
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// - capabilities-v2.2.captured.json and status-v5.captured.json were captured
|
|
20
|
-
// 2026-08-15 from the real binary at /home/gentleman/.cargo/bin/gentle-ai
|
|
21
|
-
// reporting "gentle-ai 2.4.0-rc.8+fix.verify-attestation-recovery" (built
|
|
22
|
-
// from the gentle-ai main line), in a scratch git repository:
|
|
23
|
-
// gentle-ai review capabilities --contract gentle-ai.review-integration/v2
|
|
24
|
-
// GENTLE_PI_REVIEW_RELAY_CONTRACT=gentle-pi.review-relay/v1 \
|
|
25
|
-
// gentle-ai review status --contract gentle-ai.review-integration/v2 \
|
|
26
|
-
// --cwd <scratch> --projection workspace --next-transition
|
|
27
|
-
// - capabilities-v2.1.derived.json is derived from the captured v2.2 payload
|
|
28
|
-
// plus gentle-ai origin/main contracts/review-integration/v2/schemas/
|
|
29
|
-
// capabilities-v2.1.schema.json (schema const v2.1, protocol minor 1,
|
|
30
|
-
// consent/v3 + status/v3 schema advertisements, 17 optional features): no
|
|
31
|
-
// installed binary emits v2.1 to capture from.
|
|
32
|
-
// - The v5-only next_transition samples (correction_request, provider_task,
|
|
33
|
-
// submission descriptor forms) are constructed from gentle-ai origin/main
|
|
34
|
-
// contracts/review-integration/v2/schemas/status-v5.schema.json and
|
|
35
|
-
// contracts/review-integration/v1/schemas/correction-plan-request.schema.json.
|
|
36
|
-
// - consent-v3.captured.json, start-v3-consent-granted.captured.json, and
|
|
37
|
-
// start-v3-consent-declined.captured.json were captured 2026-08-16 from the
|
|
38
|
-
// real binary at /home/gentleman/.cargo/bin/gentle-ai reporting
|
|
39
|
-
// "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository holding a
|
|
40
|
-
// committed internal/runner/{runner.go,runner_test.go} baseline plus 12
|
|
41
|
-
// uncommitted changed lines that add a process-starting helper (risk signal
|
|
42
|
-
// shell_process, high tier):
|
|
43
|
-
// gentle-ai review status --contract gentle-ai.review-integration/v2 \
|
|
44
|
-
// --cwd <scratch> --projection workspace --next-transition # target source
|
|
45
|
-
// gentle-ai review start --contract gentle-ai.review-integration/v2 \
|
|
46
|
-
// --cwd <scratch> --target <identity> --projection workspace \
|
|
47
|
-
// --consent relay # consent-v3
|
|
48
|
-
// then the envelope's exact declined invocation (declined capture, which
|
|
49
|
-
// persists nothing) followed by its exact granted invocation (granted
|
|
50
|
-
// start/v3 capture). Note: the live granted/declined invocations carry no
|
|
51
|
-
// --agent token even though the envelope pins agent: claude-code — the
|
|
52
|
-
// published consent-v3.schema.json invocation pattern is narrower than the
|
|
53
|
-
// emitter, so the capture is authoritative (parity playbook, known traps).
|
|
54
|
-
// - status-v5-repository-context.captured.json was captured 2026-08-16 from
|
|
55
|
-
// the real binary at /home/gentleman/.cargo/bin/gentle-ai reporting
|
|
56
|
-
// "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository with one
|
|
57
|
-
// medium-tier lineage in state `reviewing` whose single reviewer result was
|
|
58
|
-
// already admitted (consent granted start, then review capture-result):
|
|
59
|
-
// gentle-ai review status --contract gentle-ai.review-integration/v2 \
|
|
60
|
-
// --cwd <scratch> --agent claude-code --lineage <lineage> \
|
|
61
|
-
// --projection workspace --next-transition
|
|
62
|
-
// The top-level `repository_context` reference it carries is emitted by the
|
|
63
|
-
// live binary but MISSING from the published status-v5.schema.json
|
|
64
|
-
// (additionalProperties: false) at the same commit — the capture is
|
|
65
|
-
// authoritative (parity playbook, known traps).
|
|
66
|
-
// - result-artifact-v2.captured.json and result-artifact-v2-path.captured.json
|
|
67
|
-
// were captured 2026-08-16 from the real binary at
|
|
68
|
-
// /home/gentleman/.cargo/bin/gentle-ai reporting
|
|
69
|
-
// "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository holding a
|
|
70
|
-
// committed src/greet.js baseline plus an uncommitted shout() helper
|
|
71
|
-
// (medium tier, one review-reliability lens): consent-granted START, then
|
|
72
|
-
// the STATUS transition's exact `review capture-result` collect arguments
|
|
73
|
-
// with a completed reviewer input document. The reference form is the
|
|
74
|
-
// stdout of the --repository-context invocation (opaque rart1_ locator);
|
|
75
|
-
// the path form re-ran the exact same admitted slot with --cwd instead,
|
|
76
|
-
// which discovers the identical canonical bytes and answers with the
|
|
77
|
-
// provider-owned store path. Both agree on every binding field.
|
|
78
|
-
// - status-v5-capture-result-submission.captured.json was captured 2026-08-16
|
|
79
|
-
// from the real binary at /home/gentleman/.cargo/bin/gentle-ai reporting
|
|
80
|
-
// "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository holding a
|
|
81
|
-
// committed src/add.js baseline plus an uncommitted double() helper
|
|
82
|
-
// (medium tier, one review-reliability lens), with
|
|
83
|
-
// GENTLE_PI_REVIEW_RELAY_CONTRACT=gentle-pi.review-relay/v1 exported:
|
|
84
|
-
// negotiated STATUS (--agent pi), the rendered consent/v3 START, its exact
|
|
85
|
-
// granted invocation, then
|
|
86
|
-
// gentle-ai review status --contract gentle-ai.review-integration/v2 \
|
|
87
|
-
// --cwd <scratch> --agent pi --next-transition
|
|
88
|
-
// at reviewer_results_required. Its materialize capture-result collect
|
|
89
|
-
// input carries the submission descriptor as a SINGULAR `value` object
|
|
90
|
-
// with a `schema` key (emitter: gentle-ai f1a95179, singular from its
|
|
91
|
-
// first commit) — not the `values` array this decoder was originally
|
|
92
|
-
// written against. The capture is authoritative.
|
|
93
|
-
|
|
94
|
-
const fixtureRoot = join(import.meta.dirname, "fixtures", "devbinary");
|
|
95
|
-
const fixture = <T = Record<string, unknown>>(name: string): T => JSON.parse(readFileSync(join(fixtureRoot, name), "utf8")) as T;
|
|
96
|
-
const v2FixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
|
|
97
|
-
const v2Fixture = <T = Record<string, unknown>>(name: string): T => JSON.parse(readFileSync(join(v2FixtureRoot, name), "utf8")) as T;
|
|
98
|
-
const capturedDigest = "ffc91d8fa79c869aba9aa3d1ec80edebb5b1744e5a06fef75d4c8b73c0e46bc1";
|
|
99
|
-
const pinnedFixtureDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
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";
|
|
100
24
|
|
|
101
25
|
type JsonObject = Record<string, unknown>;
|
|
102
26
|
const clone = <T>(value: T): T => structuredClone(value);
|
|
103
27
|
const sha = (fill: string): string => `sha256:${fill.repeat(64)}`;
|
|
104
28
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
);
|
|
107
78
|
assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
|
|
108
79
|
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v5"), true);
|
|
109
|
-
|
|
110
|
-
|
|
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");
|
|
111
215
|
});
|
|
112
216
|
|
|
113
217
|
test("the derived capabilities/v2.1 payload decodes with protocol minor 1", () => {
|
|
114
|
-
const capabilities = decodeReviewCapabilitiesV2(fixture("capabilities-v2.1.derived.json"),
|
|
218
|
+
const capabilities = decodeReviewCapabilitiesV2(fixture(DEV_FIXTURES, "capabilities-v2.1.derived.json"), CAPTURED_DIGEST);
|
|
115
219
|
assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
|
|
116
220
|
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v3"), true);
|
|
117
221
|
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v3"), true);
|
|
118
222
|
});
|
|
119
223
|
|
|
120
|
-
test("the pinned capabilities/v2 fixture still decodes unchanged", () => {
|
|
121
|
-
const capabilities = decodeReviewCapabilitiesV2(v2Fixture("capabilities.fixture.json"), pinnedFixtureDigest);
|
|
122
|
-
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v3"), true);
|
|
123
|
-
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v2"), true);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("capabilities schema identity and protocol minor must agree", () => {
|
|
127
|
-
const base = fixture<JsonObject>("capabilities-v2.2.captured.json");
|
|
128
|
-
const wrongMinor = clone(base);
|
|
129
|
-
(wrongMinor.protocol as JsonObject).minor = 1;
|
|
130
|
-
assert.throws(() => decodeReviewCapabilitiesV2(wrongMinor, capturedDigest), /protocol/);
|
|
131
|
-
const wrongSchema = clone(base);
|
|
132
|
-
wrongSchema.schema = "gentle-ai.review-integration.capabilities/v2.3";
|
|
133
|
-
assert.throws(() => decodeReviewCapabilitiesV2(wrongSchema, capturedDigest), /schema/);
|
|
134
|
-
// The old identity never inherits the new advertisements: a v2 envelope
|
|
135
|
-
// advertising the v2.2 schema surface is missing its own required floor.
|
|
136
|
-
const crossed = clone(base);
|
|
137
|
-
crossed.schema = "gentle-ai.review-integration.capabilities/v2";
|
|
138
|
-
(crossed.protocol as JsonObject).minor = 0;
|
|
139
|
-
assert.throws(() => decodeReviewCapabilitiesV2(crossed, capturedDigest), /schemas/);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("the captured status/v5 payload decodes with its forecast and base-ref collect input", () => {
|
|
143
|
-
const status = decodeReviewStatusV3(fixture("status-v5.captured.json"));
|
|
144
|
-
assert.equal(status.applicability, "unrelated");
|
|
145
|
-
assert.equal(status.action, "start");
|
|
146
|
-
assert.equal(status.forecast?.horizon, "partial");
|
|
147
|
-
assert.deepEqual(status.forecast?.steps, [{ step: 1, kind: "collect", reasonCode: "empty_candidate_base_ref_required", description: "empty candidate base ref required" }]);
|
|
148
|
-
assert.equal(status.nextTransition?.kind, "collect");
|
|
149
|
-
assert.equal(status.nextTransition?.collect?.inputs[0]?.captureOperation, "external.select_base_ref");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("the pinned status/v3 fixture still decodes unchanged", () => {
|
|
153
|
-
const status = decodeReviewStatusV3(v2Fixture("status.fixture.json"));
|
|
154
|
-
assert.equal(status.contract, "gentle-ai.review-integration/v2");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("the captured status/v5 payload decodes its top-level repository context reference", () => {
|
|
158
|
-
const status = decodeReviewStatusV3(fixture("status-v5-repository-context.captured.json"));
|
|
159
|
-
assert.equal(status.authority?.state, "reviewing");
|
|
160
|
-
assert.equal(status.repositoryContext?.capability, "review.opaque_repository_context");
|
|
161
|
-
assert.match(status.repositoryContext?.handle ?? "", /^rctx1_[0-9a-f]{64}$/);
|
|
162
|
-
assert.equal(status.repositoryContext?.revision, status.authority?.revision);
|
|
163
|
-
assert.equal(status.repositoryContext?.targetIdentity, status.targetIdentity);
|
|
164
|
-
assert.equal(status.repositoryContext?.outcome, "applied");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
224
|
test("a status/v3 payload never carries a top-level repository context", () => {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
delete
|
|
172
|
-
assert.throws(() => decodeReviewStatusV3(
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("a status/v3 payload never carries v5 fields", () => {
|
|
176
|
-
const v5 = fixture<JsonObject>("status-v5.captured.json");
|
|
177
|
-
const downgraded = clone(v5);
|
|
178
|
-
downgraded.schema = "gentle-ai.review-integration.status/v3";
|
|
179
|
-
// forecast is a v5 field; the v3 identity must keep rejecting it.
|
|
180
|
-
assert.throws(() => decodeReviewStatusV3(downgraded), /forecast/);
|
|
181
|
-
const withoutForecast = clone(downgraded);
|
|
182
|
-
delete withoutForecast.forecast;
|
|
183
|
-
const status = decodeReviewStatusV3(withoutForecast);
|
|
184
|
-
assert.equal(status.forecast, undefined);
|
|
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/);
|
|
185
230
|
});
|
|
186
231
|
|
|
187
232
|
test("a v5 forecast requires the next transition and a coherent horizon", () => {
|
|
188
|
-
const
|
|
189
|
-
const orphanForecast = clone(base);
|
|
233
|
+
const orphanForecast = currentStatusFixture("status-v5.captured.json");
|
|
190
234
|
delete orphanForecast.next_transition;
|
|
191
235
|
assert.throws(() => decodeReviewStatusV3(orphanForecast), /forecast/);
|
|
192
|
-
const wrongHorizon =
|
|
236
|
+
const wrongHorizon = currentStatusFixture("status-v5.captured.json");
|
|
193
237
|
(wrongHorizon.forecast as JsonObject).horizon = "terminal";
|
|
194
238
|
assert.throws(() => decodeReviewStatusV3(wrongHorizon), /horizon/);
|
|
195
|
-
const badStep =
|
|
239
|
+
const badStep = currentStatusFixture("status-v5.captured.json");
|
|
196
240
|
((badStep.forecast as JsonObject).steps as JsonObject[])[0]!.step = 2;
|
|
197
241
|
assert.throws(() => decodeReviewStatusV3(badStep), /step/);
|
|
198
|
-
const extraKey = clone(base);
|
|
199
|
-
(extraKey.forecast as JsonObject).unadvertised = true;
|
|
200
|
-
assert.throws(() => decodeReviewStatusV3(extraKey), /not allowed/);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// --- v5-only next_transition surfaces, constructed per the vendored schemas ---
|
|
204
|
-
|
|
205
|
-
function v5StatusWith(nextTransition: JsonObject, forecast?: JsonObject): JsonObject {
|
|
206
|
-
const base = fixture<JsonObject>("status-v5.captured.json");
|
|
207
|
-
base.next_transition = nextTransition;
|
|
208
|
-
if (forecast === undefined) delete base.forecast;
|
|
209
|
-
else base.forecast = forecast;
|
|
210
|
-
return base;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const correctionPlanRequest: JsonObject = {
|
|
214
|
-
schema: "gentle-ai.review-correction-plan-request/v1",
|
|
215
|
-
request_hash: sha("a"),
|
|
216
|
-
lineage_id: "review-1d5aadacc600e167",
|
|
217
|
-
expected_revision: sha("b"),
|
|
218
|
-
target_identity: sha("c"),
|
|
219
|
-
correction_budget: 25,
|
|
220
|
-
fix_finding_ids: ["risk-1"],
|
|
221
|
-
findings: [{
|
|
222
|
-
id: "risk-1",
|
|
223
|
-
lens: "risk",
|
|
224
|
-
location: "app.ts:10",
|
|
225
|
-
severity: "BLOCKER",
|
|
226
|
-
claim: "candidate-introduced credential exposure",
|
|
227
|
-
proof_refs: ["app.ts:10-14"],
|
|
228
|
-
evidence: "the candidate logs the raw token",
|
|
229
|
-
evidence_class: "deterministic",
|
|
230
|
-
causal_disposition: "introduced",
|
|
231
|
-
}],
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
test("a v5 correction_plan_required transition carries its correction request and plan submission", () => {
|
|
235
|
-
const transition: JsonObject = {
|
|
236
|
-
kind: "collect",
|
|
237
|
-
reason_code: "correction_plan_required",
|
|
238
|
-
correction_request: correctionPlanRequest,
|
|
239
|
-
collect: {
|
|
240
|
-
inputs: [{
|
|
241
|
-
name: "correction_plan",
|
|
242
|
-
schema: "gentle-ai.review-correction-plan/v1",
|
|
243
|
-
capture_operation: "external.plan_correction",
|
|
244
|
-
arguments: [{ name: "lineage", value: "review-1d5aadacc600e167" }],
|
|
245
|
-
submission: {
|
|
246
|
-
operation_token: "finalize",
|
|
247
|
-
argument_tokens: [
|
|
248
|
-
"--contract=gentle-ai.review-integration/v2",
|
|
249
|
-
"--lineage=review-1d5aadacc600e167",
|
|
250
|
-
`--expected-revision=${sha("b")}`,
|
|
251
|
-
`--target=${sha("c")}`,
|
|
252
|
-
`--request-hash=${sha("a")}`,
|
|
253
|
-
`--repository-context=rctx1_${"0".repeat(64)}`,
|
|
254
|
-
"--correction-lines={{value}}",
|
|
255
|
-
],
|
|
256
|
-
value: { slot: "correction_lines", domain: "positive_correction_lines", minimum: 1, maximum: 25, substitution_location: 6 },
|
|
257
|
-
},
|
|
258
|
-
}],
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
const status = decodeReviewStatusV3(v5StatusWith(transition));
|
|
262
|
-
assert.equal(status.nextTransition?.reasonCode, "correction_plan_required");
|
|
263
|
-
assert.equal(status.nextTransition?.correctionRequest?.correctionBudget, 25);
|
|
264
|
-
assert.equal(status.nextTransition?.collect?.inputs[0]?.submissionDescriptor?.operationToken, "finalize");
|
|
265
|
-
// The same reason code without its correction request is incomplete.
|
|
266
|
-
const missing = clone(transition);
|
|
267
|
-
delete missing.correction_request;
|
|
268
|
-
assert.throws(() => decodeReviewStatusV3(v5StatusWith(missing)), /correction_request/);
|
|
269
|
-
// And correction_request never rides an unrelated reason code.
|
|
270
|
-
const unrelated = clone(transition);
|
|
271
|
-
unrelated.reason_code = "verification_evidence_required";
|
|
272
|
-
assert.throws(() => decodeReviewStatusV3(v5StatusWith(unrelated)), /correction_request/);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test("a v5 capture-evidence transition decodes its evidence submission descriptor", () => {
|
|
276
|
-
const transition: JsonObject = {
|
|
277
|
-
kind: "collect",
|
|
278
|
-
reason_code: "verification_evidence_required",
|
|
279
|
-
collect: {
|
|
280
|
-
inputs: [{
|
|
281
|
-
name: "verification_evidence",
|
|
282
|
-
schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1",
|
|
283
|
-
capture_operation: "review.capture-evidence",
|
|
284
|
-
arguments: [{ name: "lineage", value: "review-1d5aadacc600e167" }],
|
|
285
|
-
submission: {
|
|
286
|
-
operation_token: "capture-evidence",
|
|
287
|
-
argument_tokens: [
|
|
288
|
-
"--lineage=review-1d5aadacc600e167",
|
|
289
|
-
`--expected-revision=${sha("b")}`,
|
|
290
|
-
`--target=${sha("c")}`,
|
|
291
|
-
`--repository-context=rctx1_${"0".repeat(64)}`,
|
|
292
|
-
"--outcome={{outcome}}",
|
|
293
|
-
"--input={{input}}",
|
|
294
|
-
],
|
|
295
|
-
values: [
|
|
296
|
-
{ slot: "outcome", domain: "verification_outcome", allowed_values: ["passed", "verification_failed", "procedural_tooling_failed"], substitution_location: 4 },
|
|
297
|
-
{ slot: "input", domain: "artifact_path_or_stdin", schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1", substitution_location: 5 },
|
|
298
|
-
],
|
|
299
|
-
},
|
|
300
|
-
}],
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
const status = decodeReviewStatusV3(v5StatusWith(transition));
|
|
304
|
-
const input = status.nextTransition?.collect?.inputs[0];
|
|
305
|
-
assert.equal(input?.submissionDescriptor?.operationToken, "capture-evidence");
|
|
306
|
-
assert.equal(input?.submission, undefined);
|
|
307
|
-
// Missing its required submission descriptor is incomplete.
|
|
308
|
-
const missing = clone(transition);
|
|
309
|
-
delete ((missing.collect as JsonObject).inputs as JsonObject[])[0]!.submission;
|
|
310
|
-
assert.throws(() => decodeReviewStatusV3(v5StatusWith(missing)), /submission/);
|
|
311
242
|
});
|
|
312
243
|
|
|
313
244
|
test("a v5 provider role task input decodes and is confined to external.run_provider_role", () => {
|
|
314
|
-
const
|
|
315
|
-
const transition
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
const status = decodeReviewStatusV3(v5StatusWith(transition));
|
|
329
|
-
assert.deepEqual(status.nextTransition?.collect?.inputs[0]?.providerTask, { agent: "review-refuter", role: "refuter", prompt: "GENTLE_AI_REVIEW_BINDING {}" });
|
|
330
|
-
// The task is required on its own vector and forbidden anywhere else.
|
|
331
|
-
const missing = clone(transition);
|
|
332
|
-
delete ((missing.collect as JsonObject).inputs as JsonObject[])[0]!.provider_task;
|
|
333
|
-
assert.throws(() => decodeReviewStatusV3(v5StatusWith(missing)), /provider_task/);
|
|
334
|
-
const misplaced = fixture<JsonObject>("status-v5.captured.json");
|
|
335
|
-
(((misplaced.next_transition as JsonObject).collect as JsonObject).inputs as JsonObject[])[0]!.provider_task = providerTask;
|
|
336
|
-
assert.throws(() => decodeReviewStatusV3(misplaced), /provider_task/);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
// --- v5 pi materialize capture-result submission: the singular `value` form ---
|
|
340
|
-
|
|
341
|
-
test("the captured v5 materialize capture-result submission decodes its singular value form", () => {
|
|
342
|
-
const status = decodeReviewStatusV3(fixture("status-v5-capture-result-submission.captured.json"));
|
|
343
|
-
assert.equal(status.authority?.state, "reviewing");
|
|
344
|
-
assert.equal(status.nextTransition?.reasonCode, "reviewer_results_required");
|
|
345
|
-
const input = status.nextTransition?.collect?.inputs[0];
|
|
346
|
-
assert.equal(input?.captureOperation, "review.capture-result");
|
|
347
|
-
assert.equal(input?.submissionDescriptor, undefined);
|
|
348
|
-
assert.equal(input?.submission?.operationToken, "capture-result");
|
|
349
|
-
// The singular wire `value` normalizes into the typed one-entry values
|
|
350
|
-
// array the host relay already consumes.
|
|
351
|
-
assert.deepEqual(input?.submission?.values, [{
|
|
352
|
-
slot: "reviewer_result",
|
|
353
|
-
domain: "artifact_path_or_stdin",
|
|
354
|
-
schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
355
|
-
substitutionLocation: 7,
|
|
356
|
-
}]);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test("the v5 singular capture-result value form is closed to its captured shape", () => {
|
|
360
|
-
const base = fixture<JsonObject>("status-v5-capture-result-submission.captured.json");
|
|
361
|
-
const withSubmission = (mutate: (submission: JsonObject) => void): JsonObject => {
|
|
362
|
-
const body = clone(base);
|
|
363
|
-
mutate((((body.next_transition as JsonObject).collect as JsonObject).inputs as JsonObject[])[0]!.submission as JsonObject);
|
|
364
|
-
return body;
|
|
365
|
-
};
|
|
366
|
-
// Carrying both wire forms at once matches no captured shape.
|
|
367
|
-
assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
|
|
368
|
-
submission.values = [submission.value];
|
|
369
|
-
})), /value/);
|
|
370
|
-
// The singular form owns no numeric or enumerated domain: minimum,
|
|
371
|
-
// maximum, and allowed_values belong to the finalize/capture-evidence
|
|
372
|
-
// descriptor forms only.
|
|
373
|
-
for (const [key, smuggled] of [["minimum", 1], ["maximum", 2], ["allowed_values", ["x"]]] as const) {
|
|
374
|
-
assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
|
|
375
|
-
(submission.value as JsonObject)[key] = smuggled;
|
|
376
|
-
})), /not allowed/);
|
|
377
|
-
}
|
|
378
|
-
// The captured form always names its reviewer schema and slot.
|
|
379
|
-
assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
|
|
380
|
-
delete (submission.value as JsonObject).schema;
|
|
381
|
-
})), /schema/);
|
|
382
|
-
assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
|
|
383
|
-
(submission.value as JsonObject).slot = "validation";
|
|
384
|
-
})), /slot/);
|
|
385
|
-
assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
|
|
386
|
-
submission.operation_token = "finalize-relay";
|
|
387
|
-
})), /operation_token/);
|
|
388
|
-
// The legacy values-array rows stay exactly as they were: they never
|
|
389
|
-
// learned the schema key the singular form carries.
|
|
390
|
-
assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
|
|
391
|
-
const row = submission.value as JsonObject;
|
|
392
|
-
delete submission.value;
|
|
393
|
-
submission.values = [row];
|
|
394
|
-
})), /not allowed/);
|
|
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/);
|
|
395
257
|
});
|
|
396
258
|
|
|
397
259
|
test("the v3 identity keeps rejecting the singular capture-result value form", () => {
|
|
398
|
-
const body =
|
|
260
|
+
const body = currentStatusFixture("status-v5-capture-result-submission.captured.json");
|
|
399
261
|
body.schema = "gentle-ai.review-integration.status/v3";
|
|
262
|
+
delete body.receipt;
|
|
400
263
|
delete body.forecast;
|
|
401
264
|
delete body.repository_context;
|
|
402
265
|
assert.throws(() => decodeReviewStatusV3(body), /values is required/);
|
|
403
266
|
});
|
|
404
267
|
|
|
405
268
|
test("the v3 next transition keeps rejecting every v5-only surface", () => {
|
|
406
|
-
const v5 =
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
((
|
|
419
|
-
})), /not allowed|provider_task/);
|
|
420
|
-
assert.throws(() => decodeReviewStatusV3(asV3((transition) => {
|
|
421
|
-
(((transition.collect as JsonObject).inputs as JsonObject[])[0]!).submission = { operation_token: "finalize", argument_tokens: ["--correction-lines={{value}}"], value: { slot: "correction_lines", domain: "positive_correction_lines", minimum: 1, maximum: 25, substitution_location: 6 } };
|
|
422
|
-
})), /submission|values/);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// --- consent/v3 — the negotiated v2.1+ consent question (adds `agent`) ---
|
|
426
|
-
|
|
427
|
-
test("the captured consent/v3 envelope decodes with its fixed agent binding", () => {
|
|
428
|
-
const consent = decodeReviewConsentV3(fixture("consent-v3.captured.json"));
|
|
429
|
-
assert.equal(consent.schema, "gentle-ai.review-integration.consent/v3");
|
|
430
|
-
assert.equal(consent.agent, "claude-code");
|
|
431
|
-
assert.equal(consent.action, "consent_required");
|
|
432
|
-
assert.equal(consent.blocking, true);
|
|
433
|
-
assert.equal(consent.riskLevel, "high");
|
|
434
|
-
assert.equal(consent.changedFiles, 2);
|
|
435
|
-
assert.equal(consent.changedLines, 12);
|
|
436
|
-
assert.equal(consent.choices[0].answer, "granted");
|
|
437
|
-
assert.equal(consent.choices[1].answer, "declined");
|
|
438
|
-
for (const choice of consent.choices) {
|
|
439
|
-
assert.ok(choice.invocation.includes(` --target ${consent.targetIdentity} `));
|
|
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/);
|
|
440
282
|
}
|
|
441
|
-
assert.equal(consent.offPath.command, "gentle-ai review mode disable");
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("consent identities never cross-decode", () => {
|
|
445
|
-
const v3 = fixture<JsonObject>("consent-v3.captured.json");
|
|
446
|
-
const v2 = v2Fixture<JsonObject>("consent.fixture.json");
|
|
447
|
-
// The old identity never accepts the new surface, and vice versa.
|
|
448
|
-
assert.throws(() => decodeReviewConsentV2(clone(v3)), /agent|schema/);
|
|
449
|
-
assert.throws(() => decodeReviewConsentV3(clone(v2)), /agent|schema/);
|
|
450
|
-
// A schema-swapped v3 body keeps its agent key, which the v2 identity
|
|
451
|
-
// still rejects as an unadvertised surface.
|
|
452
|
-
const downgraded = clone(v3);
|
|
453
|
-
downgraded.schema = "gentle-ai.review-integration.consent/v2";
|
|
454
|
-
assert.throws(() => decodeReviewConsentV2(downgraded), /agent/);
|
|
455
|
-
// A schema-swapped v2 body is missing the agent key v3 requires.
|
|
456
|
-
const upgraded = clone(v2);
|
|
457
|
-
upgraded.schema = "gentle-ai.review-integration.consent/v3";
|
|
458
|
-
assert.throws(() => decodeReviewConsentV3(upgraded), /agent/);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("consent/v3 keeps every v2 semantic guard and pins its agent constant", () => {
|
|
462
|
-
const base = fixture<JsonObject>("consent-v3.captured.json");
|
|
463
|
-
const wrongAgent = clone(base);
|
|
464
|
-
wrongAgent.agent = "opencode";
|
|
465
|
-
assert.throws(() => decodeReviewConsentV3(wrongAgent), /agent/);
|
|
466
283
|
const swapped = clone(base);
|
|
467
284
|
swapped.choices = [...(swapped.choices as JsonObject[])].reverse();
|
|
468
285
|
assert.throws(() => decodeReviewConsentV3(swapped), /answer/);
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
assert.throws(() => decodeReviewConsentV3(
|
|
472
|
-
const differentTarget = clone(base);
|
|
473
|
-
differentTarget.target_identity = sha("d");
|
|
474
|
-
assert.throws(() => decodeReviewConsentV3(differentTarget), /target|invocation/);
|
|
475
|
-
const notBlocking = clone(base);
|
|
476
|
-
notBlocking.blocking = false;
|
|
477
|
-
assert.throws(() => decodeReviewConsentV3(notBlocking), /blocking/);
|
|
478
|
-
const extraKey = clone(base);
|
|
479
|
-
extraKey.unadvertised = true;
|
|
480
|
-
assert.throws(() => decodeReviewConsentV3(extraKey), /not allowed/);
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
test("the pinned consent/v2 fixture still decodes unchanged", () => {
|
|
484
|
-
const consent = decodeReviewConsentV2(v2Fixture("consent.fixture.json"));
|
|
485
|
-
assert.equal(consent.schema, "gentle-ai.review-integration.consent/v2");
|
|
486
|
-
assert.equal(consent.action, "consent_required");
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// --- start/v3 — main extends repository_context with event_id/outcome ---
|
|
490
|
-
|
|
491
|
-
test("the captured granted start/v3 decodes with its repository context event binding", () => {
|
|
492
|
-
const start = decodeReviewStartV3(fixture("start-v3-consent-granted.captured.json"));
|
|
493
|
-
assert.equal(start.lineageId, "review-377c60e10b852cfc");
|
|
494
|
-
assert.equal(start.state, "reviewing");
|
|
495
|
-
assert.equal(start.riskLevel, "high");
|
|
496
|
-
assert.equal(start.selectedLenses.length, 4);
|
|
497
|
-
assert.equal(start.correctionBudget, 6);
|
|
498
|
-
assert.equal(start.repositoryContext?.outcome, "applied");
|
|
499
|
-
assert.match(start.repositoryContext?.eventId ?? "", /^sha256:[0-9a-f]{64}$/);
|
|
286
|
+
const wrongTarget = clone(base);
|
|
287
|
+
wrongTarget.target_identity = sha("d");
|
|
288
|
+
assert.throws(() => decodeReviewConsentV3(wrongTarget), /target|invocation/);
|
|
500
289
|
});
|
|
501
290
|
|
|
502
291
|
test("start/v3 repository context event fields stay optional and exact", () => {
|
|
503
|
-
|
|
504
|
-
const pinned = decodeReviewStartV3(v2Fixture("start.fixture.json"));
|
|
292
|
+
const pinned = decodeReviewStartV3(fixture(V2_FIXTURES, "start.fixture.json"));
|
|
505
293
|
assert.equal(pinned.repositoryContext?.eventId, undefined);
|
|
506
294
|
assert.equal(pinned.repositoryContext?.outcome, undefined);
|
|
507
|
-
const
|
|
508
|
-
const badEvent = clone(base);
|
|
295
|
+
const badEvent = clone(fixture(DEV_FIXTURES, "start-v3-consent-granted.captured.json") as JsonObject);
|
|
509
296
|
(badEvent.repository_context as JsonObject).event_id = "not-a-digest";
|
|
510
297
|
assert.throws(() => decodeReviewStartV3(badEvent), /event_id/);
|
|
511
|
-
const badOutcome = clone(
|
|
298
|
+
const badOutcome = clone(fixture(DEV_FIXTURES, "start-v3-consent-granted.captured.json") as JsonObject);
|
|
512
299
|
(badOutcome.repository_context as JsonObject).outcome = "unheard-of";
|
|
513
300
|
assert.throws(() => decodeReviewStartV3(badOutcome), /outcome/);
|
|
514
|
-
const extraKey = clone(base);
|
|
515
|
-
(extraKey.repository_context as JsonObject).unadvertised = true;
|
|
516
|
-
assert.throws(() => decodeReviewStartV3(extraKey), /not allowed/);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// --- result-artifact/v2: the `review capture-result` admission envelope ---
|
|
520
|
-
|
|
521
|
-
test("the captured reference-form result artifact decodes with its rart1 locator", () => {
|
|
522
|
-
const artifact = decodeReviewResultArtifactV2(fixture("result-artifact-v2.captured.json"));
|
|
523
|
-
assert.equal(artifact.schema, "gentle-ai.review-result-artifact/v2");
|
|
524
|
-
assert.equal(artifact.capability, "review.native_result_artifact");
|
|
525
|
-
assert.match(artifact.reference ?? "", /^rart1_[0-9a-f]{64}$/);
|
|
526
|
-
assert.equal(artifact.path, undefined);
|
|
527
|
-
assert.equal(artifact.lineageId, "review-ceeb2b862bd39709");
|
|
528
|
-
assert.equal(artifact.lens, "review-reliability");
|
|
529
|
-
assert.equal(artifact.selectedOrder, 0);
|
|
530
|
-
assert.equal(artifact.admissionDecision, "completed");
|
|
531
|
-
assert.match(artifact.sha256, /^sha256:[0-9a-f]{64}$/);
|
|
532
|
-
assert.match(artifact.subjectHash, /^sha256:[0-9a-f]{64}$/);
|
|
533
|
-
assert.match(artifact.targetIdentity, /^sha256:[0-9a-f]{64}$/);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
test("the captured path-form result artifact decodes with its provider-owned path", () => {
|
|
537
|
-
const artifact = decodeReviewResultArtifactV2(fixture("result-artifact-v2-path.captured.json"));
|
|
538
|
-
assert.equal(artifact.reference, undefined);
|
|
539
|
-
assert.match(artifact.path ?? "", /reviewer-results\/00-review-reliability\.json$/);
|
|
540
|
-
// Same admitted slot through both locator forms: every binding agrees.
|
|
541
|
-
const reference = decodeReviewResultArtifactV2(fixture("result-artifact-v2.captured.json"));
|
|
542
|
-
assert.equal(artifact.sha256, reference.sha256);
|
|
543
|
-
assert.equal(artifact.subjectHash, reference.subjectHash);
|
|
544
|
-
assert.equal(artifact.lineageId, reference.lineageId);
|
|
545
|
-
assert.equal(artifact.targetIdentity, reference.targetIdentity);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
test("a result artifact carries exactly one locator", () => {
|
|
549
|
-
const base = fixture<JsonObject>("result-artifact-v2.captured.json");
|
|
550
|
-
const both = clone(base);
|
|
551
|
-
both.path = "/store/reviewer-results/00-review-reliability.json";
|
|
552
|
-
assert.throws(() => decodeReviewResultArtifactV2(both), /exactly one/);
|
|
553
|
-
const neither = clone(base);
|
|
554
|
-
delete neither.reference;
|
|
555
|
-
assert.throws(() => decodeReviewResultArtifactV2(neither), /exactly one/);
|
|
556
301
|
});
|
|
557
302
|
|
|
558
303
|
test("a result artifact rejects unknown keys and weakened bindings", () => {
|
|
559
|
-
const base = fixture
|
|
304
|
+
const base = fixture(DEV_FIXTURES, "result-artifact-v2.captured.json") as JsonObject;
|
|
560
305
|
const extra = clone(base);
|
|
561
306
|
extra.unadvertised = true;
|
|
562
307
|
assert.throws(() => decodeReviewResultArtifactV2(extra), /not allowed/);
|
|
563
308
|
const wrongCapability = clone(base);
|
|
564
309
|
wrongCapability.capability = "review.result_artifact";
|
|
565
310
|
assert.throws(() => decodeReviewResultArtifactV2(wrongCapability), /capability/);
|
|
566
|
-
const unadmitted = clone(base);
|
|
567
|
-
unadmitted.admission_decision = "quarantined";
|
|
568
|
-
assert.throws(() => decodeReviewResultArtifactV2(unadmitted), /admission_decision/);
|
|
569
311
|
const foreignLocator = clone(base);
|
|
570
312
|
foreignLocator.reference = `rref1_${"b".repeat(64)}`;
|
|
571
313
|
assert.throws(() => decodeReviewResultArtifactV2(foreignLocator), /reference/);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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`);
|
|
626
653
|
}
|
|
627
|
-
// Cross-pinned rejection preserved: an operation outside the published
|
|
628
|
-
// 12-value enum never decodes.
|
|
629
|
-
assert.throws(() => decodeReviewFailureV2({ ...captured, operation: "review.capture-bogus" }), /failure\.operation/);
|
|
630
|
-
assert.throws(() => decodeReviewFailureV2({ ...captured, operation: "review.recover" }), /failure\.operation/);
|
|
631
654
|
});
|