gentle-pi 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -9
- package/assets/agents/review-readability.md +0 -1
- package/assets/agents/review-reliability.md +0 -1
- package/assets/agents/review-resilience.md +0 -1
- package/assets/agents/review-risk.md +0 -1
- package/contracts/review-integration/v1/fixtures/binding-revision-conflict.fixture.json +25 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.2.fixture.json +263 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.3.fixture.json +301 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.4.fixture.json +312 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.5.fixture.json +321 -0
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v1/fixtures/final-verification-incident.fixture.json +1 -0
- package/contracts/review-integration/v1/fixtures/operation.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/repair-preflight.fixture.json +54 -0
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +97 -0
- package/contracts/review-integration/v1/fixtures/start.fixture.json +25 -1
- package/contracts/review-integration/v1/fixtures/status-ambiguous.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-corrupted.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-recover.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-unrelated.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-v2-ambiguous.fixture.json +110 -0
- package/contracts/review-integration/v1/fixtures/status-v2-corrupted.fixture.json +107 -0
- package/contracts/review-integration/v1/fixtures/status-v2-final-verification-retry.fixture.json +192 -0
- package/contracts/review-integration/v1/fixtures/status-v2-recover.fixture.json +130 -0
- package/contracts/review-integration/v1/fixtures/status-v2-repair.fixture.json +161 -0
- package/contracts/review-integration/v1/fixtures/status-v2-unrelated.fixture.json +103 -0
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +193 -0
- package/contracts/review-integration/v1/fixtures/status.fixture.json +19 -5
- package/contracts/review-integration/v1/fixtures/verification-evidence.fixture.json +1 -0
- package/contracts/review-integration/v1/schemas/admitted-result.schema.json +35 -0
- package/contracts/review-integration/v1/schemas/artifact-subject.schema.json +33 -0
- package/contracts/review-integration/v1/schemas/authority-repair-assessment.schema.json +247 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.2.schema.json +249 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.3.schema.json +429 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.4.schema.json +432 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.5.schema.json +154 -0
- package/contracts/review-integration/v1/schemas/consent.schema.json +86 -0
- package/contracts/review-integration/v1/schemas/failure.schema.json +54 -8
- package/contracts/review-integration/v1/schemas/final-verification-incident.schema.json +51 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +204 -31
- package/contracts/review-integration/v1/schemas/repair.schema.json +251 -0
- package/contracts/review-integration/v1/schemas/result-artifact-v2.schema.json +38 -0
- package/contracts/review-integration/v1/schemas/start-v2.schema.json +216 -0
- package/contracts/review-integration/v1/schemas/start.schema.json +79 -3
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +313 -0
- package/contracts/review-integration/v1/schemas/status.schema.json +37 -10
- package/contracts/review-integration/v1/schemas/targeted-validation-request.schema.json +46 -0
- package/contracts/review-integration/v1/schemas/verification-evidence.schema.json +64 -0
- package/contracts/review-integration/v2/fixtures/capabilities.fixture.json +111 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v2/fixtures/start.fixture.json +93 -0
- package/contracts/review-integration/v2/fixtures/status.fixture.json +113 -0
- package/contracts/review-integration/v2/schemas/admitted-result.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/artifact-subject.schema.json +36 -0
- package/contracts/review-integration/v2/schemas/capabilities.schema.json +111 -0
- package/contracts/review-integration/v2/schemas/consent.schema.json +70 -0
- package/contracts/review-integration/v2/schemas/failure.schema.json +35 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +46 -0
- package/contracts/review-integration/v2/schemas/repair.schema.json +22 -0
- package/contracts/review-integration/v2/schemas/start.schema.json +106 -0
- package/contracts/review-integration/v2/schemas/status.schema.json +104 -0
- package/docs/native-authority-architecture.md +18 -3
- package/docs/review-integration.md +175 -27
- package/extensions/gentle-ai.ts +414 -19
- package/lib/gentle-ai-binary.ts +1 -1
- package/lib/native-review-cli.ts +679 -57
- package/lib/review-candidate-view.ts +131 -1
- package/lib/review-compact-contract.ts +12 -3
- package/lib/review-consent-latch.ts +51 -0
- package/lib/review-correction-lifecycle.ts +170 -0
- package/lib/review-integration-v2.ts +1563 -0
- package/package.json +15 -11
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +663 -41
- package/runtime/review-integration-v2.mjs +1564 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -1
- package/scripts/gentle-ai-installer.mjs +16 -11
- package/scripts/install-gentle-ai.mjs +7 -3
- package/scripts/test-packed-runner.mjs +2 -2
- package/scripts/verify-package-files.mjs +216 -64
- package/skills/release/SKILL.md +38 -11
- package/tests/devbinary/native-review-parity.devtest.ts +348 -0
- package/tests/gentle-ai-binary.test.ts +8 -1
- package/tests/gentle-ai-installer.test.ts +24 -21
- package/tests/native-binary-gate.test.ts +145 -0
- package/tests/native-review-capability-contract.test.ts +87 -0
- package/tests/native-review-cli.test.ts +44 -1
- package/tests/native-review-parity-runtime.test.ts +70 -23
- package/tests/native-review-parity.test.ts +673 -0
- package/tests/native-risk-evidence-parity.test.ts +91 -0
- package/tests/package-manifest.test.ts +72 -7
- package/tests/review-actor-tool-deny.test.ts +20 -5
- package/tests/review-candidate-view.test.ts +331 -1
- package/tests/review-compact-contract.test.ts +5 -0
- package/tests/review-consent-latch.test.ts +67 -0
- package/tests/review-controller-native-recovery.test.ts +180 -5
- package/tests/review-controller-native-routing.test.ts +385 -12
- package/tests/review-controller-workspace-root.test.ts +26 -6
- package/tests/review-correction-lifecycle.test.ts +174 -0
- package/tests/review-integration-v2.test.ts +539 -0
- package/tests/runtime-harness.mjs +187 -0
- package/tests/support/native-binary-gate.ts +86 -0
- package/tests/verify-package-files.test.ts +119 -0
- package/lib/review-integration-v1.ts +0 -795
- package/runtime/review-integration-v1.mjs +0 -796
- package/tests/native-review-integration-v1.test.ts +0 -102
- package/tests/review-integration-v1.test.ts +0 -530
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
NATIVE_REVIEW_ERROR_CODE,
|
|
7
|
-
NativeReviewCliV216,
|
|
8
|
-
NativeReviewCliError,
|
|
9
|
-
NativeReviewIntegrationError,
|
|
10
|
-
clearNativeReviewCapabilitiesCacheForTesting,
|
|
11
|
-
type ExecFileAdapter,
|
|
12
|
-
} from "../lib/native-review-cli.ts";
|
|
13
|
-
|
|
14
|
-
const fixtureRoot = join(process.cwd(), "contracts", "review-integration", "v1", "fixtures");
|
|
15
|
-
const fixture = (name: string): Record<string, unknown> => JSON.parse(readFileSync(join(fixtureRoot, name), "utf8")) as Record<string, unknown>;
|
|
16
|
-
|
|
17
|
-
interface Result {
|
|
18
|
-
stdout: string;
|
|
19
|
-
exitCode?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function queued(results: Result[]): { adapter: ExecFileAdapter; calls: readonly { arguments: readonly string[]; timeoutMs: number | undefined }[] } {
|
|
23
|
-
const calls: { arguments: readonly string[]; timeoutMs: number | undefined }[] = [];
|
|
24
|
-
return {
|
|
25
|
-
calls,
|
|
26
|
-
adapter: async (request) => {
|
|
27
|
-
calls.push({ arguments: request.arguments, timeoutMs: request.timeoutMs });
|
|
28
|
-
const result = results.shift();
|
|
29
|
-
if (result === undefined) throw new Error("unexpected native invocation");
|
|
30
|
-
return { stdout: result.stdout, stderr: "", exitCode: result.exitCode ?? 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test("v2.1.7 negotiates once per verified digest and binds every operation argv", async () => {
|
|
36
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
37
|
-
const digest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
38
|
-
const queue = queued([
|
|
39
|
-
{ stdout: JSON.stringify(fixture("capabilities.fixture.json")) },
|
|
40
|
-
{ stdout: JSON.stringify(fixture("start.fixture.json")) },
|
|
41
|
-
{ stdout: JSON.stringify(fixture("status.fixture.json")) },
|
|
42
|
-
]);
|
|
43
|
-
const client = new NativeReviewCliV216(queue.adapter, "/package/gentle-ai", 321, 654, undefined, () => digest);
|
|
44
|
-
await client.start({ cwd: "/repo with spaces" });
|
|
45
|
-
await client.targetStatus({ cwd: "/repo with spaces", lineageId: "review-status-fixture" });
|
|
46
|
-
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
47
|
-
["review", "capabilities", "--contract", "gentle-ai.review-integration/v1"],
|
|
48
|
-
["review", "start", "--contract", "gentle-ai.review-integration/v1", "--cwd", "/repo with spaces"],
|
|
49
|
-
["review", "status", "--contract", "gentle-ai.review-integration/v1", "--cwd", "/repo with spaces", "--projection", "workspace", "--lineage", "review-status-fixture"],
|
|
50
|
-
]);
|
|
51
|
-
assert.equal(queue.calls[0]?.timeoutMs, 321);
|
|
52
|
-
assert.equal(queue.calls[1]?.timeoutMs, undefined);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("v2.1.7 preserves the native uniform failure envelope", async () => {
|
|
56
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
57
|
-
const digest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
58
|
-
const queue = queued([
|
|
59
|
-
{ stdout: JSON.stringify(fixture("capabilities.fixture.json")) },
|
|
60
|
-
{ stdout: JSON.stringify(fixture("failure.fixture.json")), exitCode: 1 },
|
|
61
|
-
]);
|
|
62
|
-
const client = new NativeReviewCliV216(queue.adapter, "/package/gentle-ai", 321, 654, undefined, () => digest);
|
|
63
|
-
await assert.rejects(
|
|
64
|
-
() => client.finalize({ cwd: "/repo", lineageId: "review-failure-fixture" }),
|
|
65
|
-
(error: unknown) => error instanceof NativeReviewIntegrationError && error.failureEnvelope.code === "gate_scope_changed" && error.mutationOutcome === "not_started" && error.nextAction === "explicit-maintainer-action",
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("capability cache invalidates when the verified executable digest changes", async () => {
|
|
70
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
71
|
-
const firstDigest = "1".repeat(64);
|
|
72
|
-
const secondDigest = "2".repeat(64);
|
|
73
|
-
const first = fixture("capabilities.fixture.json");
|
|
74
|
-
(first.executable as Record<string, unknown>).sha256 = `sha256:${firstDigest}`;
|
|
75
|
-
const second = structuredClone(first);
|
|
76
|
-
(second.executable as Record<string, unknown>).sha256 = `sha256:${secondDigest}`;
|
|
77
|
-
const queue = queued([{ stdout: JSON.stringify(first) }, { stdout: JSON.stringify(second) }]);
|
|
78
|
-
await new NativeReviewCliV216(queue.adapter, "/package/gentle-ai", 321, 654, undefined, () => firstDigest).capabilities();
|
|
79
|
-
await new NativeReviewCliV216(queue.adapter, "/package/gentle-ai", 321, 654, undefined, () => secondDigest).capabilities();
|
|
80
|
-
assert.equal(queue.calls.length, 2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("v2.1.7 bind-sdd rejects a response from the wrong lifecycle gate", async () => {
|
|
84
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
85
|
-
const digest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
86
|
-
const binding = JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "native-review-cli", "v2.1.3", "bind-sdd.json"), "utf8")) as Record<string, unknown>;
|
|
87
|
-
const gateContext = binding.gate_context as Record<string, unknown>;
|
|
88
|
-
const queue = queued([
|
|
89
|
-
{ stdout: JSON.stringify(fixture("capabilities.fixture.json")) },
|
|
90
|
-
{ stdout: JSON.stringify({
|
|
91
|
-
schema: "gentle-ai.review-integration.operation/v1",
|
|
92
|
-
contract: "gentle-ai.review-integration/v1",
|
|
93
|
-
operation: "review.bind_sdd",
|
|
94
|
-
result: { ...binding, gate_context: { ...gateContext, gate: "pre-commit" } },
|
|
95
|
-
}) },
|
|
96
|
-
]);
|
|
97
|
-
const client = new NativeReviewCliV216(queue.adapter, "/package/gentle-ai", 321, 654, undefined, () => digest);
|
|
98
|
-
await assert.rejects(
|
|
99
|
-
() => client.bindSdd({ cwd: "/repo", change: "native-review-authority-parity", lineage: "issue136-contract-runtime", expectedBindingRevision: "" }),
|
|
100
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH,
|
|
101
|
-
);
|
|
102
|
-
});
|
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import test from "node:test";
|
|
5
|
-
import {
|
|
6
|
-
REVIEW_INTEGRATION_CONTRACT,
|
|
7
|
-
REVIEW_START_STATE,
|
|
8
|
-
decodeReviewCapabilitiesV1,
|
|
9
|
-
decodeReviewFailureV1,
|
|
10
|
-
decodeReviewOperationV1,
|
|
11
|
-
decodeReviewProjectionV1,
|
|
12
|
-
decodeReviewStartV1,
|
|
13
|
-
decodeReviewStatusV1,
|
|
14
|
-
} from "../lib/review-integration-v1.ts";
|
|
15
|
-
|
|
16
|
-
const fixtureRoot = join(process.cwd(), "contracts", "review-integration", "v1", "fixtures");
|
|
17
|
-
const fixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(fixtureRoot, name), "utf8")) as T;
|
|
18
|
-
const schema = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(process.cwd(), "contracts", "review-integration", "v1", "schemas", name), "utf8")) as T;
|
|
19
|
-
const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
20
|
-
const digest = `sha256:${"a".repeat(64)}`;
|
|
21
|
-
|
|
22
|
-
type JsonObject = Record<string, unknown>;
|
|
23
|
-
type Decoder = (value: unknown) => unknown;
|
|
24
|
-
|
|
25
|
-
function clone<T>(value: T): T {
|
|
26
|
-
return structuredClone(value);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function assertRequired(decoder: Decoder, source: JsonObject, fields: readonly string[]): void {
|
|
30
|
-
for (const field of fields) {
|
|
31
|
-
const candidate = clone(source);
|
|
32
|
-
delete candidate[field];
|
|
33
|
-
assert.throws(() => decoder(candidate), new RegExp(`${field}.*required|required.*${field}`), field);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function assertNestedRequired(decoder: Decoder, source: JsonObject, path: readonly string[], fields: readonly string[]): void {
|
|
38
|
-
for (const field of fields) {
|
|
39
|
-
const candidate = clone(source);
|
|
40
|
-
let target = candidate;
|
|
41
|
-
for (const segment of path) target = target[segment] as JsonObject;
|
|
42
|
-
delete target[field];
|
|
43
|
-
assert.throws(() => decoder(candidate), /required/, `${path.join(".")}.${field}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function assertAdditionalProperty(decoder: Decoder, source: JsonObject, path: readonly string[] = []): void {
|
|
48
|
-
const candidate = clone(source);
|
|
49
|
-
let target = candidate;
|
|
50
|
-
for (const segment of path) target = target[segment] as JsonObject;
|
|
51
|
-
target.unadvertised = true;
|
|
52
|
-
assert.throws(() => decoder(candidate), /not allowed/, path.length === 0 ? "top-level" : path.join("."));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function finalizeEnvelope(): JsonObject {
|
|
56
|
-
return fixture<JsonObject>("operation.fixture.json");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function validateEnvelope(): JsonObject {
|
|
60
|
-
return {
|
|
61
|
-
schema: "gentle-ai.review-integration.operation/v1",
|
|
62
|
-
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
63
|
-
operation: "review.validate",
|
|
64
|
-
result: {
|
|
65
|
-
schema: "gentle-ai.review-gate-result/v1",
|
|
66
|
-
result: "allow",
|
|
67
|
-
allowed: true,
|
|
68
|
-
action: "publish",
|
|
69
|
-
reason: "receipt matches",
|
|
70
|
-
context: { gate: "pre-pr" },
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function bindEnvelope(): JsonObject {
|
|
76
|
-
return {
|
|
77
|
-
schema: "gentle-ai.review-integration.operation/v1",
|
|
78
|
-
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
79
|
-
operation: "review.bind_sdd",
|
|
80
|
-
result: {
|
|
81
|
-
schema: "gentle-ai.sdd-review-binding/v1",
|
|
82
|
-
revision: digest,
|
|
83
|
-
change: "review-integration-v1",
|
|
84
|
-
lineage: "review-fixture",
|
|
85
|
-
authority_revision: digest,
|
|
86
|
-
receipt_hash: digest,
|
|
87
|
-
gate_context: { gate: "post-apply" },
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
test("every published review integration fixture decodes", () => {
|
|
93
|
-
assert.equal(decodeReviewCapabilitiesV1(fixture("capabilities.fixture.json"), executableDigest).contract, REVIEW_INTEGRATION_CONTRACT);
|
|
94
|
-
assert.equal(decodeReviewCapabilitiesV1(fixture("capabilities-v1.1.fixture.json"), executableDigest).contract, REVIEW_INTEGRATION_CONTRACT);
|
|
95
|
-
assert.equal(decodeReviewStartV1(fixture("start.fixture.json")).riskLevel, "high");
|
|
96
|
-
const failure = decodeReviewFailureV1(fixture("failure.fixture.json"));
|
|
97
|
-
assert.equal(failure.mutationOutcome, "not_started");
|
|
98
|
-
assert.equal(failure.code, "gate_scope_changed");
|
|
99
|
-
assert.equal(decodeReviewOperationV1(fixture("operation.fixture.json")).operation, "review.finalize");
|
|
100
|
-
for (const name of ["status.fixture.json", "status-unrelated.fixture.json", "status-ambiguous.fixture.json", "status-corrupted.fixture.json", "status-recover.fixture.json"]) {
|
|
101
|
-
assert.equal(decodeReviewStatusV1(fixture(name)).contract, REVIEW_INTEGRATION_CONTRACT);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("status accepts a provider-selected recovery disposition only for recover", () => {
|
|
106
|
-
const recover = fixture<JsonObject>("status-recover.fixture.json");
|
|
107
|
-
assert.equal(decodeReviewStatusV1(recover).actionDisposition, "scope_changed");
|
|
108
|
-
for (const disposition of ["invalidated", "escalated"]) {
|
|
109
|
-
const candidate = clone(recover);
|
|
110
|
-
candidate.action_disposition = disposition;
|
|
111
|
-
assert.equal(decodeReviewStatusV1(candidate).actionDisposition, disposition);
|
|
112
|
-
}
|
|
113
|
-
for (const candidate of [
|
|
114
|
-
(() => { const value = clone(recover); delete value.action_disposition; return value; })(),
|
|
115
|
-
{ ...clone(recover), action_disposition: "unknown" },
|
|
116
|
-
{ ...fixture<JsonObject>("status.fixture.json"), action_disposition: "scope_changed" },
|
|
117
|
-
]) assert.throws(() => decodeReviewStatusV1(candidate), /action_disposition/);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("capabilities enforce every required top-level and nested property", () => {
|
|
121
|
-
const source = fixture<JsonObject>("capabilities.fixture.json");
|
|
122
|
-
const decode: Decoder = (value) => decodeReviewCapabilitiesV1(value, executableDigest);
|
|
123
|
-
assertRequired(decode, source, ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"]);
|
|
124
|
-
assertNestedRequired(decode, source, ["protocol"], ["major", "minor"]);
|
|
125
|
-
assertNestedRequired(decode, source, ["package"], ["name", "version", "release_channel"]);
|
|
126
|
-
assertNestedRequired(decode, source, ["build"], ["id", "go_version", "module_version", "vcs", "vcs_revision", "vcs_time", "vcs_modified"]);
|
|
127
|
-
assertNestedRequired(decode, source, ["executable"], ["sha256", "evidence", "verification"]);
|
|
128
|
-
assertNestedRequired(decode, source, ["features"], ["mandatory", "optional"]);
|
|
129
|
-
assertNestedRequired(decode, source, ["features", "mandatory", "0"], ["name", "supported", "requires"]);
|
|
130
|
-
assertNestedRequired(decode, source, ["compatibility"], ["minimum_protocol_major", "maximum_protocol_major", "additive_minor_policy", "unknown_mandatory", "unknown_optional", "modes", "legacy_window"]);
|
|
131
|
-
assertNestedRequired(decode, source, ["compatibility", "legacy_window"], ["mode", "state", "read_only", "deprecation_started", "removal", "minimum_compatibility_releases"]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("capabilities reject additional properties at every exact object boundary", () => {
|
|
135
|
-
const source = fixture<JsonObject>("capabilities.fixture.json");
|
|
136
|
-
const decode: Decoder = (value) => decodeReviewCapabilitiesV1(value, executableDigest);
|
|
137
|
-
for (const path of [[], ["protocol"], ["package"], ["build"], ["executable"], ["features"], ["features", "mandatory", "0"], ["features", "optional", "0"], ["compatibility"], ["compatibility", "legacy_window"]] as const) {
|
|
138
|
-
assertAdditionalProperty(decode, source, path);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("capabilities accept additive minor optional fields while rejecting unknown mandatory behavior", () => {
|
|
143
|
-
const source = fixture<JsonObject>("capabilities-v1.1.fixture.json");
|
|
144
|
-
source.future_diagnostics = { enabled: true };
|
|
145
|
-
(source.package as JsonObject).future_channel_metadata = "additive";
|
|
146
|
-
(((source.features as JsonObject).optional as JsonObject[])).push({
|
|
147
|
-
name: "future_optional_diagnostics",
|
|
148
|
-
supported: true,
|
|
149
|
-
requires: [],
|
|
150
|
-
future_detail: "ignored",
|
|
151
|
-
});
|
|
152
|
-
const decoded = decodeReviewCapabilitiesV1(source, executableDigest);
|
|
153
|
-
assert.equal(decoded.optionalFeatures.has("risk_reasons"), true);
|
|
154
|
-
assert.equal((decoded.optionalFeatures as ReadonlySet<string>).has("future_optional_diagnostics"), false);
|
|
155
|
-
|
|
156
|
-
const unknownMandatory = clone(source);
|
|
157
|
-
((unknownMandatory.features as JsonObject).mandatory as JsonObject[]).push({ name: "future_required_authority", supported: true, requires: [] });
|
|
158
|
-
assert.throws(() => decodeReviewCapabilitiesV1(unknownMandatory, executableDigest), /unsupported/);
|
|
159
|
-
|
|
160
|
-
const incompatibleMajor = clone(source);
|
|
161
|
-
(incompatibleMajor.protocol as JsonObject).major = 2;
|
|
162
|
-
assert.throws(() => decodeReviewCapabilitiesV1(incompatibleMajor, executableDigest), /incompatible/);
|
|
163
|
-
|
|
164
|
-
const negativeMinor = clone(source);
|
|
165
|
-
(negativeMinor.protocol as JsonObject).minor = -1;
|
|
166
|
-
assert.throws(() => decodeReviewCapabilitiesV1(negativeMinor, executableDigest), /integer in range/);
|
|
167
|
-
|
|
168
|
-
const duplicateOptional = clone(source);
|
|
169
|
-
((duplicateOptional.features as JsonObject).optional as JsonObject[]).push({ name: "future_optional_diagnostics", supported: false, requires: [] });
|
|
170
|
-
assert.throws(() => decodeReviewCapabilitiesV1(duplicateOptional, executableDigest), /duplicate/);
|
|
171
|
-
|
|
172
|
-
const overlappingFeature = clone(source);
|
|
173
|
-
((overlappingFeature.features as JsonObject).optional as JsonObject[]).push({ name: "compact_v2_authority", supported: true, requires: [] });
|
|
174
|
-
assert.throws(() => decodeReviewCapabilitiesV1(overlappingFeature, executableDigest), /overlap/);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("capabilities enforce exact v1.0 arrays, enums, patterns, and digest binding", () => {
|
|
178
|
-
const source = fixture<JsonObject>("capabilities.fixture.json");
|
|
179
|
-
const decode = (value: unknown) => decodeReviewCapabilitiesV1(value, executableDigest);
|
|
180
|
-
|
|
181
|
-
const optionalDrift = clone(source);
|
|
182
|
-
(optionalDrift.features as { optional: unknown[] }).optional.push({ name: "risk_reasons", supported: true, requires: [] });
|
|
183
|
-
assert.throws(() => decode(optionalDrift), /invalid length/);
|
|
184
|
-
|
|
185
|
-
for (const field of ["operations", "gates", "projections", "schemas"] as const) {
|
|
186
|
-
const duplicate = clone(source);
|
|
187
|
-
const values = duplicate[field] as unknown[];
|
|
188
|
-
values[values.length - 1] = values[0];
|
|
189
|
-
assert.throws(() => decode(duplicate), /duplicates|required integration surface/, field);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
for (const [path, invalid] of [
|
|
193
|
-
[["package", "release_channel"], "nightly"],
|
|
194
|
-
[["build", "vcs_modified"], "dirty"],
|
|
195
|
-
[["compatibility", "legacy_window", "state"], "removed"],
|
|
196
|
-
[["features", "optional", "0", "name"], "future_optional_diagnostics"],
|
|
197
|
-
] as const) {
|
|
198
|
-
const candidate = clone(source);
|
|
199
|
-
let target = candidate;
|
|
200
|
-
for (const segment of path.slice(0, -1)) target = target[segment] as JsonObject;
|
|
201
|
-
target[path.at(-1)!] = invalid;
|
|
202
|
-
assert.throws(() => decode(candidate));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const badRequires = clone(source);
|
|
206
|
-
((badRequires.features as JsonObject).optional as JsonObject[])[0].requires = ["same", "same"];
|
|
207
|
-
assert.throws(() => decode(badRequires), /duplicates/);
|
|
208
|
-
const emptyGo = clone(source);
|
|
209
|
-
(emptyGo.build as JsonObject).go_version = "";
|
|
210
|
-
assert.throws(() => decode(emptyGo));
|
|
211
|
-
const badMinimum = clone(source);
|
|
212
|
-
((badMinimum.compatibility as JsonObject).legacy_window as JsonObject).minimum_compatibility_releases = 0;
|
|
213
|
-
assert.throws(() => decode(badMinimum));
|
|
214
|
-
const reversedModes = clone(source);
|
|
215
|
-
(reversedModes.compatibility as JsonObject).modes = ["legacy-v1", "compact-v2"];
|
|
216
|
-
assert.throws(() => decode(reversedModes), /out of order/);
|
|
217
|
-
assert.throws(() => decodeReviewCapabilitiesV1(source, "0".repeat(64)), /executable digest mismatch/);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("START accepts every normative enum value and rejects aliases", () => {
|
|
221
|
-
const source = fixture<JsonObject>("start.fixture.json");
|
|
222
|
-
for (const state of Object.values(REVIEW_START_STATE)) {
|
|
223
|
-
const candidate = clone(source);
|
|
224
|
-
candidate.state = state;
|
|
225
|
-
assert.equal(decodeReviewStartV1(candidate).state, state);
|
|
226
|
-
}
|
|
227
|
-
for (const action of ["created", "resumed", "reuse-receipt", "blocked-scope-action"]) {
|
|
228
|
-
const candidate = clone(source);
|
|
229
|
-
candidate.action = action;
|
|
230
|
-
assert.equal(decodeReviewStartV1(candidate).action, action);
|
|
231
|
-
}
|
|
232
|
-
for (const riskLevel of ["low", "medium", "high"]) {
|
|
233
|
-
const candidate = clone(source);
|
|
234
|
-
candidate.risk_level = riskLevel;
|
|
235
|
-
assert.equal(decodeReviewStartV1(candidate).riskLevel, riskLevel);
|
|
236
|
-
}
|
|
237
|
-
for (const projection of ["workspace", "staged"]) {
|
|
238
|
-
const candidate = clone(source);
|
|
239
|
-
candidate.projection = projection;
|
|
240
|
-
assert.equal(decodeReviewStartV1(candidate).projection, projection);
|
|
241
|
-
}
|
|
242
|
-
for (const invalid of ["correction_required", "validating", "clean"]) {
|
|
243
|
-
const candidate = clone(source);
|
|
244
|
-
candidate.state = invalid;
|
|
245
|
-
assert.throws(() => decodeReviewStartV1(candidate), /state/);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("START enforces required, exact, bounded, and deeply unique payloads", () => {
|
|
250
|
-
const source = fixture<JsonObject>("start.fixture.json");
|
|
251
|
-
const startSchema = schema<JsonObject>("start.schema.json");
|
|
252
|
-
const riskReasonCodes = (((startSchema.$defs as JsonObject).risk_reason as JsonObject).properties as JsonObject).code as JsonObject;
|
|
253
|
-
const expectedRiskReasonCodes = ["configuration_change", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"];
|
|
254
|
-
assertRequired(decodeReviewStartV1, source, ["schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level", "selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons"]);
|
|
255
|
-
assertAdditionalProperty(decodeReviewStartV1, source);
|
|
256
|
-
assertAdditionalProperty(decodeReviewStartV1, source, ["risk_reasons", "0"]);
|
|
257
|
-
assertNestedRequired(decodeReviewStartV1, source, ["risk_reasons", "0"], ["code"]);
|
|
258
|
-
|
|
259
|
-
assert.deepEqual(riskReasonCodes.enum, expectedRiskReasonCodes);
|
|
260
|
-
for (const code of expectedRiskReasonCodes) {
|
|
261
|
-
const candidate = clone(source);
|
|
262
|
-
candidate.risk_reasons = [{ code }];
|
|
263
|
-
assert.equal(decodeReviewStartV1(candidate).riskReasons[0]?.code, code);
|
|
264
|
-
}
|
|
265
|
-
for (const signal of ["auth", "update", "security", "payments", "permissions", "shell_process"]) {
|
|
266
|
-
const candidate = clone(source);
|
|
267
|
-
candidate.risk_reasons = [{ code: "hot_path", signal }];
|
|
268
|
-
assert.equal(decodeReviewStartV1(candidate).riskReasons[0]?.signal, signal);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
for (const [field, value] of [["changed_files", -1], ["changed_lines", 1.5], ["correction_budget", 201]] as const) {
|
|
272
|
-
const candidate = clone(source);
|
|
273
|
-
candidate[field] = value;
|
|
274
|
-
assert.throws(() => decodeReviewStartV1(candidate), /range/);
|
|
275
|
-
}
|
|
276
|
-
const tooManyLenses = clone(source);
|
|
277
|
-
tooManyLenses.selected_lenses = ["review-risk", "review-resilience", "review-readability", "review-reliability", "review-risk"];
|
|
278
|
-
assert.throws(() => decodeReviewStartV1(tooManyLenses), /invalid length/);
|
|
279
|
-
const unknownLens = clone(source);
|
|
280
|
-
unknownLens.selected_lenses = ["review-security"];
|
|
281
|
-
assert.throws(() => decodeReviewStartV1(unknownLens), /selected_lenses/);
|
|
282
|
-
const emptyReasons = clone(source);
|
|
283
|
-
emptyReasons.risk_reasons = [];
|
|
284
|
-
assert.throws(() => decodeReviewStartV1(emptyReasons), /invalid length/);
|
|
285
|
-
const duplicateReasons = clone(source);
|
|
286
|
-
duplicateReasons.risk_reasons = [{ code: "hot_path", signal: "auth" }, { signal: "auth", code: "hot_path" }];
|
|
287
|
-
assert.throws(() => decodeReviewStartV1(duplicateReasons), /duplicates/);
|
|
288
|
-
for (const mode of ["100644", "10064", "100648"]) {
|
|
289
|
-
const candidate = clone(source);
|
|
290
|
-
candidate.risk_reasons = [{ code: "executable_mode", old_mode: mode }];
|
|
291
|
-
if (mode === "100644") assert.doesNotThrow(() => decodeReviewStartV1(candidate));
|
|
292
|
-
else assert.throws(() => decodeReviewStartV1(candidate));
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test("projection enforces every kind, Git identity, digest, and safe path boundary", () => {
|
|
297
|
-
const source = (fixture<JsonObject>("status.fixture.json").projection as JsonObject);
|
|
298
|
-
for (const kind of ["current-changes", "base-diff", "exact-revision", "fix-diff"]) {
|
|
299
|
-
const candidate = clone(source);
|
|
300
|
-
candidate.kind = kind;
|
|
301
|
-
assert.equal(decodeReviewProjectionV1(candidate).kind, kind);
|
|
302
|
-
}
|
|
303
|
-
assertRequired(decodeReviewProjectionV1, source, ["schema", "kind", "projection", "base_tree", "initial_review_tree", "current_candidate_tree", "paths_digest", "paths", "intended_untracked", "intended_untracked_proof", "initial_snapshot_identity", "current_snapshot_identity"]);
|
|
304
|
-
assertAdditionalProperty(decodeReviewProjectionV1, source);
|
|
305
|
-
|
|
306
|
-
for (const tree of ["a".repeat(40), "b".repeat(64)]) {
|
|
307
|
-
const candidate = clone(source);
|
|
308
|
-
candidate.base_tree = tree;
|
|
309
|
-
assert.equal(decodeReviewProjectionV1(candidate).baseTree, tree);
|
|
310
|
-
}
|
|
311
|
-
for (const tree of ["a".repeat(39), "A".repeat(40), `sha256:${"a".repeat(64)}`]) {
|
|
312
|
-
const candidate = clone(source);
|
|
313
|
-
candidate.base_tree = tree;
|
|
314
|
-
assert.throws(() => decodeReviewProjectionV1(candidate), /base_tree/);
|
|
315
|
-
}
|
|
316
|
-
for (const path of ["/absolute", "../escape", "safe/../escape", "safe/.."] ) {
|
|
317
|
-
const candidate = clone(source);
|
|
318
|
-
candidate.paths = [path];
|
|
319
|
-
assert.throws(() => decodeReviewProjectionV1(candidate), /paths/);
|
|
320
|
-
}
|
|
321
|
-
const duplicatePaths = clone(source);
|
|
322
|
-
duplicatePaths.paths = ["same", "same"];
|
|
323
|
-
assert.throws(() => decodeReviewProjectionV1(duplicatePaths), /duplicates/);
|
|
324
|
-
const badDigest = clone(source);
|
|
325
|
-
badDigest.paths_digest = `sha256:${"A".repeat(64)}`;
|
|
326
|
-
assert.throws(() => decodeReviewProjectionV1(badDigest), /paths_digest/);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test("status enforces required and additionalProperties at every exact boundary", () => {
|
|
330
|
-
const source = fixture<JsonObject>("status.fixture.json");
|
|
331
|
-
assertRequired(decodeReviewStatusV1, source, ["schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "candidates"]);
|
|
332
|
-
assertNestedRequired(decodeReviewStatusV1, source, ["authority"], ["version", "lineage_id", "state", "generation", "revision"]);
|
|
333
|
-
assertNestedRequired(decodeReviewStatusV1, source, ["receipt"], ["status"]);
|
|
334
|
-
assertNestedRequired(decodeReviewStatusV1, source, ["frozen"], ["tier", "original_changed_lines", "correction_budget"]);
|
|
335
|
-
for (const path of [[], ["authority"], ["receipt"], ["frozen"], ["projection"]] as const) assertAdditionalProperty(decodeReviewStatusV1, source, path);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("status enforces applicability, authority version, receipt, and frozen conditionals", () => {
|
|
339
|
-
const current = fixture<JsonObject>("status.fixture.json");
|
|
340
|
-
for (const name of ["status-unrelated.fixture.json", "status-ambiguous.fixture.json", "status-corrupted.fixture.json"]) assert.doesNotThrow(() => decodeReviewStatusV1(fixture(name)));
|
|
341
|
-
const missingAuthority = clone(current);
|
|
342
|
-
delete missingAuthority.authority;
|
|
343
|
-
assert.throws(() => decodeReviewStatusV1(missingAuthority), /requires authority/);
|
|
344
|
-
const nonCurrentAuthority = fixture<JsonObject>("status-unrelated.fixture.json");
|
|
345
|
-
nonCurrentAuthority.authority = clone(current.authority);
|
|
346
|
-
assert.throws(() => decodeReviewStatusV1(nonCurrentAuthority), /cannot expose authority/);
|
|
347
|
-
const compactMissingFrozen = clone(current);
|
|
348
|
-
delete compactMissingFrozen.frozen;
|
|
349
|
-
assert.throws(() => decodeReviewStatusV1(compactMissingFrozen), /requires frozen/);
|
|
350
|
-
const legacy = clone(current);
|
|
351
|
-
(legacy.authority as JsonObject).version = "legacy-v1";
|
|
352
|
-
delete legacy.frozen;
|
|
353
|
-
assert.doesNotThrow(() => decodeReviewStatusV1(legacy));
|
|
354
|
-
(legacy.receipt as JsonObject).status = "publication_pending";
|
|
355
|
-
assert.throws(() => decodeReviewStatusV1(legacy), /legacy status receipt/);
|
|
356
|
-
|
|
357
|
-
const badLineage = clone(current);
|
|
358
|
-
(badLineage.authority as JsonObject).lineage_id = "Review_Bad";
|
|
359
|
-
assert.throws(() => decodeReviewStatusV1(badLineage), /lineage_id/);
|
|
360
|
-
const badGeneration = clone(current);
|
|
361
|
-
(badGeneration.authority as JsonObject).generation = 0;
|
|
362
|
-
assert.throws(() => decodeReviewStatusV1(badGeneration), /range/);
|
|
363
|
-
const badBudget = clone(current);
|
|
364
|
-
(badBudget.frozen as JsonObject).correction_budget = 201;
|
|
365
|
-
assert.throws(() => decodeReviewStatusV1(badBudget), /range/);
|
|
366
|
-
const badCandidate = clone(current);
|
|
367
|
-
badCandidate.candidates = ["review-a", "review-a"];
|
|
368
|
-
assert.throws(() => decodeReviewStatusV1(badCandidate), /duplicates/);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test("projection accepts the v2.1.7 base-workspace-overlay kind", () => {
|
|
372
|
-
const source = (fixture<JsonObject>("status.fixture.json").projection as JsonObject);
|
|
373
|
-
const candidate = clone(source);
|
|
374
|
-
candidate.kind = "base-workspace-overlay";
|
|
375
|
-
assert.equal(decodeReviewProjectionV1(candidate).kind, "base-workspace-overlay");
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
test("START accepts the complete workspace-overlay target binding and rejects partial bindings", () => {
|
|
379
|
-
const source = fixture<JsonObject>("start.fixture.json");
|
|
380
|
-
const tree = "a".repeat(40);
|
|
381
|
-
const overlay = {
|
|
382
|
-
target_mode: "base-workspace-overlay",
|
|
383
|
-
target_identity: digest,
|
|
384
|
-
base_tree: tree,
|
|
385
|
-
candidate_tree: tree,
|
|
386
|
-
};
|
|
387
|
-
const complete = { ...clone(source), ...overlay };
|
|
388
|
-
const decoded = decodeReviewStartV1(complete);
|
|
389
|
-
assert.equal(decoded.targetMode, "base-workspace-overlay");
|
|
390
|
-
assert.equal(decoded.targetIdentity, digest);
|
|
391
|
-
assert.equal(decoded.baseTree, tree);
|
|
392
|
-
assert.equal(decoded.candidateTree, tree);
|
|
393
|
-
assert.equal(decodeReviewStartV1(fixture("start.fixture.json")).targetMode, undefined);
|
|
394
|
-
for (const missing of ["target_mode", "target_identity", "base_tree", "candidate_tree"]) {
|
|
395
|
-
const partial = { ...clone(source), ...overlay } as JsonObject;
|
|
396
|
-
delete partial[missing];
|
|
397
|
-
assert.throws(() => decodeReviewStartV1(partial), /target|tree/, missing);
|
|
398
|
-
}
|
|
399
|
-
const wrongMode = { ...clone(source), ...overlay, target_mode: "workspace" };
|
|
400
|
-
assert.throws(() => decodeReviewStartV1(wrongMode), /target_mode/);
|
|
401
|
-
const badIdentity = { ...clone(source), ...overlay, target_identity: "not-a-digest" };
|
|
402
|
-
assert.throws(() => decodeReviewStartV1(badIdentity), /target_identity/);
|
|
403
|
-
const badTree = { ...clone(source), ...overlay, base_tree: "zz" };
|
|
404
|
-
assert.throws(() => decodeReviewStartV1(badTree), /base_tree/);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
function reconcileStatus(): JsonObject {
|
|
408
|
-
const source = fixture<JsonObject>("status.fixture.json");
|
|
409
|
-
source.action = "reconcile_finalize";
|
|
410
|
-
source.replayability = "status_required";
|
|
411
|
-
source.reconciliation = { required: true };
|
|
412
|
-
return source;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
test("status accepts the v2.1.7 finalize reconciliation state", () => {
|
|
416
|
-
const decoded = decodeReviewStatusV1(reconcileStatus());
|
|
417
|
-
assert.equal(decoded.action, "reconcile_finalize");
|
|
418
|
-
assert.equal(decoded.replayability, "status_required");
|
|
419
|
-
assert.deepEqual(decoded.reconciliation, { required: true });
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
test("status rejects contradictory finalize reconciliation shapes", () => {
|
|
423
|
-
const missingReconciliation = reconcileStatus();
|
|
424
|
-
delete missingReconciliation.reconciliation;
|
|
425
|
-
assert.throws(() => decodeReviewStatusV1(missingReconciliation), /reconciliation/);
|
|
426
|
-
const wrongReplayability = reconcileStatus();
|
|
427
|
-
wrongReplayability.replayability = "exact_replay_safe";
|
|
428
|
-
assert.throws(() => decodeReviewStatusV1(wrongReplayability), /status_required/);
|
|
429
|
-
const strayReconciliation = fixture<JsonObject>("status.fixture.json");
|
|
430
|
-
strayReconciliation.reconciliation = { required: true };
|
|
431
|
-
assert.throws(() => decodeReviewStatusV1(strayReconciliation), /reconciliation/);
|
|
432
|
-
const notRequired = reconcileStatus();
|
|
433
|
-
notRequired.reconciliation = { required: false };
|
|
434
|
-
assert.throws(() => decodeReviewStatusV1(notRequired), /required/);
|
|
435
|
-
const extraReconciliation = reconcileStatus();
|
|
436
|
-
extraReconciliation.reconciliation = { required: true, extra: true };
|
|
437
|
-
assert.throws(() => decodeReviewStatusV1(extraReconciliation), /not allowed/);
|
|
438
|
-
const nonCurrent = fixture<JsonObject>("status-unrelated.fixture.json");
|
|
439
|
-
nonCurrent.action = "reconcile_finalize";
|
|
440
|
-
nonCurrent.replayability = "status_required";
|
|
441
|
-
nonCurrent.reconciliation = { required: true };
|
|
442
|
-
assert.throws(() => decodeReviewStatusV1(nonCurrent), /current_target/);
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
test("failure accepts v2.1.7 recovery inputs, bounded backoff, bind replay, cause category, and scope-change context", () => {
|
|
446
|
-
const source = fixture<JsonObject>("failure.fixture.json");
|
|
447
|
-
const decoded = decodeReviewFailureV1(source);
|
|
448
|
-
assert.deepEqual(decoded.requiredInputs, ["predecessor_lineage_id", "expected_predecessor_revision", "successor_lineage_id", "disposition", "reason", "actor"]);
|
|
449
|
-
assert.equal(decoded.nextAction, "explicit-maintainer-action");
|
|
450
|
-
assert.ok(decoded.context);
|
|
451
|
-
const bindReplay = clone(source);
|
|
452
|
-
bindReplay.next_action = "review.bind_sdd";
|
|
453
|
-
bindReplay.required_inputs = ["change", "lineage_id", "expected_binding_revision"];
|
|
454
|
-
const bindDecoded = decodeReviewFailureV1(bindReplay);
|
|
455
|
-
assert.equal(bindDecoded.nextAction, "review.bind_sdd");
|
|
456
|
-
assert.deepEqual(bindDecoded.requiredInputs, ["change", "lineage_id", "expected_binding_revision"]);
|
|
457
|
-
const backoff = clone(source);
|
|
458
|
-
backoff.next_action = "retry_with_bounded_backoff";
|
|
459
|
-
assert.equal(decodeReviewFailureV1(backoff).nextAction, "retry_with_bounded_backoff");
|
|
460
|
-
// cause_category is diagnostic metadata, not a routing key: the v2.1.8 emitter
|
|
461
|
-
// already produces "incomplete_store_entry" beyond the vendored schema enum, so
|
|
462
|
-
// the decoder accepts schema values, the known emitter extension, and tolerates
|
|
463
|
-
// forward-compatible unknown snake_case values while rejecting malformed ones.
|
|
464
|
-
for (const category of ["inventory_io_or_layout", "lock_ambiguous", "reset_residue", "record_or_graph_invalid", "inventory_incomplete", "incomplete_store_entry", "future_unknown_cause"]) {
|
|
465
|
-
const candidate = clone(source);
|
|
466
|
-
candidate.cause_category = category;
|
|
467
|
-
assert.equal(decodeReviewFailureV1(candidate).causeCategory, category);
|
|
468
|
-
}
|
|
469
|
-
for (const malformed of ["Cosmic_Rays", "bad-category", "", "with space", 7]) {
|
|
470
|
-
const badCategory = clone(source);
|
|
471
|
-
badCategory.cause_category = malformed;
|
|
472
|
-
assert.throws(() => decodeReviewFailureV1(badCategory), /cause_category/, String(malformed));
|
|
473
|
-
}
|
|
474
|
-
const badContext = clone(source);
|
|
475
|
-
badContext.context = { scope_change: { unexpected: true } };
|
|
476
|
-
assert.throws(() => decodeReviewFailureV1(badContext), /context/);
|
|
477
|
-
const strayContextKey = clone(source);
|
|
478
|
-
(strayContextKey.context as JsonObject).unadvertised = true;
|
|
479
|
-
assert.throws(() => decodeReviewFailureV1(strayContextKey), /not allowed/);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
test("failure enforces exact keys, enums, identifiers, message bounds, and required-input uniqueness", () => {
|
|
483
|
-
const source = fixture<JsonObject>("failure.fixture.json");
|
|
484
|
-
assertRequired(decodeReviewFailureV1, source, ["schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action"]);
|
|
485
|
-
assertAdditionalProperty(decodeReviewFailureV1, source);
|
|
486
|
-
for (const [field, value] of [
|
|
487
|
-
["code", "Bad-Code"],
|
|
488
|
-
["message", "line one\nline two"],
|
|
489
|
-
["message", "x".repeat(241)],
|
|
490
|
-
["lineage_id", "Review_Bad"],
|
|
491
|
-
["request_digest", `sha256:${"A".repeat(64)}`],
|
|
492
|
-
["mutation_outcome", "not_committed"],
|
|
493
|
-
["authority_applicability", "clean"],
|
|
494
|
-
["next_action", "replay"],
|
|
495
|
-
] as const) {
|
|
496
|
-
const candidate = clone(source);
|
|
497
|
-
candidate[field] = value;
|
|
498
|
-
assert.throws(() => decodeReviewFailureV1(candidate), new RegExp(field));
|
|
499
|
-
}
|
|
500
|
-
const duplicateInputs = clone(source);
|
|
501
|
-
duplicateInputs.required_inputs = ["lineage_id", "lineage_id"];
|
|
502
|
-
assert.throws(() => decodeReviewFailureV1(duplicateInputs), /duplicates/);
|
|
503
|
-
const unknownInput = clone(source);
|
|
504
|
-
unknownInput.required_inputs = ["cwd"];
|
|
505
|
-
assert.throws(() => decodeReviewFailureV1(unknownInput), /required_inputs/);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
test("operation envelopes strictly bind the outer operation to one exact result variant", () => {
|
|
509
|
-
const envelopes = [finalizeEnvelope(), validateEnvelope(), bindEnvelope()];
|
|
510
|
-
for (const envelope of envelopes) {
|
|
511
|
-
assert.equal(decodeReviewOperationV1(envelope).operation, envelope.operation);
|
|
512
|
-
assertAdditionalProperty(decodeReviewOperationV1, envelope);
|
|
513
|
-
assertAdditionalProperty(decodeReviewOperationV1, envelope, ["result"]);
|
|
514
|
-
}
|
|
515
|
-
assertRequired(decodeReviewOperationV1, finalizeEnvelope(), ["schema", "contract", "operation", "result"]);
|
|
516
|
-
assertNestedRequired(decodeReviewOperationV1, finalizeEnvelope(), ["result"], ["operation", "lineage_id", "state", "action", "store_revision"]);
|
|
517
|
-
assertNestedRequired(decodeReviewOperationV1, validateEnvelope(), ["result"], ["schema", "result", "allowed", "action", "reason", "context"]);
|
|
518
|
-
assertNestedRequired(decodeReviewOperationV1, bindEnvelope(), ["result"], ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
|
|
519
|
-
|
|
520
|
-
for (const [operation, result] of [
|
|
521
|
-
["review.finalize", validateEnvelope().result],
|
|
522
|
-
["review.validate", bindEnvelope().result],
|
|
523
|
-
["review.bind_sdd", finalizeEnvelope().result],
|
|
524
|
-
] as const) {
|
|
525
|
-
const candidate = finalizeEnvelope();
|
|
526
|
-
candidate.operation = operation;
|
|
527
|
-
candidate.result = result;
|
|
528
|
-
assert.throws(() => decodeReviewOperationV1(candidate), /does not match|not allowed|required/);
|
|
529
|
-
}
|
|
530
|
-
});
|