gentle-pi 2.1.2 → 2.2.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 +70 -12
- 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 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- 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 +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- 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/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/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 +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -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/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -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/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/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 +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- 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 +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- 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/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/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import {
|
|
6
|
+
decodeReviewCapabilitiesV2,
|
|
7
|
+
decodeReviewConsentV2,
|
|
8
|
+
decodeReviewConsentV3,
|
|
9
|
+
decodeReviewFailureV2,
|
|
10
|
+
decodeReviewOperationV2,
|
|
11
|
+
decodeReviewResultArtifactV2,
|
|
12
|
+
decodeReviewStartV3,
|
|
13
|
+
decodeReviewStatusV3,
|
|
14
|
+
} from "../lib/review-integration-v2.ts";
|
|
15
|
+
|
|
16
|
+
// Forward decoders for the gentle-ai main line (dev-binary field-test lane).
|
|
17
|
+
//
|
|
18
|
+
// Fixture provenance:
|
|
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";
|
|
100
|
+
|
|
101
|
+
type JsonObject = Record<string, unknown>;
|
|
102
|
+
const clone = <T>(value: T): T => structuredClone(value);
|
|
103
|
+
const sha = (fill: string): string => `sha256:${fill.repeat(64)}`;
|
|
104
|
+
|
|
105
|
+
test("the captured capabilities/v2.2 payload decodes and reports its own package version", () => {
|
|
106
|
+
const capabilities = decodeReviewCapabilitiesV2(fixture("capabilities-v2.2.captured.json"), capturedDigest);
|
|
107
|
+
assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
|
|
108
|
+
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v5"), true);
|
|
109
|
+
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v3"), true);
|
|
110
|
+
assert.equal(capabilities.optionalFeatures.has("provider_bound_native_git_context"), true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("the derived capabilities/v2.1 payload decodes with protocol minor 1", () => {
|
|
114
|
+
const capabilities = decodeReviewCapabilitiesV2(fixture("capabilities-v2.1.derived.json"), capturedDigest);
|
|
115
|
+
assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
|
|
116
|
+
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v3"), true);
|
|
117
|
+
assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v3"), true);
|
|
118
|
+
});
|
|
119
|
+
|
|
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
|
+
test("a status/v3 payload never carries a top-level repository context", () => {
|
|
168
|
+
const v5 = fixture<JsonObject>("status-v5-repository-context.captured.json");
|
|
169
|
+
const downgraded = clone(v5);
|
|
170
|
+
downgraded.schema = "gentle-ai.review-integration.status/v3";
|
|
171
|
+
delete downgraded.forecast; // forecast is rejected first; isolate the field under test
|
|
172
|
+
assert.throws(() => decodeReviewStatusV3(downgraded), /repository_context/);
|
|
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);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a v5 forecast requires the next transition and a coherent horizon", () => {
|
|
188
|
+
const base = fixture<JsonObject>("status-v5.captured.json");
|
|
189
|
+
const orphanForecast = clone(base);
|
|
190
|
+
delete orphanForecast.next_transition;
|
|
191
|
+
assert.throws(() => decodeReviewStatusV3(orphanForecast), /forecast/);
|
|
192
|
+
const wrongHorizon = clone(base);
|
|
193
|
+
(wrongHorizon.forecast as JsonObject).horizon = "terminal";
|
|
194
|
+
assert.throws(() => decodeReviewStatusV3(wrongHorizon), /horizon/);
|
|
195
|
+
const badStep = clone(base);
|
|
196
|
+
((badStep.forecast as JsonObject).steps as JsonObject[])[0]!.step = 2;
|
|
197
|
+
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
|
+
});
|
|
312
|
+
|
|
313
|
+
test("a v5 provider role task input decodes and is confined to external.run_provider_role", () => {
|
|
314
|
+
const providerTask = { agent: "review-refuter", role: "refuter", prompt: "GENTLE_AI_REVIEW_BINDING {}" };
|
|
315
|
+
const transition: JsonObject = {
|
|
316
|
+
kind: "collect",
|
|
317
|
+
reason_code: "refuter_batch_required",
|
|
318
|
+
collect: {
|
|
319
|
+
inputs: [{
|
|
320
|
+
name: "refuter_batch",
|
|
321
|
+
schema: "https://gentle-ai.dev/schema/review/refuter/v1",
|
|
322
|
+
capture_operation: "external.run_provider_role",
|
|
323
|
+
arguments: [{ name: "lineage", value: "review-1d5aadacc600e167" }],
|
|
324
|
+
provider_task: providerTask,
|
|
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/);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("the v3 identity keeps rejecting the singular capture-result value form", () => {
|
|
398
|
+
const body = clone(fixture<JsonObject>("status-v5-capture-result-submission.captured.json"));
|
|
399
|
+
body.schema = "gentle-ai.review-integration.status/v3";
|
|
400
|
+
delete body.forecast;
|
|
401
|
+
delete body.repository_context;
|
|
402
|
+
assert.throws(() => decodeReviewStatusV3(body), /values is required/);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("the v3 next transition keeps rejecting every v5-only surface", () => {
|
|
406
|
+
const v5 = fixture<JsonObject>("status-v5.captured.json");
|
|
407
|
+
// Re-identify the captured envelope as v3 (drop the v5-only forecast) and
|
|
408
|
+
// then try to smuggle each v5 surface through the old identity.
|
|
409
|
+
const asV3 = (mutate: (transition: JsonObject) => void): JsonObject => {
|
|
410
|
+
const body = clone(v5);
|
|
411
|
+
body.schema = "gentle-ai.review-integration.status/v3";
|
|
412
|
+
delete body.forecast;
|
|
413
|
+
mutate(body.next_transition as JsonObject);
|
|
414
|
+
return body;
|
|
415
|
+
};
|
|
416
|
+
assert.throws(() => decodeReviewStatusV3(asV3((transition) => { transition.correction_request = correctionPlanRequest; })), /not allowed|correction_request/);
|
|
417
|
+
assert.throws(() => decodeReviewStatusV3(asV3((transition) => {
|
|
418
|
+
(((transition.collect as JsonObject).inputs as JsonObject[])[0]!).provider_task = { agent: "review-refuter", role: "refuter", prompt: "x" };
|
|
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} `));
|
|
440
|
+
}
|
|
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
|
+
const swapped = clone(base);
|
|
467
|
+
swapped.choices = [...(swapped.choices as JsonObject[])].reverse();
|
|
468
|
+
assert.throws(() => decodeReviewConsentV3(swapped), /answer/);
|
|
469
|
+
const badInvocation = clone(base);
|
|
470
|
+
((badInvocation.choices as JsonObject[])[0]!).invocation = "gentle-ai review finalize --consent granted";
|
|
471
|
+
assert.throws(() => decodeReviewConsentV3(badInvocation), /invocation/);
|
|
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}$/);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("start/v3 repository context event fields stay optional and exact", () => {
|
|
503
|
+
// The pinned fixture carries neither field and still decodes unchanged.
|
|
504
|
+
const pinned = decodeReviewStartV3(v2Fixture("start.fixture.json"));
|
|
505
|
+
assert.equal(pinned.repositoryContext?.eventId, undefined);
|
|
506
|
+
assert.equal(pinned.repositoryContext?.outcome, undefined);
|
|
507
|
+
const base = fixture<JsonObject>("start-v3-consent-granted.captured.json");
|
|
508
|
+
const badEvent = clone(base);
|
|
509
|
+
(badEvent.repository_context as JsonObject).event_id = "not-a-digest";
|
|
510
|
+
assert.throws(() => decodeReviewStartV3(badEvent), /event_id/);
|
|
511
|
+
const badOutcome = clone(base);
|
|
512
|
+
(badOutcome.repository_context as JsonObject).outcome = "unheard-of";
|
|
513
|
+
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
|
+
});
|
|
557
|
+
|
|
558
|
+
test("a result artifact rejects unknown keys and weakened bindings", () => {
|
|
559
|
+
const base = fixture<JsonObject>("result-artifact-v2.captured.json");
|
|
560
|
+
const extra = clone(base);
|
|
561
|
+
extra.unadvertised = true;
|
|
562
|
+
assert.throws(() => decodeReviewResultArtifactV2(extra), /not allowed/);
|
|
563
|
+
const wrongCapability = clone(base);
|
|
564
|
+
wrongCapability.capability = "review.result_artifact";
|
|
565
|
+
assert.throws(() => decodeReviewResultArtifactV2(wrongCapability), /capability/);
|
|
566
|
+
const unadmitted = clone(base);
|
|
567
|
+
unadmitted.admission_decision = "quarantined";
|
|
568
|
+
assert.throws(() => decodeReviewResultArtifactV2(unadmitted), /admission_decision/);
|
|
569
|
+
const foreignLocator = clone(base);
|
|
570
|
+
foreignLocator.reference = `rref1_${"b".repeat(64)}`;
|
|
571
|
+
assert.throws(() => decodeReviewResultArtifactV2(foreignLocator), /reference/);
|
|
572
|
+
const outOfRange = clone(base);
|
|
573
|
+
outOfRange.selected_order = 4;
|
|
574
|
+
assert.throws(() => decodeReviewResultArtifactV2(outOfRange), /selected_order/);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("result artifact identities never cross-decode", () => {
|
|
578
|
+
// A prior identity rejects the new surface (it carries no negotiated
|
|
579
|
+
// contract/operation identity pair at all)...
|
|
580
|
+
assert.throws(() => decodeReviewOperationV2(fixture("result-artifact-v2.captured.json")), /contract|schema/);
|
|
581
|
+
// ...the new decoder pins its own exact identity...
|
|
582
|
+
const downgraded = clone(fixture<JsonObject>("result-artifact-v2.captured.json"));
|
|
583
|
+
downgraded.schema = "gentle-ai.review-result-artifact/v1";
|
|
584
|
+
assert.throws(() => decodeReviewResultArtifactV2(downgraded), /schema/);
|
|
585
|
+
// ...and rejects prior surfaces wholesale.
|
|
586
|
+
assert.throws(() => decodeReviewResultArtifactV2({
|
|
587
|
+
schema: "gentle-ai.review-provider-role-capture/v1",
|
|
588
|
+
lineage_id: "review-1d5aadacc600e167",
|
|
589
|
+
target_identity: `sha256:${"9".repeat(64)}`,
|
|
590
|
+
role: "targeted-validator",
|
|
591
|
+
captured: true,
|
|
592
|
+
}));
|
|
593
|
+
assert.throws(() => decodeReviewResultArtifactV2(v2Fixture("status.fixture.json")));
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Fixture provenance:
|
|
597
|
+
// - failure-v2-capture-evidence.captured.json was captured 2026-08-16 from a
|
|
598
|
+
// binary built at gentle-ai commit a2d57117 (branch
|
|
599
|
+
// fix/capture-evidence-typed-refusal, `go build ./cmd/gentle-ai`, version
|
|
600
|
+
// banner "gentle-ai dev"), in a scratch git repository driven to the
|
|
601
|
+
// correction evidence-pending state (medium start with consent granted,
|
|
602
|
+
// one deterministic BLOCKER capture-result, rendered finalize transition,
|
|
603
|
+
// rendered plan-correction submission, bounded fix edit), then a
|
|
604
|
+
// deliberately misbound capture:
|
|
605
|
+
// gentle-ai review capture-evidence --cwd <scratch> --lineage <lineage> \
|
|
606
|
+
// --target <live workspace identity> --expected-revision <revision> \
|
|
607
|
+
// --outcome passed --input <evidence file>
|
|
608
|
+
// The live workspace identity instead of the slot's fix-diff --target is
|
|
609
|
+
// the exact fambig misbinding shape; the branch answers it with the typed
|
|
610
|
+
// failure/v2 envelope on stdout (operation review.capture-evidence, code
|
|
611
|
+
// verification_evidence_binding_mismatch, mutation_outcome not_started)
|
|
612
|
+
// instead of a bare stderr line.
|
|
613
|
+
// - The other three capture-verb operations are constructed from the same
|
|
614
|
+
// captured envelope, grounded in that branch's
|
|
615
|
+
// contracts/review-integration/v2/schemas/failure.schema.json 12-value
|
|
616
|
+
// operation enum; no cheap live flow provokes them individually.
|
|
617
|
+
test("failure/v2 decodes the four collect-capture operations the typed-refusal branch emits", () => {
|
|
618
|
+
const captured = JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "failure-v2-capture-evidence.captured.json"), "utf8")) as Record<string, unknown>;
|
|
619
|
+
const decoded = decodeReviewFailureV2(captured);
|
|
620
|
+
assert.equal(decoded.operation, "review.capture-evidence");
|
|
621
|
+
assert.equal(decoded.code, "verification_evidence_binding_mismatch");
|
|
622
|
+
assert.equal(decoded.mutationOutcome, "not_started");
|
|
623
|
+
assert.equal(decoded.nextAction, "review.status");
|
|
624
|
+
for (const operation of ["review.capture-result", "review.capture-refuter", "review.capture-validation"]) {
|
|
625
|
+
assert.equal(decodeReviewFailureV2({ ...captured, operation }).operation, operation, operation);
|
|
626
|
+
}
|
|
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
|
+
});
|