gentle-pi 2.2.0 → 2.4.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 +76 -79
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +56 -249
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +20 -40
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2349 -3785
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/agent-home.ts +8 -0
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +387 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +324 -96
- package/lib/review-integration-v2.ts +868 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +179 -68
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +337 -890
- package/runtime/review-integration-v2.mjs +816 -255
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +23 -23
- package/skills/_shared/review-ledger-contract.md +14 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +53 -93
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +821 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +45 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +152 -36
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-contract-prompt.test.ts +139 -0
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +328 -91
- package/tests/review-host-relay.test.ts +436 -14
- package/tests/review-integration-v2-forward.test.ts +623 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +362 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +366 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
export const REVIEW_INTEGRATION_CONTRACT = "gentle-ai.review-integration/v2";
|
|
2
2
|
|
|
3
3
|
export const REVIEW_INTEGRATION_OPERATION = {
|
|
4
|
-
BIND_SDD: "review.bind_sdd",
|
|
5
|
-
CAPABILITIES: "review.capabilities",
|
|
6
|
-
FINALIZE: "review.finalize",
|
|
7
4
|
REPAIR: "review.repair",
|
|
8
5
|
START: "review.start",
|
|
9
6
|
STATUS: "review.status",
|
|
10
|
-
VALIDATE: "review.validate",
|
|
11
7
|
} as const;
|
|
12
8
|
export type ReviewIntegrationOperation = (typeof REVIEW_INTEGRATION_OPERATION)[keyof typeof REVIEW_INTEGRATION_OPERATION];
|
|
13
9
|
|
|
@@ -78,30 +74,27 @@ export const REVIEW_START_STATE = {
|
|
|
78
74
|
} as const;
|
|
79
75
|
export type ReviewStartState = (typeof REVIEW_START_STATE)[keyof typeof REVIEW_START_STATE];
|
|
80
76
|
|
|
81
|
-
const START_ACTIONS = ["created", "resumed", "
|
|
77
|
+
const START_ACTIONS = ["created", "resumed", "closed", "blocked-scope-action"] as const;
|
|
82
78
|
const RISK_LEVELS = ["low", "medium", "high"] as const;
|
|
83
79
|
const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
|
|
84
80
|
const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] as const;
|
|
85
81
|
const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] as const;
|
|
86
|
-
const STATUS_ACTIONS = ["start", "
|
|
82
|
+
const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop"] as const;
|
|
83
|
+
const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
|
|
84
|
+
type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
|
|
87
85
|
export const REVIEW_STATUS_ACTION_DISPOSITION = {
|
|
88
86
|
SCOPE_CHANGED: "scope_changed",
|
|
89
87
|
INVALIDATED: "invalidated",
|
|
90
88
|
ESCALATED: "escalated",
|
|
91
|
-
FINAL_VERIFICATION_RETRY: "final_verification_retry",
|
|
92
89
|
} as const;
|
|
93
90
|
export type ReviewStatusActionDisposition = (typeof REVIEW_STATUS_ACTION_DISPOSITION)[keyof typeof REVIEW_STATUS_ACTION_DISPOSITION];
|
|
94
|
-
const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
|
|
95
91
|
const REQUIRED_OPERATIONS = Object.freeze(Object.values(REVIEW_INTEGRATION_OPERATION));
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// fix/capture-evidence-typed-refusal; exact strings from that branch's
|
|
99
|
-
// published failure.schema.json 12-value operation enum). Additive forward
|
|
100
|
-
// surface: every operation outside the published enum still rejects.
|
|
92
|
+
// Typed failures are accepted only for the supported start, status, repair,
|
|
93
|
+
// and last-event capture operations. Retired lifecycle verbs fail closed.
|
|
101
94
|
const FAILURE_OPERATIONS = Object.freeze([
|
|
102
95
|
...REQUIRED_OPERATIONS,
|
|
103
96
|
"review.capture-result",
|
|
104
|
-
"review.capture-
|
|
97
|
+
"review.capture-correction-plan",
|
|
105
98
|
"review.capture-refuter",
|
|
106
99
|
"review.capture-validation",
|
|
107
100
|
] as const);
|
|
@@ -124,7 +117,6 @@ const REQUIRED_SCHEMAS_COMMON = Object.freeze([
|
|
|
124
117
|
"gentle-ai.review-integration.operation/v2",
|
|
125
118
|
"gentle-ai.review-integration.projection/v1",
|
|
126
119
|
"gentle-ai.review-integration.repair/v2",
|
|
127
|
-
"gentle-ai.review-integration.start/v3",
|
|
128
120
|
"gentle-ai.review-receipt/v1",
|
|
129
121
|
"gentle-ai.review-receipt/v2",
|
|
130
122
|
"gentle-ai.review-result-artifact/v2",
|
|
@@ -134,18 +126,75 @@ const REQUIRED_SCHEMAS_COMMON = Object.freeze([
|
|
|
134
126
|
"https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
135
127
|
"https://gentle-ai.dev/schema/review/validator/v1",
|
|
136
128
|
] as const);
|
|
137
|
-
|
|
129
|
+
// The v2.3 provider contract (first advertised by the pinned v2.5.0-rc.3
|
|
130
|
+
// runtime) retired exact_receipt_replay, five_delivery_gates, and
|
|
131
|
+
// sdd_receipt_binding from the mandatory feature set and
|
|
132
|
+
// exact_gate_receipt_discovery, one_shot_final_verification_retry, and
|
|
133
|
+
// outcome_bound_verification_evidence from the optional set; none of them is
|
|
134
|
+
// consumed by Pi's negotiated v2 lane. Earlier minors keep their frozen
|
|
135
|
+
// requirement sets unchanged.
|
|
136
|
+
const REQUIRED_MANDATORY_FEATURES_V23 = Object.freeze([
|
|
137
|
+
"compact_v2_authority",
|
|
138
|
+
"immutable_snapshot",
|
|
139
|
+
"legacy_v1_target_scoped_read_only",
|
|
140
|
+
"repository_independent_capabilities",
|
|
141
|
+
"restart_safe_projection",
|
|
142
|
+
"target_scoped_status",
|
|
143
|
+
"uniform_failure_envelope",
|
|
144
|
+
] as const);
|
|
145
|
+
// v2.3 (first advertised by the pinned v2.5.0-rc.3 provider) retired three
|
|
146
|
+
// v1-era identities from its advertisement. Pi never decodes those envelopes
|
|
147
|
+
// on the negotiated v2 lane, so the v2.3 requirement list is the exact set the
|
|
148
|
+
// v2.3 provider contract defines; earlier minors keep the full common list.
|
|
149
|
+
// The load-time length assertion keeps this filter honest: if the common-list
|
|
150
|
+
// identifier form ever drifts away from these bare identifiers, the module
|
|
151
|
+
// fails loudly instead of silently requiring schemas the provider retired.
|
|
152
|
+
const RETIRED_SCHEMAS_V23: readonly string[] = Object.freeze([
|
|
153
|
+
"gentle-ai.review-final-verification-incident/v1",
|
|
154
|
+
"gentle-ai.review-receipt/v2",
|
|
155
|
+
"gentle-ai.review-verification-evidence/v2",
|
|
156
|
+
]);
|
|
157
|
+
const REQUIRED_SCHEMAS_COMMON_V23 = Object.freeze(REQUIRED_SCHEMAS_COMMON.filter((schema) => !RETIRED_SCHEMAS_V23.includes(schema)));
|
|
158
|
+
if (REQUIRED_SCHEMAS_COMMON_V23.length !== REQUIRED_SCHEMAS_COMMON.length - RETIRED_SCHEMAS_V23.length) {
|
|
159
|
+
throw new TypeError("v2.3 retired-schema filter must remove exactly the retired identities from the common schema list");
|
|
160
|
+
}
|
|
161
|
+
const CAPABILITIES_SCHEMA_IDENTITIES: Readonly<Record<string, { protocolMinor: number; requiredSchemas: readonly string[]; requiredMandatoryFeatures?: readonly string[]; optionalFeatureFloor?: number }>> = Object.freeze({
|
|
138
162
|
"gentle-ai.review-integration.capabilities/v2": Object.freeze({
|
|
139
163
|
protocolMinor: 0,
|
|
140
|
-
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2", "gentle-ai.review-integration.consent/v2", "gentle-ai.review-integration.status/v3"]),
|
|
164
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2", "gentle-ai.review-integration.consent/v2", "gentle-ai.review-integration.start/v3", "gentle-ai.review-integration.status/v3"]),
|
|
141
165
|
}),
|
|
142
166
|
"gentle-ai.review-integration.capabilities/v2.1": Object.freeze({
|
|
143
167
|
protocolMinor: 1,
|
|
144
|
-
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.1", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v3"]),
|
|
168
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.1", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v3", "gentle-ai.review-integration.status/v3"]),
|
|
145
169
|
}),
|
|
146
170
|
"gentle-ai.review-integration.capabilities/v2.2": Object.freeze({
|
|
147
171
|
protocolMinor: 2,
|
|
148
|
-
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.2", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v5"]),
|
|
172
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.2", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v3", "gentle-ai.review-integration.status/v5"]),
|
|
173
|
+
}),
|
|
174
|
+
"gentle-ai.review-integration.capabilities/v2.3": Object.freeze({
|
|
175
|
+
protocolMinor: 3,
|
|
176
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.3", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v5"]),
|
|
177
|
+
requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
|
|
178
|
+
optionalFeatureFloor: 14,
|
|
179
|
+
}),
|
|
180
|
+
"gentle-ai.review-integration.capabilities/v2.4": Object.freeze({
|
|
181
|
+
protocolMinor: 4,
|
|
182
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.4", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
|
|
183
|
+
requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
|
|
184
|
+
optionalFeatureFloor: 14,
|
|
185
|
+
}),
|
|
186
|
+
// Ground-truthed against the published v2.6.0 binary: capabilities/v2.5
|
|
187
|
+
// advertises the same required surface as v2.4 (status/v6 is still
|
|
188
|
+
// advertised for compatibility) plus the new status/v7 schema, which is a
|
|
189
|
+
// superset-checked addition, not a requirement -- decodeReviewStatusV3
|
|
190
|
+
// accepts v7 as an additive extension of v6, so the required-schema floor
|
|
191
|
+
// stays unchanged. The v2.6.0 binary advertised 15 optional features
|
|
192
|
+
// (floor stays at 14, its established minimum).
|
|
193
|
+
"gentle-ai.review-integration.capabilities/v2.5": Object.freeze({
|
|
194
|
+
protocolMinor: 5,
|
|
195
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.5", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
|
|
196
|
+
requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
|
|
197
|
+
optionalFeatureFloor: 14,
|
|
149
198
|
}),
|
|
150
199
|
});
|
|
151
200
|
const OPTIONAL_FEATURE_NAMES = Object.freeze([
|
|
@@ -204,7 +253,6 @@ type ReviewLens = (typeof REVIEW_LENSES)[number];
|
|
|
204
253
|
type RiskReasonCode = (typeof RISK_REASON_CODES)[number];
|
|
205
254
|
type RiskSignal = (typeof RISK_SIGNALS)[number];
|
|
206
255
|
type ReviewStatusAction = (typeof STATUS_ACTIONS)[number];
|
|
207
|
-
type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
|
|
208
256
|
|
|
209
257
|
export interface ReviewFeatureV2 {
|
|
210
258
|
name: (typeof FEATURE_NAMES)[number];
|
|
@@ -300,6 +348,11 @@ export interface ReviewStartV3 {
|
|
|
300
348
|
raw: Readonly<Record<string, unknown>>;
|
|
301
349
|
}
|
|
302
350
|
|
|
351
|
+
export interface ReviewStartV4 extends Omit<ReviewStartV3, "action"> {
|
|
352
|
+
action: "created" | "replayed" | "closed" | "blocked-scope-action";
|
|
353
|
+
nextTransition?: ReviewNextTransitionV3;
|
|
354
|
+
}
|
|
355
|
+
|
|
303
356
|
export interface ReviewProjectionDescriptorV1 {
|
|
304
357
|
schema: "gentle-ai.review-integration.projection/v1";
|
|
305
358
|
kind: ReviewProjectionKind;
|
|
@@ -347,6 +400,7 @@ export interface ReviewTransitionArgumentV3 {
|
|
|
347
400
|
export interface ReviewNextTransitionExecuteV3 {
|
|
348
401
|
operation: string;
|
|
349
402
|
arguments: readonly ReviewTransitionArgumentV3[];
|
|
403
|
+
selectorArguments?: readonly ReviewTransitionArgumentV3[];
|
|
350
404
|
preconditions: readonly ReviewTransitionArgumentV3[];
|
|
351
405
|
binding: { targetIdentity: string; lineageId?: string; revision?: string };
|
|
352
406
|
command?: string;
|
|
@@ -369,6 +423,8 @@ export interface ReviewCaptureSubmissionValueV1 {
|
|
|
369
423
|
* array rows never carry it and existing consumers stay untouched.
|
|
370
424
|
*/
|
|
371
425
|
schema?: string;
|
|
426
|
+
minimum?: number;
|
|
427
|
+
maximum?: number;
|
|
372
428
|
}
|
|
373
429
|
|
|
374
430
|
export interface ReviewCaptureSubmissionV1 {
|
|
@@ -411,27 +467,6 @@ export interface ReviewProviderTaskV1 {
|
|
|
411
467
|
prompt: string;
|
|
412
468
|
}
|
|
413
469
|
|
|
414
|
-
export interface ReviewSubmissionDescriptorValueV5 {
|
|
415
|
-
slot: string;
|
|
416
|
-
domain: string;
|
|
417
|
-
substitutionLocation: number;
|
|
418
|
-
schema?: string;
|
|
419
|
-
minimum?: number;
|
|
420
|
-
maximum?: number;
|
|
421
|
-
allowedValues?: readonly string[];
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// The two v5 Go-owned submission descriptor forms (finalize carries one
|
|
425
|
-
// singular `value`; capture-evidence carries an outcome/input `values` pair).
|
|
426
|
-
// Distinct from the host-relay ReviewCaptureSubmissionV1 form, which stays
|
|
427
|
-
// exactly as it is.
|
|
428
|
-
export interface ReviewSubmissionDescriptorV5 {
|
|
429
|
-
operationToken: "finalize" | "capture-evidence";
|
|
430
|
-
argumentTokens: readonly string[];
|
|
431
|
-
value?: ReviewSubmissionDescriptorValueV5;
|
|
432
|
-
values?: readonly ReviewSubmissionDescriptorValueV5[];
|
|
433
|
-
}
|
|
434
|
-
|
|
435
470
|
export interface ReviewCorrectionPlanFindingV1 {
|
|
436
471
|
id: string;
|
|
437
472
|
lens: "risk" | "resilience" | "readability" | "reliability";
|
|
@@ -455,6 +490,45 @@ export interface ReviewCorrectionPlanRequestV1 {
|
|
|
455
490
|
findings: readonly ReviewCorrectionPlanFindingV1[];
|
|
456
491
|
}
|
|
457
492
|
|
|
493
|
+
export interface ReviewTargetedValidationFindingV1 {
|
|
494
|
+
id: string;
|
|
495
|
+
lens: "risk" | "resilience" | "readability" | "reliability";
|
|
496
|
+
location: string;
|
|
497
|
+
severity: "BLOCKER" | "CRITICAL";
|
|
498
|
+
claim: string;
|
|
499
|
+
proofRefs: readonly string[];
|
|
500
|
+
evidenceClass: "deterministic" | "inferential";
|
|
501
|
+
causalDisposition: "introduced" | "behavior-activated" | "worsened";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export interface ReviewTargetedValidationClassificationV1 {
|
|
505
|
+
findingId: string;
|
|
506
|
+
// The provider's FindingEvidence carries severity as an omitempty field, so
|
|
507
|
+
// a classification may or may not name one. Rejecting it made every
|
|
508
|
+
// targeted-validation STATUS that carried one undecodable.
|
|
509
|
+
severity?: string;
|
|
510
|
+
class: "deterministic" | "inferential";
|
|
511
|
+
causalDisposition: "introduced" | "behavior-activated" | "worsened";
|
|
512
|
+
proof: string;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export interface ReviewTargetedValidationRequestV1 {
|
|
516
|
+
schema: "gentle-ai.review-targeted-validation-request/v1";
|
|
517
|
+
requestHash: string;
|
|
518
|
+
lineageId: string;
|
|
519
|
+
expectedRevision: string;
|
|
520
|
+
targetIdentity: string;
|
|
521
|
+
fixFindingIds: readonly string[];
|
|
522
|
+
policyContent: string;
|
|
523
|
+
fixFindings: readonly ReviewTargetedValidationFindingV1[];
|
|
524
|
+
fixClassifications: readonly ReviewTargetedValidationClassificationV1[];
|
|
525
|
+
projection: ReviewProjection;
|
|
526
|
+
correctionCandidateTree: string;
|
|
527
|
+
correctionTargetIdentity: string;
|
|
528
|
+
correctionPaths: readonly string[];
|
|
529
|
+
correctionPathsDigest: string;
|
|
530
|
+
}
|
|
531
|
+
|
|
458
532
|
export interface ReviewCollectInputV3 {
|
|
459
533
|
name: string;
|
|
460
534
|
schema: string;
|
|
@@ -464,12 +538,11 @@ export interface ReviewCollectInputV3 {
|
|
|
464
538
|
baseTree?: string;
|
|
465
539
|
candidateTree?: string;
|
|
466
540
|
changedPathManifest?: readonly ChangedPathEntry[];
|
|
467
|
-
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
468
541
|
submission?: ReviewCaptureSubmissionV1;
|
|
469
|
-
/** status/v5 only: the finalize/capture-evidence descriptor forms. */
|
|
470
|
-
submissionDescriptor?: ReviewSubmissionDescriptorV5;
|
|
471
542
|
/** status/v5 only: the self-contained external.run_provider_role task. */
|
|
472
543
|
providerTask?: ReviewProviderTaskV1;
|
|
544
|
+
/** status/v5 only: the provider-owned request for a Go-owned targeted validator. */
|
|
545
|
+
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
473
546
|
}
|
|
474
547
|
|
|
475
548
|
export interface ReviewNextTransitionV3 {
|
|
@@ -481,35 +554,12 @@ export interface ReviewNextTransitionV3 {
|
|
|
481
554
|
correctionRequest?: ReviewCorrectionPlanRequestV1;
|
|
482
555
|
}
|
|
483
556
|
|
|
484
|
-
export interface ReviewTargetedValidationRequestV1 {
|
|
485
|
-
schema: "gentle-ai.review-targeted-validation-request/v1";
|
|
486
|
-
requestHash: string;
|
|
487
|
-
lineageId: string;
|
|
488
|
-
expectedRevision: string;
|
|
489
|
-
targetIdentity: string;
|
|
490
|
-
fixFindingIds: readonly string[];
|
|
491
|
-
projection: ReviewProjection;
|
|
492
|
-
correctionCandidateTree: string;
|
|
493
|
-
correctionTargetIdentity: string;
|
|
494
|
-
correctionPaths: readonly string[];
|
|
495
|
-
correctionPathsDigest: string;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
export interface ReviewFinalVerificationRetryV1 {
|
|
499
|
-
incidentSchema: "gentle-ai.review-final-verification-incident/v1";
|
|
500
|
-
incidentClass: "procedural_tooling_failure";
|
|
501
|
-
validatingRevision: string;
|
|
502
|
-
targetIdentity: string;
|
|
503
|
-
failedEvidenceHash: string;
|
|
504
|
-
failedEvidenceRecordDigest?: string;
|
|
505
|
-
finalizeRequestDigest: string;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
557
|
export interface ReviewStatusV3 {
|
|
509
558
|
contract: typeof REVIEW_INTEGRATION_CONTRACT;
|
|
510
559
|
applicability: Exclude<ReviewAuthorityApplicability, "not_evaluated">;
|
|
511
560
|
authority?: ReviewStatusAuthorityV1;
|
|
512
|
-
|
|
561
|
+
/** status/v5 compatibility metadata; it is decoded but never used as routing authority. */
|
|
562
|
+
receipt?: ReviewStatusReceiptV1;
|
|
513
563
|
action: ReviewStatusAction;
|
|
514
564
|
actionDisposition?: ReviewStatusActionDisposition;
|
|
515
565
|
replayability: ReviewReplayability;
|
|
@@ -521,8 +571,6 @@ export interface ReviewStatusV3 {
|
|
|
521
571
|
repair: AuthorityRepairAssessmentV1;
|
|
522
572
|
candidates: readonly string[];
|
|
523
573
|
nextTransition?: ReviewNextTransitionV3;
|
|
524
|
-
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
525
|
-
finalVerificationRetry?: ReviewFinalVerificationRetryV1;
|
|
526
574
|
/** status/v5 only: the descriptive, non-routing transition preview. */
|
|
527
575
|
forecast?: ReviewForecastV1;
|
|
528
576
|
/**
|
|
@@ -532,6 +580,10 @@ export interface ReviewStatusV3 {
|
|
|
532
580
|
* published status-v5.schema.json omits it (capture is authoritative).
|
|
533
581
|
*/
|
|
534
582
|
repositoryContext?: ReviewRepositoryContextV2;
|
|
583
|
+
/** status/v5 only: the provider-owned request mirrored by the targeted-validator collect input. */
|
|
584
|
+
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
585
|
+
/** status/v7 only: the eligible untracked-path inventory digest, absent on the `staged` projection. */
|
|
586
|
+
eligibleUntrackedInventory?: string;
|
|
535
587
|
raw: Readonly<Record<string, unknown>>;
|
|
536
588
|
}
|
|
537
589
|
|
|
@@ -566,9 +618,17 @@ export interface ReviewConsentV2 {
|
|
|
566
618
|
// blocking question with one net-new required member, the provider-fixed
|
|
567
619
|
// `agent` runtime binding. Everything shared with v2 keeps its exact shape so
|
|
568
620
|
// consumers of either identity read one structural surface.
|
|
621
|
+
export const REVIEW_CONSENT_AGENT_V3 = {
|
|
622
|
+
CLAUDE_CODE: "claude-code",
|
|
623
|
+
OPENCODE: "opencode",
|
|
624
|
+
CODEX: "codex",
|
|
625
|
+
PI: "pi",
|
|
626
|
+
} as const;
|
|
627
|
+
export type ReviewConsentAgentV3 = (typeof REVIEW_CONSENT_AGENT_V3)[keyof typeof REVIEW_CONSENT_AGENT_V3];
|
|
628
|
+
|
|
569
629
|
export interface ReviewConsentV3 extends Omit<ReviewConsentV2, "schema"> {
|
|
570
630
|
schema: "gentle-ai.review-integration.consent/v3";
|
|
571
|
-
agent:
|
|
631
|
+
agent: ReviewConsentAgentV3;
|
|
572
632
|
}
|
|
573
633
|
|
|
574
634
|
// Either accepted consent identity. Consumers that relay the envelope (rather
|
|
@@ -585,12 +645,11 @@ const FAILURE_REQUIRED_INPUTS = [
|
|
|
585
645
|
"disposition",
|
|
586
646
|
"reason",
|
|
587
647
|
"actor",
|
|
588
|
-
"incident",
|
|
589
648
|
"maintainer_authorization",
|
|
590
649
|
"base_ref",
|
|
591
650
|
] as const;
|
|
592
651
|
export type ReviewFailureRequiredInputV2 = (typeof FAILURE_REQUIRED_INPUTS)[number];
|
|
593
|
-
const FAILURE_NEXT_ACTIONS = ["correct_request", "retry", "retry_with_bounded_backoff", "review.status", "review.
|
|
652
|
+
const FAILURE_NEXT_ACTIONS = ["correct_request", "retry", "retry_with_bounded_backoff", "review.status", "review.repair", "explicit-maintainer-action", "stop"] as const;
|
|
594
653
|
export type ReviewFailureNextActionV2 = (typeof FAILURE_NEXT_ACTIONS)[number];
|
|
595
654
|
// Known cause_category values: the vendored failure.schema.json enum plus
|
|
596
655
|
// "incomplete_store_entry", which the v2.1.8 emitter produces beyond that enum.
|
|
@@ -625,7 +684,7 @@ export interface ReviewFailureContextV2 {
|
|
|
625
684
|
bindingRevision?: ReviewFailureBindingRevisionV1;
|
|
626
685
|
}
|
|
627
686
|
|
|
628
|
-
export type ReviewFailureOperation = ReviewIntegrationOperation | "review.capture-result" | "review.capture-
|
|
687
|
+
export type ReviewFailureOperation = ReviewIntegrationOperation | "review.capture-result" | "review.capture-correction-plan" | "review.capture-refuter" | "review.capture-validation";
|
|
629
688
|
|
|
630
689
|
export interface ReviewFailureV2 {
|
|
631
690
|
schema: "gentle-ai.review-integration.failure/v2";
|
|
@@ -649,11 +708,32 @@ export interface ReviewFailureV2 {
|
|
|
649
708
|
raw: Readonly<Record<string, unknown>>;
|
|
650
709
|
}
|
|
651
710
|
|
|
652
|
-
export
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
711
|
+
export const REVIEW_ADVISORY_FINDING_SEVERITY = {
|
|
712
|
+
BLOCKER: "BLOCKER",
|
|
713
|
+
CRITICAL: "CRITICAL",
|
|
714
|
+
WARNING: "WARNING",
|
|
715
|
+
SUGGESTION: "SUGGESTION",
|
|
716
|
+
} as const;
|
|
717
|
+
export type ReviewAdvisoryFindingSeverity = (typeof REVIEW_ADVISORY_FINDING_SEVERITY)[keyof typeof REVIEW_ADVISORY_FINDING_SEVERITY];
|
|
718
|
+
|
|
719
|
+
export const REVIEW_ADVISORY_FINDING_DISPOSITION = {
|
|
720
|
+
INFORMATIONAL: "informational",
|
|
721
|
+
FOLLOW_UP: "follow_up",
|
|
722
|
+
REFUTED: "refuted",
|
|
723
|
+
} as const;
|
|
724
|
+
export type ReviewAdvisoryFindingDisposition = (typeof REVIEW_ADVISORY_FINDING_DISPOSITION)[keyof typeof REVIEW_ADVISORY_FINDING_DISPOSITION];
|
|
725
|
+
|
|
726
|
+
export interface ReviewAdvisoryFindingV1 {
|
|
727
|
+
id: string;
|
|
728
|
+
lens?: string;
|
|
729
|
+
location?: string;
|
|
730
|
+
severity: ReviewAdvisoryFindingSeverity;
|
|
731
|
+
disposition: ReviewAdvisoryFindingDisposition;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export interface ReviewAdvisoryFindingsV1 {
|
|
735
|
+
statement: string;
|
|
736
|
+
findings: readonly ReviewAdvisoryFindingV1[];
|
|
657
737
|
}
|
|
658
738
|
|
|
659
739
|
export interface AuthorityRepairAssessmentCandidateV1 {
|
|
@@ -905,11 +985,12 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
|
|
|
905
985
|
assertSupersetOf(advertisedSchemas, identity.requiredSchemas, "capabilities schemas");
|
|
906
986
|
|
|
907
987
|
const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
|
|
908
|
-
const
|
|
909
|
-
const
|
|
988
|
+
const requiredMandatoryFeatures = identity.requiredMandatoryFeatures ?? REQUIRED_MANDATORY_FEATURES;
|
|
989
|
+
const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: requiredMandatoryFeatures.length });
|
|
990
|
+
const optional = array(features.optional, "capabilities.features.optional", (entry, label) => decodeOptionalFeature(entry, label), { minimum: identity.optionalFeatureFloor ?? 17, unique: true });
|
|
910
991
|
const mandatoryNames = mandatory.map((feature) => feature.name);
|
|
911
992
|
const optionalNames = optional.map((feature) => feature.name);
|
|
912
|
-
assertExactSet(mandatoryNames,
|
|
993
|
+
assertExactSet(mandatoryNames, requiredMandatoryFeatures, "mandatory capabilities");
|
|
913
994
|
if (new Set(optionalNames).size !== optionalNames.length) throw new TypeError("optional capabilities contain duplicate names");
|
|
914
995
|
if (optionalNames.some((name) => mandatoryNames.includes(name as ReviewFeatureV2["name"]))) throw new TypeError("mandatory and optional capabilities overlap");
|
|
915
996
|
if (mandatory.some((feature) => !feature.supported)) throw new TypeError("mandatory capability is unsupported");
|
|
@@ -1001,12 +1082,12 @@ function decodeChangedPathEntry(value: unknown, label: string): ChangedPathEntry
|
|
|
1001
1082
|
// start/v3
|
|
1002
1083
|
// ---------------------------------------------------------------------------
|
|
1003
1084
|
|
|
1004
|
-
export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
1085
|
+
export function decodeReviewStartV3(value: unknown, overlayIdentitySatisfiesRepositoryContext = false): ReviewStartV3 {
|
|
1005
1086
|
const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] as const;
|
|
1006
1087
|
const body = exactRecord(value, "start", [
|
|
1007
1088
|
"schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
|
|
1008
1089
|
"selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
|
|
1009
|
-
], [...overlayFields, "changed_path_manifest", "repository_context"]);
|
|
1090
|
+
], [...overlayFields, "changed_path_manifest", "repository_context", "acknowledgement"]);
|
|
1010
1091
|
requireIdentity(body, "gentle-ai.review-integration.start/v3", REVIEW_INTEGRATION_OPERATION.START);
|
|
1011
1092
|
|
|
1012
1093
|
// dependentRequired binds base_tree<->candidate_tree bidirectionally, and
|
|
@@ -1035,9 +1116,12 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
|
1035
1116
|
throw new TypeError("start with selected_lenses requires base_tree, candidate_tree, and changed_path_manifest");
|
|
1036
1117
|
}
|
|
1037
1118
|
|
|
1038
|
-
const
|
|
1119
|
+
const reviewingCreatedOrResumed = (action === "created" || action === "resumed") && state === REVIEW_START_STATE.REVIEWING;
|
|
1120
|
+
// START/v4 may render the frozen target as the overlay identity instead of
|
|
1121
|
+
// repository_context; absence is only excused when that identity is present.
|
|
1122
|
+
const requiresRepositoryContext = reviewingCreatedOrResumed && !(overlayIdentitySatisfiesRepositoryContext && targetIdentity !== undefined);
|
|
1039
1123
|
if (requiresRepositoryContext && body.repository_context === undefined) throw new TypeError("start.repository_context is required when action is created/resumed and state is reviewing");
|
|
1040
|
-
if (!
|
|
1124
|
+
if (!reviewingCreatedOrResumed && body.repository_context !== undefined) throw new TypeError("start.repository_context is only valid when action is created/resumed and state is reviewing");
|
|
1041
1125
|
|
|
1042
1126
|
let repositoryContext: ReviewRepositoryContextV2 | undefined;
|
|
1043
1127
|
if (body.repository_context !== undefined) {
|
|
@@ -1045,7 +1129,7 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
|
1045
1129
|
if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
|
|
1046
1130
|
repositoryContext = {
|
|
1047
1131
|
capability: "review.opaque_repository_context",
|
|
1048
|
-
handle: text(source.handle, "start.repository_context.handle", { pattern: /^
|
|
1132
|
+
handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx[12]_[0-9a-f]{64}$/ }),
|
|
1049
1133
|
revision: sha256(source.revision, "start.repository_context.revision"),
|
|
1050
1134
|
targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
|
|
1051
1135
|
...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "start.repository_context.event_id") }),
|
|
@@ -1090,6 +1174,78 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
|
1090
1174
|
};
|
|
1091
1175
|
}
|
|
1092
1176
|
|
|
1177
|
+
function assertReviewStartV4StatusBinding(execute: ReviewNextTransitionExecuteV3, start: ReviewStartV3): void {
|
|
1178
|
+
const expectedTargetIdentity = start.repositoryContext?.targetIdentity ?? start.targetIdentity;
|
|
1179
|
+
const targetIdentityConflicts = expectedTargetIdentity === undefined || execute.binding.targetIdentity !== expectedTargetIdentity
|
|
1180
|
+
|| (start.targetIdentity !== undefined && start.targetIdentity !== expectedTargetIdentity)
|
|
1181
|
+
|| start.artifactSubjects.some((subject) => subject.targetIdentity !== expectedTargetIdentity);
|
|
1182
|
+
if (targetIdentityConflicts) throw new TypeError("START/v4 status target identity binding conflicts with frozen START");
|
|
1183
|
+
if (execute.binding.lineageId !== undefined && execute.binding.lineageId !== start.lineageId) {
|
|
1184
|
+
throw new TypeError("START/v4 status lineage binding conflicts with frozen START");
|
|
1185
|
+
}
|
|
1186
|
+
const optionalSelectors: Readonly<Record<string, string | undefined>> = {
|
|
1187
|
+
"base-ref": start.baseTree,
|
|
1188
|
+
"base-tree": start.baseTree,
|
|
1189
|
+
projection: start.projection,
|
|
1190
|
+
target: expectedTargetIdentity,
|
|
1191
|
+
};
|
|
1192
|
+
const argumentsByName = new Map<string, string>();
|
|
1193
|
+
for (const argument of execute.arguments) {
|
|
1194
|
+
if (argumentsByName.has(argument.name)) throw new TypeError(`START/v4 status arguments duplicate ${argument.name} binding`);
|
|
1195
|
+
argumentsByName.set(argument.name, argument.value);
|
|
1196
|
+
if (Object.hasOwn(optionalSelectors, argument.name) && argument.value !== optionalSelectors[argument.name]) throw new TypeError(`START/v4 status ${argument.name} binding conflicts with frozen START`);
|
|
1197
|
+
}
|
|
1198
|
+
if (argumentsByName.get("lineage") !== start.lineageId) throw new TypeError("START/v4 status requires exactly one lineage binding for the frozen START");
|
|
1199
|
+
if (execute.arguments.find((argument) => argument.name === "lineage")?.token !== `--lineage=${start.lineageId}`) throw new TypeError("START/v4 status lineage binding conflicts with frozen START");
|
|
1200
|
+
for (const [name, value] of [["contract", REVIEW_INTEGRATION_CONTRACT], ["next-transition", "true"], ["agent", "pi"]]) {
|
|
1201
|
+
if (argumentsByName.get(name) !== value || execute.arguments.find((argument) => argument.name === name)?.token !== `--${name}=${value}`) throw new TypeError(`START/v4 status ${name} binding conflicts with frozen START`);
|
|
1202
|
+
}
|
|
1203
|
+
const selectorArguments = execute.selectorArguments;
|
|
1204
|
+
if (selectorArguments === undefined || selectorArguments.length === 0) throw new TypeError("START/v4 status selector arguments are required");
|
|
1205
|
+
const selectorsByName = new Map<string, ReviewTransitionArgumentV3>();
|
|
1206
|
+
for (const selector of selectorArguments) {
|
|
1207
|
+
const full = execute.arguments.find((argument) => argument.name === selector.name);
|
|
1208
|
+
if (selectorsByName.has(selector.name) || full === undefined || full.value !== selector.value || full.token !== selector.token) throw new TypeError(`START/v4 status selector ${selector.name} binding conflicts with full arguments`);
|
|
1209
|
+
selectorsByName.set(selector.name, selector);
|
|
1210
|
+
}
|
|
1211
|
+
const selector = (name: string, value: string): void => {
|
|
1212
|
+
if (selectorsByName.get(name)?.value !== value || selectorsByName.get(name)?.token !== `--${name}=${value}` || execute.arguments.find((argument) => argument.name === name)?.token !== `--${name}=${value}`) throw new TypeError(`START/v4 status selector ${name} binding conflicts with frozen START`);
|
|
1213
|
+
};
|
|
1214
|
+
const baseRef = argumentsByName.get("base-ref");
|
|
1215
|
+
if (start.targetMode === "base-workspace-overlay") {
|
|
1216
|
+
if (baseRef !== start.baseTree || argumentsByName.has("committed-only")) throw new TypeError("START/v4 status workspace overlay binding conflicts with frozen START");
|
|
1217
|
+
selector("base-ref", start.baseTree!); selector("workspace-overlay", "true");
|
|
1218
|
+
} else if (baseRef !== undefined) {
|
|
1219
|
+
if (baseRef !== start.baseTree || argumentsByName.has("workspace-overlay")) throw new TypeError("START/v4 status committed range binding conflicts with frozen START");
|
|
1220
|
+
selector("base-ref", start.baseTree!); selector("committed-only", "true");
|
|
1221
|
+
} else {
|
|
1222
|
+
if (argumentsByName.has("committed-only") || argumentsByName.has("workspace-overlay")) throw new TypeError("START/v4 status current projection binding conflicts with frozen START");
|
|
1223
|
+
selector("projection", start.projection);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
export function decodeReviewStartV4(value: unknown): ReviewStartV4 {
|
|
1228
|
+
const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] as const;
|
|
1229
|
+
const body = exactRecord(value, "start", [
|
|
1230
|
+
"schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
|
|
1231
|
+
"selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
|
|
1232
|
+
], [...overlayFields, "changed_path_manifest", "repository_context", "acknowledgement", "next_transition"]);
|
|
1233
|
+
requireIdentity(body, "gentle-ai.review-integration.start/v4", REVIEW_INTEGRATION_OPERATION.START);
|
|
1234
|
+
const action = enumeration(body.action, ["created", "replayed", "closed", "blocked-scope-action"] as const, "start.action");
|
|
1235
|
+
const v3Action = action === "replayed" ? "resumed" : action;
|
|
1236
|
+
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
|
|
1237
|
+
const reviewing = (action === "created" || action === "replayed") && body.state === REVIEW_START_STATE.REVIEWING;
|
|
1238
|
+
if (reviewing && nextTransition?.kind !== "execute") throw new TypeError("reviewing created/replayed START requires next_transition.execute");
|
|
1239
|
+
if (reviewing && nextTransition.execute?.operation !== "review.status") throw new TypeError("reviewing created/replayed START next_transition.execute.operation must be review.status");
|
|
1240
|
+
const closedApprovedZeroLens = action === "closed" && body.state === REVIEW_START_STATE.APPROVED && Array.isArray(body.selected_lenses) && body.selected_lenses.length === 0;
|
|
1241
|
+
if (closedApprovedZeroLens && nextTransition !== undefined) throw new TypeError("closed approved zero-lens START cannot carry next_transition");
|
|
1242
|
+
const v3Body = { ...body };
|
|
1243
|
+
delete v3Body.next_transition;
|
|
1244
|
+
const decoded = decodeReviewStartV3({ ...v3Body, schema: "gentle-ai.review-integration.start/v3", action: v3Action }, true);
|
|
1245
|
+
if (reviewing) assertReviewStartV4StatusBinding(nextTransition!.execute!, decoded);
|
|
1246
|
+
return { ...decoded, action, ...(nextTransition === undefined ? {} : { nextTransition }), raw: body };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1093
1249
|
// ---------------------------------------------------------------------------
|
|
1094
1250
|
// projection/v1 — reused verbatim; the v2 capabilities schema still advertises
|
|
1095
1251
|
// gentle-ai.review-integration.projection/v1, so renaming this decoder would
|
|
@@ -1187,36 +1343,13 @@ export function decodeAuthorityRepairAssessmentV1(value: unknown): AuthorityRepa
|
|
|
1187
1343
|
};
|
|
1188
1344
|
}
|
|
1189
1345
|
|
|
1190
|
-
// ---------------------------------------------------------------------------
|
|
1191
|
-
// targeted-validation-request/v1 — reused for status.validation_request,
|
|
1192
|
-
// operation.result.validation_request, and next_transition collect inputs.
|
|
1193
|
-
// ---------------------------------------------------------------------------
|
|
1194
|
-
|
|
1195
|
-
function decodeTargetedValidationRequestV1(value: unknown, label: string): ReviewTargetedValidationRequestV1 {
|
|
1196
|
-
const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "fix_finding_ids", "projection", "correction_candidate_tree", "correction_target_identity", "correction_paths", "correction_paths_digest"]);
|
|
1197
|
-
if (body.schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
|
|
1198
|
-
return {
|
|
1199
|
-
schema: "gentle-ai.review-targeted-validation-request/v1",
|
|
1200
|
-
requestHash: sha256(body.request_hash, `${label}.request_hash`),
|
|
1201
|
-
lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
|
|
1202
|
-
expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
|
|
1203
|
-
targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
|
|
1204
|
-
fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
|
|
1205
|
-
projection: enumeration(body.projection, REQUIRED_PROJECTIONS, `${label}.projection`),
|
|
1206
|
-
correctionCandidateTree: gitTree(body.correction_candidate_tree, `${label}.correction_candidate_tree`),
|
|
1207
|
-
correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
|
|
1208
|
-
correctionPaths: stringArray(body.correction_paths, `${label}.correction_paths`, { minimum: 1, unique: true }),
|
|
1209
|
-
correctionPathsDigest: sha256(body.correction_paths_digest, `${label}.correction_paths_digest`),
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
1346
|
// ---------------------------------------------------------------------------
|
|
1214
1347
|
// next-transition/v3 — net-new; unlike v1's decodeNextTransition (void),
|
|
1215
1348
|
// this decoder returns a typed value so callers can read the manifest-bound
|
|
1216
1349
|
// collect inputs and the execute binding.
|
|
1217
1350
|
// ---------------------------------------------------------------------------
|
|
1218
1351
|
|
|
1219
|
-
const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.
|
|
1352
|
+
const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.status", "review.recover", "review.repair", "review.acknowledge-approved"] as const;
|
|
1220
1353
|
|
|
1221
1354
|
function decodeTransitionArguments(value: unknown, label: string): readonly ReviewTransitionArgumentV3[] {
|
|
1222
1355
|
return array(value, label, (entry, entryLabel) => {
|
|
@@ -1228,7 +1361,29 @@ function decodeTransitionArguments(value: unknown, label: string): readonly Revi
|
|
|
1228
1361
|
});
|
|
1229
1362
|
}
|
|
1230
1363
|
|
|
1231
|
-
function decodeCaptureSubmission(value: unknown, label: string, v5: boolean): ReviewCaptureSubmissionV1 {
|
|
1364
|
+
function decodeCaptureSubmission(value: unknown, label: string, v5: boolean, v6: boolean, captureOperation: string): ReviewCaptureSubmissionV1 {
|
|
1365
|
+
// status/v6 adds the provider-owned intended-untracked selection form.
|
|
1366
|
+
if (v6 && captureOperation === "external.select_intended_untracked" && typeof value === "object" && value !== null && (value as Record<string, unknown>).operation_token === "status") {
|
|
1367
|
+
const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
|
|
1368
|
+
const expectedTokens = [
|
|
1369
|
+
"--contract=gentle-ai.review-integration/v2", "--next-transition=true", "--agent=pi",
|
|
1370
|
+
"--projection=workspace", "--intended-untracked-selection={{value}}",
|
|
1371
|
+
] as const;
|
|
1372
|
+
const operationToken = enumeration(submission.operation_token, ["status"] as const, `${label}.operation_token`);
|
|
1373
|
+
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: expectedTokens.length, maximum: expectedTokens.length });
|
|
1374
|
+
if (argumentTokens.some((token, index) => token !== expectedTokens[index])) throw new TypeError(`${label}.argument_tokens substitution binding is invalid`);
|
|
1375
|
+
const row = exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"]);
|
|
1376
|
+
return {
|
|
1377
|
+
operationToken,
|
|
1378
|
+
argumentTokens,
|
|
1379
|
+
values: [{
|
|
1380
|
+
slot: enumeration(row.slot, ["intended_untracked_selection"] as const, `${label}.value.slot`),
|
|
1381
|
+
domain: enumeration(row.domain, ["schema_bound_json"] as const, `${label}.value.domain`),
|
|
1382
|
+
schema: enumeration(row.schema, ["gentle-ai.review-intended-untracked-selection/v1"] as const, `${label}.value.schema`),
|
|
1383
|
+
substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 4, 4),
|
|
1384
|
+
}],
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1232
1387
|
// status/v5: the live 2.4.0-main binary emits the materialize
|
|
1233
1388
|
// capture-result submission as a SINGULAR `value` object carrying a
|
|
1234
1389
|
// `schema` key (captured 2026-08-16 from 2.4.0-main.b1afef46; the
|
|
@@ -1239,16 +1394,22 @@ function decodeCaptureSubmission(value: unknown, label: string, v5: boolean): Re
|
|
|
1239
1394
|
// through to the legacy decoder, which rejects the unknown `value` key.
|
|
1240
1395
|
if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
|
|
1241
1396
|
const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
|
|
1242
|
-
|
|
1397
|
+
const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan"] as const, `${label}.operation_token`);
|
|
1243
1398
|
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
|
|
1244
|
-
const row =
|
|
1399
|
+
const row = operationToken === "capture-result"
|
|
1400
|
+
? exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"])
|
|
1401
|
+
: exactRecord(submission.value, `${label}.value`, ["slot", "domain", "minimum", "maximum", "substitution_location"]);
|
|
1245
1402
|
return {
|
|
1246
|
-
operationToken
|
|
1403
|
+
operationToken,
|
|
1247
1404
|
argumentTokens,
|
|
1248
1405
|
values: [{
|
|
1249
|
-
slot:
|
|
1406
|
+
slot: operationToken === "capture-result"
|
|
1407
|
+
? enumeration(row.slot, ["reviewer_result"] as const, `${label}.value.slot`)
|
|
1408
|
+
: enumeration(row.slot, ["correction_lines"] as const, `${label}.value.slot`),
|
|
1250
1409
|
domain: nonempty(row.domain, `${label}.value.domain`),
|
|
1251
|
-
schema: nonempty(row.schema, `${label}.value.schema`),
|
|
1410
|
+
...(row.schema === undefined ? {} : { schema: nonempty(row.schema, `${label}.value.schema`) }),
|
|
1411
|
+
...(row.minimum === undefined ? {} : { minimum: integer(row.minimum, `${label}.value.minimum`, 1, 200) }),
|
|
1412
|
+
...(row.maximum === undefined ? {} : { maximum: integer(row.maximum, `${label}.value.maximum`, 1, 200) }),
|
|
1252
1413
|
substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
|
|
1253
1414
|
}],
|
|
1254
1415
|
};
|
|
@@ -1279,40 +1440,6 @@ function decodeProviderTask(value: unknown, label: string): ReviewProviderTaskV1
|
|
|
1279
1440
|
};
|
|
1280
1441
|
}
|
|
1281
1442
|
|
|
1282
|
-
function decodeSubmissionDescriptorV5(value: unknown, label: string, operationToken: "finalize" | "capture-evidence"): ReviewSubmissionDescriptorV5 {
|
|
1283
|
-
if (operationToken === "finalize") {
|
|
1284
|
-
const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
|
|
1285
|
-
const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 7 });
|
|
1286
|
-
const source = exactRecord(descriptor.value, `${label}.value`, ["slot", "domain", "substitution_location"], ["schema", "minimum", "maximum"]);
|
|
1287
|
-
const slot = enumeration(source.slot, ["correction_lines", "validation"] as const, `${label}.value.slot`);
|
|
1288
|
-
return {
|
|
1289
|
-
operationToken,
|
|
1290
|
-
argumentTokens,
|
|
1291
|
-
value: {
|
|
1292
|
-
slot,
|
|
1293
|
-
domain: nonempty(source.domain, `${label}.value.domain`),
|
|
1294
|
-
substitutionLocation: integer(source.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
|
|
1295
|
-
...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${label}.value.schema`) }),
|
|
1296
|
-
...(source.minimum === undefined ? {} : { minimum: integer(source.minimum, `${label}.value.minimum`, 1, 200) }),
|
|
1297
|
-
...(source.maximum === undefined ? {} : { maximum: integer(source.maximum, `${label}.value.maximum`, 1, 200) }),
|
|
1298
|
-
},
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
|
|
1302
|
-
const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 6, maximum: 6 });
|
|
1303
|
-
const values = array(descriptor.values, `${label}.values`, (entry, entryLabel) => {
|
|
1304
|
-
const source = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"], ["schema", "allowed_values"]);
|
|
1305
|
-
return {
|
|
1306
|
-
slot: nonempty(source.slot, `${entryLabel}.slot`),
|
|
1307
|
-
domain: nonempty(source.domain, `${entryLabel}.domain`),
|
|
1308
|
-
substitutionLocation: integer(source.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
|
|
1309
|
-
...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${entryLabel}.schema`) }),
|
|
1310
|
-
...(source.allowed_values === undefined ? {} : { allowedValues: stringArray(source.allowed_values, `${entryLabel}.allowed_values`, { minimum: 1, unique: true }) }),
|
|
1311
|
-
};
|
|
1312
|
-
}, { minimum: 2, maximum: 2 });
|
|
1313
|
-
return { operationToken, argumentTokens, values };
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
1443
|
function decodeCorrectionPlanRequestV1(value: unknown, label: string): ReviewCorrectionPlanRequestV1 {
|
|
1317
1444
|
const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "correction_budget", "fix_finding_ids", "findings"]);
|
|
1318
1445
|
if (body.schema !== "gentle-ai.review-correction-plan-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan-request/v1`);
|
|
@@ -1341,6 +1468,59 @@ function decodeCorrectionPlanRequestV1(value: unknown, label: string): ReviewCor
|
|
|
1341
1468
|
};
|
|
1342
1469
|
}
|
|
1343
1470
|
|
|
1471
|
+
export function decodeReviewTargetedValidationRequestV1(value: unknown, label = "targeted_validation_request"): ReviewTargetedValidationRequestV1 {
|
|
1472
|
+
const body = exactRecord(value, label, [
|
|
1473
|
+
"schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "fix_finding_ids", "policy_content", "fix_findings", "fix_classifications",
|
|
1474
|
+
"projection", "correction_candidate_tree", "correction_target_identity", "correction_paths", "correction_paths_digest",
|
|
1475
|
+
]);
|
|
1476
|
+
if (body.schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
|
|
1477
|
+
const fixFindingIds = stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true });
|
|
1478
|
+
const fixFindings = array(body.fix_findings, `${label}.fix_findings`, (entry, entryLabel): ReviewTargetedValidationFindingV1 => {
|
|
1479
|
+
const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence_class", "causal_disposition"]);
|
|
1480
|
+
return {
|
|
1481
|
+
id: nonempty(finding.id, `${entryLabel}.id`),
|
|
1482
|
+
lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] as const, `${entryLabel}.lens`),
|
|
1483
|
+
location: nonempty(finding.location, `${entryLabel}.location`),
|
|
1484
|
+
severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] as const, `${entryLabel}.severity`),
|
|
1485
|
+
claim: nonempty(finding.claim, `${entryLabel}.claim`),
|
|
1486
|
+
proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
|
|
1487
|
+
evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] as const, `${entryLabel}.evidence_class`),
|
|
1488
|
+
causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
|
|
1489
|
+
};
|
|
1490
|
+
}, { minimum: 1 });
|
|
1491
|
+
const fixClassifications = array(body.fix_classifications, `${label}.fix_classifications`, (entry, entryLabel): ReviewTargetedValidationClassificationV1 => {
|
|
1492
|
+
const classification = exactRecord(entry, entryLabel, ["finding_id", "class", "causal_disposition", "proof"], ["severity"]);
|
|
1493
|
+
const severity = classification.severity === undefined ? undefined : nonempty(classification.severity, `${entryLabel}.severity`);
|
|
1494
|
+
return {
|
|
1495
|
+
findingId: nonempty(classification.finding_id, `${entryLabel}.finding_id`),
|
|
1496
|
+
...(severity === undefined ? {} : { severity }),
|
|
1497
|
+
class: enumeration(classification.class, ["deterministic", "inferential"] as const, `${entryLabel}.class`),
|
|
1498
|
+
causalDisposition: enumeration(classification.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
|
|
1499
|
+
proof: nonempty(classification.proof, `${entryLabel}.proof`),
|
|
1500
|
+
};
|
|
1501
|
+
}, { minimum: 1 });
|
|
1502
|
+
const findingIds = fixFindings.map((finding) => finding.id);
|
|
1503
|
+
const classificationFindingIds = fixClassifications.map((classification) => classification.findingId);
|
|
1504
|
+
assertExactSet(findingIds, fixFindingIds, `${label}.fix_findings`);
|
|
1505
|
+
assertExactSet(classificationFindingIds, fixFindingIds, `${label}.fix_classifications`);
|
|
1506
|
+
return {
|
|
1507
|
+
schema: "gentle-ai.review-targeted-validation-request/v1",
|
|
1508
|
+
requestHash: sha256(body.request_hash, `${label}.request_hash`),
|
|
1509
|
+
lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
|
|
1510
|
+
expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
|
|
1511
|
+
targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
|
|
1512
|
+
fixFindingIds,
|
|
1513
|
+
policyContent: nonempty(body.policy_content, `${label}.policy_content`),
|
|
1514
|
+
fixFindings,
|
|
1515
|
+
fixClassifications,
|
|
1516
|
+
projection: enumeration(body.projection, REQUIRED_PROJECTIONS, `${label}.projection`),
|
|
1517
|
+
correctionCandidateTree: gitTree(body.correction_candidate_tree, `${label}.correction_candidate_tree`),
|
|
1518
|
+
correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
|
|
1519
|
+
correctionPaths: stringArray(body.correction_paths, `${label}.correction_paths`, { minimum: 1, unique: true }),
|
|
1520
|
+
correctionPathsDigest: sha256(body.correction_paths_digest, `${label}.correction_paths_digest`),
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1344
1524
|
export function decodeReviewForecastV1(value: unknown, label = "status.forecast"): ReviewForecastV1 {
|
|
1345
1525
|
const body = exactRecord(value, label, ["horizon", "steps"]);
|
|
1346
1526
|
const horizon = enumeration(body.horizon, ["partial", "terminal"] as const, `${label}.horizon`);
|
|
@@ -1364,24 +1544,15 @@ export function decodeReviewForecastV1(value: unknown, label = "status.forecast"
|
|
|
1364
1544
|
// request; every other reason code must not.
|
|
1365
1545
|
const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required", "corrected_candidate_unavailable"] as const);
|
|
1366
1546
|
// v5 capture operations that must carry a submission descriptor.
|
|
1367
|
-
const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["
|
|
1547
|
+
const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["review.capture-correction-plan"] as const);
|
|
1368
1548
|
|
|
1369
|
-
function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewCollectInputV3 {
|
|
1370
|
-
const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "
|
|
1549
|
+
function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: boolean): ReviewCollectInputV3 {
|
|
1550
|
+
const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "submission", ...(v5 ? ["provider_task", "validation_request"] : [])]);
|
|
1371
1551
|
const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1372
1552
|
const schema = nonempty(input.schema, `${label}.schema`);
|
|
1373
1553
|
const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
|
|
1374
1554
|
const argumentsList = decodeTransitionArguments(input.arguments, `${label}.arguments`);
|
|
1375
1555
|
|
|
1376
|
-
if (captureOperation === "external.run_targeted_validation") {
|
|
1377
|
-
if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
|
|
1378
|
-
if (schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
|
|
1379
|
-
} else if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION) {
|
|
1380
|
-
if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
|
|
1381
|
-
} else if (input.validation_request !== undefined) {
|
|
1382
|
-
throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation or review.capture-validation`);
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
1556
|
// v5-only surfaces (vendored status-v5.schema.json): the provider role task
|
|
1386
1557
|
// rides exactly the external.run_provider_role vector, and the finalize/
|
|
1387
1558
|
// capture-evidence submission descriptor forms ride their own vectors.
|
|
@@ -1393,11 +1564,39 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
|
|
|
1393
1564
|
}
|
|
1394
1565
|
if ((V5_SUBMISSION_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
|
|
1395
1566
|
if (input.submission === undefined) throw new TypeError(`${label}.submission is required for ${captureOperation}`);
|
|
1396
|
-
if (captureOperation === "
|
|
1397
|
-
if (captureOperation === "review.capture-evidence" && schema !== "https://gentle-ai.dev/schema/review/verification-evidence/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/verification-evidence/v1`);
|
|
1567
|
+
if (captureOperation === "review.capture-correction-plan" && schema !== "gentle-ai.review-correction-plan/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan/v1`);
|
|
1398
1568
|
}
|
|
1399
1569
|
}
|
|
1400
1570
|
|
|
1571
|
+
const intendedUntracked = v6
|
|
1572
|
+
&& name === "intended_untracked_selection"
|
|
1573
|
+
&& schema === "gentle-ai.review-intended-untracked-selection/v1"
|
|
1574
|
+
&& captureOperation === "external.select_intended_untracked";
|
|
1575
|
+
if (captureOperation === "external.select_intended_untracked" && (name !== "intended_untracked_selection" || schema !== "gentle-ai.review-intended-untracked-selection/v1")) {
|
|
1576
|
+
throw new TypeError(`${label} external.select_intended_untracked requires the intended_untracked_selection binding`);
|
|
1577
|
+
}
|
|
1578
|
+
if (intendedUntracked) {
|
|
1579
|
+
const expectedNames = ["target_identity", "projection", "base_tree", "candidate_tree", "eligible_paths_json", "expected_untracked_inventory"] as const;
|
|
1580
|
+
if (argumentsList.length !== expectedNames.length || argumentsList.some((argument, index) => argument.name !== expectedNames[index] || argument.token !== undefined)) {
|
|
1581
|
+
throw new TypeError(`${label}.arguments must preserve the exact intended-untracked metadata binding`);
|
|
1582
|
+
}
|
|
1583
|
+
sha256(argumentsList[0]!.value, `${label}.arguments[0].value`);
|
|
1584
|
+
enumeration(argumentsList[1]!.value, ["workspace"] as const, `${label}.arguments[1].value`);
|
|
1585
|
+
gitTree(argumentsList[2]!.value, `${label}.arguments[2].value`);
|
|
1586
|
+
gitTree(argumentsList[3]!.value, `${label}.arguments[3].value`);
|
|
1587
|
+
const eligiblePathsJson = argumentsList[4]!.value;
|
|
1588
|
+
let eligiblePaths: unknown;
|
|
1589
|
+
try { eligiblePaths = JSON.parse(eligiblePathsJson); } catch { throw new TypeError(`${label}.arguments[4].value must be canonical JSON`); }
|
|
1590
|
+
if (!Array.isArray(eligiblePaths) || eligiblePaths.length === 0 || eligiblePaths.some((path) => {
|
|
1591
|
+
if (typeof path !== "string" || path.length === 0 || path.startsWith("/") || /^[A-Za-z]:\//.test(path) || path.includes("\\") || path.includes("\0")) return true;
|
|
1592
|
+
const segments = path.split("/");
|
|
1593
|
+
return segments.some((segment) => segment === "" || segment === "." || segment === "..");
|
|
1594
|
+
}) || new Set(eligiblePaths).size !== eligiblePaths.length || JSON.stringify(eligiblePaths) !== eligiblePathsJson) {
|
|
1595
|
+
throw new TypeError(`${label}.arguments[4].value must contain unique canonical repository-relative paths`);
|
|
1596
|
+
}
|
|
1597
|
+
sha256(argumentsList[5]!.value, `${label}.arguments[5].value`);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1401
1600
|
// gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
|
|
1402
1601
|
// operations render SELF-CONTAINED authority-advancing vectors. Executing
|
|
1403
1602
|
// the exact rendered tokens makes Go materialize the role prompt, run its
|
|
@@ -1411,6 +1610,30 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
|
|
|
1411
1610
|
if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION && schema !== "https://gentle-ai.dev/schema/review/validator/v1") {
|
|
1412
1611
|
throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/validator/v1`);
|
|
1413
1612
|
}
|
|
1613
|
+
const targetedValidatorInput = v5
|
|
1614
|
+
&& name === "provider_targeted_validator"
|
|
1615
|
+
&& captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION
|
|
1616
|
+
&& schema === "https://gentle-ai.dev/schema/review/validator/v1";
|
|
1617
|
+
if (input.validation_request !== undefined && !targetedValidatorInput) {
|
|
1618
|
+
throw new TypeError(`${label}.validation_request is only valid for the v5 provider-targeted-validator capture input`);
|
|
1619
|
+
}
|
|
1620
|
+
if (targetedValidatorInput && input.validation_request === undefined) {
|
|
1621
|
+
throw new TypeError(`${label}.validation_request is required for the v5 provider-targeted-validator capture input`);
|
|
1622
|
+
}
|
|
1623
|
+
const validationRequest = input.validation_request === undefined
|
|
1624
|
+
? undefined
|
|
1625
|
+
: decodeReviewTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`);
|
|
1626
|
+
if (validationRequest !== undefined) {
|
|
1627
|
+
const providerArgument = (argumentName: string): string => {
|
|
1628
|
+
const matches = argumentsList.filter((argument) => argument.name === argumentName);
|
|
1629
|
+
if (matches.length !== 1) throw new TypeError(`${label}.arguments must carry exactly one ${argumentName} binding`);
|
|
1630
|
+
return matches[0]!.value;
|
|
1631
|
+
};
|
|
1632
|
+
if (providerArgument("lineage") !== validationRequest.lineageId) throw new TypeError(`${label}.arguments lineage must bind validation_request.lineage_id`);
|
|
1633
|
+
if (providerArgument("expected-revision") !== validationRequest.expectedRevision) throw new TypeError(`${label}.arguments expected-revision must bind validation_request.expected_revision`);
|
|
1634
|
+
if (providerArgument("target") !== validationRequest.correctionTargetIdentity) throw new TypeError(`${label}.arguments target must bind validation_request.correction_target_identity`);
|
|
1635
|
+
if (providerArgument("request-hash") !== validationRequest.requestHash) throw new TypeError(`${label}.arguments request-hash must bind validation_request.request_hash`);
|
|
1636
|
+
}
|
|
1414
1637
|
if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
|
|
1415
1638
|
throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
|
|
1416
1639
|
}
|
|
@@ -1423,23 +1646,17 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
|
|
|
1423
1646
|
} else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
|
|
1424
1647
|
throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
|
|
1425
1648
|
}
|
|
1426
|
-
const submissionOperations: readonly string[] = v5
|
|
1649
|
+
const submissionOperations: readonly string[] = v5
|
|
1650
|
+
? ["review.capture-result", "review.capture-correction-plan", ...(v6 ? ["external.select_intended_untracked"] : [])]
|
|
1651
|
+
: ["review.capture-result"];
|
|
1427
1652
|
if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
|
|
1428
1653
|
throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
|
|
1429
1654
|
}
|
|
1655
|
+
if (intendedUntracked && input.submission === undefined) throw new TypeError(`${label}.submission is required for external.select_intended_untracked`);
|
|
1430
1656
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
let submissionDescriptor: ReviewSubmissionDescriptorV5 | undefined;
|
|
1435
|
-
if (input.submission !== undefined) {
|
|
1436
|
-
const operationToken = (input.submission as Record<string, unknown> | null)?.operation_token;
|
|
1437
|
-
if (v5 && (operationToken === "finalize" || operationToken === "capture-evidence")) {
|
|
1438
|
-
submissionDescriptor = decodeSubmissionDescriptorV5(input.submission, `${label}.submission`, operationToken);
|
|
1439
|
-
} else {
|
|
1440
|
-
submission = decodeCaptureSubmission(input.submission, `${label}.submission`, v5);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1657
|
+
const submission = input.submission === undefined
|
|
1658
|
+
? undefined
|
|
1659
|
+
: decodeCaptureSubmission(input.submission, `${label}.submission`, v5, v6, captureOperation);
|
|
1443
1660
|
|
|
1444
1661
|
return {
|
|
1445
1662
|
name,
|
|
@@ -1450,15 +1667,15 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
|
|
|
1450
1667
|
...(input.base_tree === undefined ? {} : { baseTree: gitTree(input.base_tree, `${label}.base_tree`) }),
|
|
1451
1668
|
...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
|
|
1452
1669
|
...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
|
|
1453
|
-
...(input.validation_request === undefined ? {} : { validationRequest: decodeTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`) }),
|
|
1454
1670
|
...(submission === undefined ? {} : { submission }),
|
|
1455
|
-
...(submissionDescriptor === undefined ? {} : { submissionDescriptor }),
|
|
1456
1671
|
...(v5 && input.provider_task !== undefined ? { providerTask: decodeProviderTask(input.provider_task, `${label}.provider_task`) } : {}),
|
|
1672
|
+
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
1457
1673
|
};
|
|
1458
1674
|
}
|
|
1459
1675
|
|
|
1460
|
-
export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean } = {}): ReviewNextTransitionV3 {
|
|
1461
|
-
const
|
|
1676
|
+
export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean; v6?: boolean } = {}): ReviewNextTransitionV3 {
|
|
1677
|
+
const v6 = options.v6 === true;
|
|
1678
|
+
const v5 = options.v5 === true || v6;
|
|
1462
1679
|
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
|
|
1463
1680
|
const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
|
|
1464
1681
|
const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
@@ -1488,6 +1705,7 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
|
|
|
1488
1705
|
const execute = exactRecord(transition.execute, "next_transition.execute", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
|
|
1489
1706
|
const operation = enumeration(execute.operation, NEXT_TRANSITION_OPERATIONS, "next_transition.execute.operation");
|
|
1490
1707
|
const argumentsList = decodeTransitionArguments(execute.arguments, "next_transition.execute.arguments");
|
|
1708
|
+
const selectorArguments = execute.selector_arguments === undefined ? undefined : decodeTransitionArguments(execute.selector_arguments, "next_transition.execute.selector_arguments");
|
|
1491
1709
|
const preconditions = decodeTransitionArguments(execute.preconditions, "next_transition.execute.preconditions");
|
|
1492
1710
|
// The schema gives `binding` no declared properties and no required list:
|
|
1493
1711
|
// it is an OPEN object. Closing it here made Pi stricter than the
|
|
@@ -1498,12 +1716,14 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
|
|
|
1498
1716
|
const lineageId = binding.lineage_id === undefined ? undefined : lineage(binding.lineage_id, "next_transition.execute.binding.lineage_id");
|
|
1499
1717
|
const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
|
|
1500
1718
|
const command = execute.command === undefined ? undefined : nonempty(execute.command, "next_transition.execute.command");
|
|
1719
|
+
const decodedExecute: ReviewNextTransitionExecuteV3 = { operation, arguments: argumentsList, ...(selectorArguments === undefined ? {} : { selectorArguments }), preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) };
|
|
1720
|
+
if (operation === "review.acknowledge-approved") assertReviewApprovedAcknowledgementExecuteV1(decodedExecute);
|
|
1501
1721
|
if (transition.collect !== undefined) throw new TypeError("next_transition.collect is incompatible with execute");
|
|
1502
|
-
return { kind, reasonCode, execute:
|
|
1722
|
+
return { kind, reasonCode, execute: decodedExecute, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1503
1723
|
}
|
|
1504
1724
|
if (kind === "collect") {
|
|
1505
1725
|
const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
|
|
1506
|
-
const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5), { minimum: 1 });
|
|
1726
|
+
const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6), { minimum: 1 });
|
|
1507
1727
|
if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
|
|
1508
1728
|
return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1509
1729
|
}
|
|
@@ -1519,8 +1739,8 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
|
|
|
1519
1739
|
// status-v2.schema.json action_eligibility definition v2 also $refs.
|
|
1520
1740
|
// ---------------------------------------------------------------------------
|
|
1521
1741
|
|
|
1522
|
-
const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.
|
|
1523
|
-
const FORBIDDEN_ACTIONS = ["review.abandon", "review.
|
|
1742
|
+
const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.recover", "review.repair"] as const;
|
|
1743
|
+
const FORBIDDEN_ACTIONS = ["review.abandon", "review.invalidate", "review.quarantine-legacy", "review.reclaim", "review.reconcile-authority", "review.reconcile-authority-batch", "review.recover", "review.repair", "review.start"] as const;
|
|
1524
1744
|
|
|
1525
1745
|
function decodeEligibility(value: unknown, label: string): void {
|
|
1526
1746
|
const eligibility = exactRecord(value, label, ["allowed_actions", "forbidden_actions"]);
|
|
@@ -1529,14 +1749,14 @@ function decodeEligibility(value: unknown, label: string): void {
|
|
|
1529
1749
|
const selected = enumeration(action.action, ELIGIBLE_ACTIONS, `${entryLabel}.action`);
|
|
1530
1750
|
text(action.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1531
1751
|
array(action.required_inputs, `${entryLabel}.required_inputs`, (input, inputLabel) => text(input, inputLabel, { minimum: 1, pattern: /^[a-z0-9_]+$/ }), { unique: true });
|
|
1532
|
-
if (selected === "review.recover"
|
|
1752
|
+
if (selected === "review.recover") {
|
|
1533
1753
|
const binding = exactRecord(action.binding, `${entryLabel}.binding`, ["lineage_id", "revision", "target_identity"]);
|
|
1534
1754
|
enumeration(action.disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), `${entryLabel}.disposition`);
|
|
1535
1755
|
lineage(binding.lineage_id, `${entryLabel}.binding.lineage_id`);
|
|
1536
1756
|
sha256(binding.revision, `${entryLabel}.binding.revision`);
|
|
1537
1757
|
sha256(binding.target_identity, `${entryLabel}.binding.target_identity`);
|
|
1538
1758
|
} else if (action.disposition !== undefined || action.binding !== undefined) {
|
|
1539
|
-
throw new TypeError(`${entryLabel} recovery fields require review.recover
|
|
1759
|
+
throw new TypeError(`${entryLabel} recovery fields require review.recover`);
|
|
1540
1760
|
}
|
|
1541
1761
|
return action;
|
|
1542
1762
|
}, { minimum: 1, maximum: 1 });
|
|
@@ -1548,21 +1768,6 @@ function decodeEligibility(value: unknown, label: string): void {
|
|
|
1548
1768
|
});
|
|
1549
1769
|
}
|
|
1550
1770
|
|
|
1551
|
-
function decodeFinalVerificationRetry(value: unknown, label: string): ReviewFinalVerificationRetryV1 {
|
|
1552
|
-
const body = exactRecord(value, label, ["incident_schema", "incident_class", "validating_revision", "target_identity", "failed_evidence_hash", "finalize_request_digest"], ["failed_evidence_record_digest"]);
|
|
1553
|
-
if (body.incident_schema !== "gentle-ai.review-final-verification-incident/v1") throw new TypeError(`${label}.incident_schema is unsupported`);
|
|
1554
|
-
if (body.incident_class !== "procedural_tooling_failure") throw new TypeError(`${label}.incident_class is unsupported`);
|
|
1555
|
-
return {
|
|
1556
|
-
incidentSchema: "gentle-ai.review-final-verification-incident/v1",
|
|
1557
|
-
incidentClass: "procedural_tooling_failure",
|
|
1558
|
-
validatingRevision: sha256(body.validating_revision, `${label}.validating_revision`),
|
|
1559
|
-
targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
|
|
1560
|
-
failedEvidenceHash: sha256(body.failed_evidence_hash, `${label}.failed_evidence_hash`),
|
|
1561
|
-
...(body.failed_evidence_record_digest === undefined ? {} : { failedEvidenceRecordDigest: sha256(body.failed_evidence_record_digest, `${label}.failed_evidence_record_digest`) }),
|
|
1562
|
-
finalizeRequestDigest: sha256(body.finalize_request_digest, `${label}.finalize_request_digest`),
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
1771
|
// ---------------------------------------------------------------------------
|
|
1567
1772
|
// status/v3
|
|
1568
1773
|
// ---------------------------------------------------------------------------
|
|
@@ -1571,18 +1776,29 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1571
1776
|
// Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
|
|
1572
1777
|
// against a live capture and the vendored status-v5.schema.json) is the v3
|
|
1573
1778
|
// key set plus the optional forecast and the v5-only next_transition
|
|
1574
|
-
// surfaces.
|
|
1575
|
-
|
|
1779
|
+
// surfaces. status/v6 adds the intended-untracked selection; status/v7
|
|
1780
|
+
// (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
|
|
1781
|
+
// adds only the top-level optional `eligible_untracked_inventory` digest,
|
|
1782
|
+
// so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
|
|
1783
|
+
// field.
|
|
1784
|
+
const schema = typeof value === "object" && value !== null ? (value as Record<string, unknown>).schema : undefined;
|
|
1785
|
+
const v7 = schema === "gentle-ai.review-integration.status/v7";
|
|
1786
|
+
const v6 = v7 || schema === "gentle-ai.review-integration.status/v6";
|
|
1787
|
+
const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
|
|
1576
1788
|
const body = exactRecord(value, "status", [
|
|
1577
|
-
"schema", "contract", "operation", "applicability", "
|
|
1578
|
-
], ["authority", "frozen", "
|
|
1579
|
-
requireIdentity(body, v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1789
|
+
"schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
|
|
1790
|
+
], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory"] : [])]);
|
|
1791
|
+
requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1580
1792
|
|
|
1581
1793
|
const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1794
|
+
let receipt: ReviewStatusReceiptV1 | undefined;
|
|
1795
|
+
if (body.receipt !== undefined) {
|
|
1796
|
+
const source = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
|
|
1797
|
+
receipt = {
|
|
1798
|
+
status: enumeration(source.status, RECEIPT_STATUSES, "status.receipt.status"),
|
|
1799
|
+
...(source.identity === undefined ? {} : { identity: sha256(source.identity, "status.receipt.identity") }),
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1586
1802
|
let authority: ReviewStatusAuthorityV1 | undefined;
|
|
1587
1803
|
if (body.authority !== undefined) {
|
|
1588
1804
|
const source = exactRecord(body.authority, "status.authority", ["version", "lineage_id", "state", "generation", "revision"]);
|
|
@@ -1610,52 +1826,32 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1610
1826
|
}
|
|
1611
1827
|
if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
|
|
1612
1828
|
if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && (frozen !== undefined || body.authority_target_identity !== undefined)) throw new TypeError("legacy status cannot expose frozen metadata or authority_target_identity");
|
|
1613
|
-
if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 &&
|
|
1829
|
+
if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && receipt !== undefined && receipt.status !== "expected_missing" && receipt.status !== "present") {
|
|
1830
|
+
throw new TypeError("legacy status receipt is incompatible");
|
|
1831
|
+
}
|
|
1614
1832
|
|
|
1615
1833
|
const action = enumeration(body.action, STATUS_ACTIONS, "status.action");
|
|
1616
1834
|
const actionDisposition = body.action_disposition === undefined ? undefined : enumeration(body.action_disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), "status.action_disposition");
|
|
1617
|
-
if (
|
|
1618
|
-
if (action !== "recover" &&
|
|
1835
|
+
if (action === "recover" && actionDisposition === undefined) throw new TypeError("recover status requires action_disposition");
|
|
1836
|
+
if (action !== "recover" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover action");
|
|
1619
1837
|
if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
|
|
1620
|
-
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5 });
|
|
1838
|
+
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6 });
|
|
1839
|
+
const validationRequest = v5 && body.validation_request !== undefined
|
|
1840
|
+
? decodeReviewTargetedValidationRequestV1(body.validation_request, "status.validation_request")
|
|
1841
|
+
: undefined;
|
|
1842
|
+
if (validationRequest !== undefined) {
|
|
1843
|
+
const collectInputs = nextTransition?.kind === "collect" ? nextTransition.collect?.inputs ?? [] : [];
|
|
1844
|
+
const matchingInputs = collectInputs.filter((input) => input.validationRequest !== undefined);
|
|
1845
|
+
if (matchingInputs.length !== 1 || canonicalJson(matchingInputs[0]!.validationRequest) !== canonicalJson(validationRequest)) {
|
|
1846
|
+
throw new TypeError("status.validation_request must exactly match the provider-targeted-validator collect input");
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1621
1849
|
// status/v5 dependentRequired: a forecast previews the transition head, so
|
|
1622
1850
|
// it can only accompany an actual next_transition.
|
|
1623
1851
|
const forecast = v5 && body.forecast !== undefined ? decodeReviewForecastV1(body.forecast) : undefined;
|
|
1624
1852
|
if (forecast !== undefined && nextTransition === undefined) throw new TypeError("status.forecast requires status.next_transition");
|
|
1625
1853
|
const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
|
|
1626
1854
|
|
|
1627
|
-
let reconciliation: ReviewStatusReconciliationV1 | undefined;
|
|
1628
|
-
if (action === "reconcile_finalize") {
|
|
1629
|
-
if (body.reconciliation === undefined) throw new TypeError("reconcile_finalize status requires reconciliation");
|
|
1630
|
-
const source = exactRecord(body.reconciliation, "status.reconciliation", ["required"]);
|
|
1631
|
-
if (source.required !== true) throw new TypeError("status.reconciliation.required must be true");
|
|
1632
|
-
if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET) throw new TypeError("reconcile_finalize status requires current_target applicability");
|
|
1633
|
-
if (replayability !== REVIEW_REPLAYABILITY.STATUS_REQUIRED) throw new TypeError("reconcile_finalize status requires status_required replayability");
|
|
1634
|
-
reconciliation = { required: true };
|
|
1635
|
-
} else if (body.reconciliation !== undefined) {
|
|
1636
|
-
throw new TypeError("status.reconciliation is only valid for the reconcile_finalize action");
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
let validationRequest: ReviewTargetedValidationRequestV1 | undefined;
|
|
1640
|
-
if (body.validation_request !== undefined) {
|
|
1641
|
-
if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.state !== "correction_required") {
|
|
1642
|
-
throw new TypeError("status.validation_request requires current_target applicability and correction_required authority state");
|
|
1643
|
-
}
|
|
1644
|
-
validationRequest = decodeTargetedValidationRequestV1(body.validation_request, "status.validation_request");
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
let finalVerificationRetry: ReviewFinalVerificationRetryV1 | undefined;
|
|
1648
|
-
if (action === "retry_final_verification") {
|
|
1649
|
-
if (body.final_verification_retry === undefined) throw new TypeError("retry_final_verification status requires final_verification_retry");
|
|
1650
|
-
if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.version !== REVIEW_AUTHORITY_VERSION.COMPACT_V2 || authority?.state !== "escalated") {
|
|
1651
|
-
throw new TypeError("retry_final_verification status requires current_target compact-v2 escalated authority");
|
|
1652
|
-
}
|
|
1653
|
-
if (actionDisposition !== REVIEW_STATUS_ACTION_DISPOSITION.FINAL_VERIFICATION_RETRY) throw new TypeError("retry_final_verification status requires final_verification_retry disposition");
|
|
1654
|
-
finalVerificationRetry = decodeFinalVerificationRetry(body.final_verification_retry, "status.final_verification_retry");
|
|
1655
|
-
} else if (body.final_verification_retry !== undefined) {
|
|
1656
|
-
throw new TypeError("status.final_verification_retry is only valid for the retry_final_verification action");
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
1855
|
// status/v5 additive top-level repository context reference (captured
|
|
1660
1856
|
// 2026-08-16 from 2.4.0-main.b1afef46; missing from the published
|
|
1661
1857
|
// status-v5.schema.json, so the live capture is authoritative). Same
|
|
@@ -1666,7 +1862,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1666
1862
|
if (source.capability !== "review.opaque_repository_context") throw new TypeError("status.repository_context.capability is unsupported");
|
|
1667
1863
|
repositoryContext = {
|
|
1668
1864
|
capability: "review.opaque_repository_context",
|
|
1669
|
-
handle: text(source.handle, "status.repository_context.handle", { pattern: /^
|
|
1865
|
+
handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx[12]_[0-9a-f]{64}$/ }),
|
|
1670
1866
|
revision: sha256(source.revision, "status.repository_context.revision"),
|
|
1671
1867
|
targetIdentity: sha256(source.target_identity, "status.repository_context.target_identity"),
|
|
1672
1868
|
...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "status.repository_context.event_id") }),
|
|
@@ -1674,26 +1870,34 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1674
1870
|
};
|
|
1675
1871
|
}
|
|
1676
1872
|
|
|
1873
|
+
// status/v7 top-level optional digest (gentle-ai v2.6.0): resolves #4066's
|
|
1874
|
+
// closed loop where `sdd-attempt finish` named a digest status never
|
|
1875
|
+
// published. Absent on the `staged` projection, which never resolves an
|
|
1876
|
+
// inventory, so it stays structurally optional rather than a required v7
|
|
1877
|
+
// field.
|
|
1878
|
+
const eligibleUntrackedInventory = v7 && body.eligible_untracked_inventory !== undefined
|
|
1879
|
+
? sha256(body.eligible_untracked_inventory, "status.eligible_untracked_inventory")
|
|
1880
|
+
: undefined;
|
|
1881
|
+
|
|
1677
1882
|
return {
|
|
1678
1883
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
1679
1884
|
applicability,
|
|
1680
1885
|
...(authority === undefined ? {} : { authority }),
|
|
1681
|
-
receipt,
|
|
1886
|
+
...(receipt === undefined ? {} : { receipt }),
|
|
1682
1887
|
action,
|
|
1683
1888
|
...(actionDisposition === undefined ? {} : { actionDisposition }),
|
|
1684
1889
|
replayability,
|
|
1685
1890
|
...(frozen === undefined ? {} : { frozen }),
|
|
1686
|
-
...(reconciliation === undefined ? {} : { reconciliation }),
|
|
1687
1891
|
targetIdentity: sha256(body.target_identity, "status.target_identity"),
|
|
1688
1892
|
...(body.authority_target_identity === undefined ? {} : { authorityTargetIdentity: sha256(body.authority_target_identity, "status.authority_target_identity") }),
|
|
1689
1893
|
projection: decodeReviewProjectionV1(body.projection),
|
|
1690
1894
|
repair: decodeAuthorityRepairAssessmentV1(body.repair),
|
|
1691
1895
|
candidates: stringArray(body.candidates, "status.candidates", { unique: true, pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ }),
|
|
1692
1896
|
...(nextTransition === undefined ? {} : { nextTransition }),
|
|
1693
|
-
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
1694
|
-
...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
|
|
1695
1897
|
...(forecast === undefined ? {} : { forecast }),
|
|
1696
1898
|
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
1899
|
+
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
1900
|
+
...(eligibleUntrackedInventory === undefined ? {} : { eligibleUntrackedInventory }),
|
|
1697
1901
|
raw: body,
|
|
1698
1902
|
};
|
|
1699
1903
|
}
|
|
@@ -1774,13 +1978,14 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
|
|
|
1774
1978
|
// published v3 schema demands an `--agent claude-code` token that the live
|
|
1775
1979
|
// emitter omits when the caller declared no --agent, so the capture is
|
|
1776
1980
|
// authoritative and Pi replays whichever provider-owned invocation arrived.
|
|
1777
|
-
export function decodeReviewConsentV3(value: unknown): ReviewConsentV3 {
|
|
1981
|
+
export function decodeReviewConsentV3(value: unknown, expectedAgent?: ReviewConsentAgentV3): ReviewConsentV3 {
|
|
1778
1982
|
const body = exactRecord(value, "consent", [...CONSENT_KEYS_V2, "agent"]);
|
|
1779
1983
|
requireIdentity(body, "gentle-ai.review-integration.consent/v3", "review.start");
|
|
1780
|
-
|
|
1984
|
+
const agent = enumeration(body.agent, Object.values(REVIEW_CONSENT_AGENT_V3), "consent.agent") as ReviewConsentAgentV3;
|
|
1985
|
+
if (expectedAgent !== undefined && agent !== expectedAgent) throw new TypeError("consent.agent does not match the expected runtime binding");
|
|
1781
1986
|
return {
|
|
1782
1987
|
schema: "gentle-ai.review-integration.consent/v3",
|
|
1783
|
-
agent
|
|
1988
|
+
agent,
|
|
1784
1989
|
...decodeConsentSemantics(body),
|
|
1785
1990
|
raw: body,
|
|
1786
1991
|
};
|
|
@@ -1882,56 +2087,21 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
|
|
|
1882
2087
|
// operation/v2
|
|
1883
2088
|
// ---------------------------------------------------------------------------
|
|
1884
2089
|
|
|
1885
|
-
export function
|
|
1886
|
-
const body = exactRecord(value,
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
decodeTargetedValidationRequestV1(result.validation_request, "operation.result.validation_request");
|
|
1901
|
-
if (result.state !== "correction_required") throw new TypeError("operation.result.validation_request requires state correction_required");
|
|
1902
|
-
}
|
|
1903
|
-
if (result.escalation !== undefined) nonempty(result.escalation, "operation.result.escalation");
|
|
1904
|
-
} else if (operation === REVIEW_INTEGRATION_OPERATION.VALIDATE) {
|
|
1905
|
-
result = exactRecord(body.result, "operation.result", ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
|
|
1906
|
-
if (result.schema !== "gentle-ai.review-gate-result/v1") throw new TypeError("operation.result does not match review.validate");
|
|
1907
|
-
enumeration(result.result, ["allow", "scope-changed", "invalidated", "escalated"] as const, "operation.result.result");
|
|
1908
|
-
boolean(result.allowed, "operation.result.allowed");
|
|
1909
|
-
nonempty(result.action, "operation.result.action");
|
|
1910
|
-
nonempty(result.reason, "operation.result.reason");
|
|
1911
|
-
record(result.context, "operation.result.context");
|
|
1912
|
-
if (result.delivery !== undefined && result.delivery !== "disabled/unmanaged") throw new TypeError("operation.result.delivery is unsupported");
|
|
1913
|
-
} else if (operation === REVIEW_INTEGRATION_OPERATION.BIND_SDD) {
|
|
1914
|
-
result = exactRecord(body.result, "operation.result", ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
|
|
1915
|
-
if (result.schema !== "gentle-ai.sdd-review-binding/v1") throw new TypeError("operation.result does not match review.bind_sdd");
|
|
1916
|
-
sha256(result.revision, "operation.result.revision");
|
|
1917
|
-
nonempty(result.change, "operation.result.change");
|
|
1918
|
-
nonempty(result.lineage, "operation.result.lineage");
|
|
1919
|
-
sha256(result.authority_revision, "operation.result.authority_revision");
|
|
1920
|
-
sha256(result.receipt_hash, "operation.result.receipt_hash");
|
|
1921
|
-
record(result.gate_context, "operation.result.gate_context");
|
|
1922
|
-
} else {
|
|
1923
|
-
result = exactRecord(body.result, "operation.result", ["operation", "predecessor_lineage_id", "predecessor_revision", "lineage_id", "state", "store_revision", "target_identity", "incident_digest", "recovery_disposition"]);
|
|
1924
|
-
if (result.operation !== "review.retry_final_verification") throw new TypeError("operation.result does not match review.retry_final_verification");
|
|
1925
|
-
lineage(result.predecessor_lineage_id, "operation.result.predecessor_lineage_id");
|
|
1926
|
-
sha256(result.predecessor_revision, "operation.result.predecessor_revision");
|
|
1927
|
-
lineage(result.lineage_id, "operation.result.lineage_id");
|
|
1928
|
-
if (result.state !== "validating") throw new TypeError("operation.result.state must be validating");
|
|
1929
|
-
sha256(result.store_revision, "operation.result.store_revision");
|
|
1930
|
-
sha256(result.target_identity, "operation.result.target_identity");
|
|
1931
|
-
sha256(result.incident_digest, "operation.result.incident_digest");
|
|
1932
|
-
if (result.recovery_disposition !== "final_verification_retry") throw new TypeError("operation.result.recovery_disposition must be final_verification_retry");
|
|
1933
|
-
}
|
|
1934
|
-
return { contract: REVIEW_INTEGRATION_CONTRACT, operation, result, raw: body };
|
|
2090
|
+
export function decodeReviewAdvisoryFindingsV1(value: unknown, label: string): ReviewAdvisoryFindingsV1 {
|
|
2091
|
+
const body = exactRecord(value, label, ["statement", "findings"]);
|
|
2092
|
+
return {
|
|
2093
|
+
statement: nonempty(body.statement, `${label}.statement`),
|
|
2094
|
+
findings: array(body.findings, `${label}.findings`, (entry, itemLabel) => {
|
|
2095
|
+
const finding = exactRecord(entry, itemLabel, ["id", "severity", "disposition"], ["lens", "location"]);
|
|
2096
|
+
return {
|
|
2097
|
+
id: nonempty(finding.id, `${itemLabel}.id`),
|
|
2098
|
+
...(finding.lens === undefined ? {} : { lens: nonempty(finding.lens, `${itemLabel}.lens`) }),
|
|
2099
|
+
...(finding.location === undefined ? {} : { location: nonempty(finding.location, `${itemLabel}.location`) }),
|
|
2100
|
+
severity: enumeration(finding.severity, Object.values(REVIEW_ADVISORY_FINDING_SEVERITY), `${itemLabel}.severity`),
|
|
2101
|
+
disposition: enumeration(finding.disposition, Object.values(REVIEW_ADVISORY_FINDING_DISPOSITION), `${itemLabel}.disposition`),
|
|
2102
|
+
};
|
|
2103
|
+
}, { minimum: 1 }),
|
|
2104
|
+
};
|
|
1935
2105
|
}
|
|
1936
2106
|
|
|
1937
2107
|
// ---------------------------------------------------------------------------
|
|
@@ -2066,3 +2236,394 @@ export function decodeReviewResultArtifactV2(value: unknown): ReviewResultArtifa
|
|
|
2066
2236
|
raw: body,
|
|
2067
2237
|
};
|
|
2068
2238
|
}
|
|
2239
|
+
|
|
2240
|
+
// Terminal capture answers are emitted directly by the native provider. Pi
|
|
2241
|
+
// validates their closed shape and identity, then treats all lifecycle meaning
|
|
2242
|
+
// as opaque provider-owned state.
|
|
2243
|
+
export const REVIEW_LAST_EVENT_CLOSURE_SCHEMA = "gentle-ai.review-last-event-closure/v1";
|
|
2244
|
+
export const REVIEW_LAST_EVENT_CLOSURE_OPERATION = {
|
|
2245
|
+
CAPTURE_RESULT: "review/capture-result",
|
|
2246
|
+
CAPTURE_CORRECTION_PLAN: "review.capture-correction-plan",
|
|
2247
|
+
CAPTURE_REFUTER: "review.capture-refuter",
|
|
2248
|
+
CAPTURE_VALIDATION: "review/capture-validation",
|
|
2249
|
+
} as const;
|
|
2250
|
+
export type ReviewLastEventClosureOperation = (typeof REVIEW_LAST_EVENT_CLOSURE_OPERATION)[keyof typeof REVIEW_LAST_EVENT_CLOSURE_OPERATION];
|
|
2251
|
+
|
|
2252
|
+
const REVIEW_LAST_EVENT_TERMINAL_STATES = ["approved", "correction_required", "escalated"] as const;
|
|
2253
|
+
export type ReviewLastEventClosureState = (typeof REVIEW_LAST_EVENT_TERMINAL_STATES)[number];
|
|
2254
|
+
|
|
2255
|
+
const REVIEW_STATUS_CONTINUATION_OPERATION = {
|
|
2256
|
+
STATUS: "review.status",
|
|
2257
|
+
} as const;
|
|
2258
|
+
|
|
2259
|
+
export interface ReviewStatusContinuationBindingV1 {
|
|
2260
|
+
targetIdentity: string;
|
|
2261
|
+
lineageId?: string;
|
|
2262
|
+
revision?: string;
|
|
2263
|
+
repositoryContext?: string;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
export interface ReviewStatusContinuationArtifactV1 {
|
|
2267
|
+
schema: "gentle-ai.review-result-artifact/v2";
|
|
2268
|
+
capability: "review.native_result_artifact";
|
|
2269
|
+
sha256: string;
|
|
2270
|
+
lineageId: string;
|
|
2271
|
+
targetIdentity: string;
|
|
2272
|
+
lens: string;
|
|
2273
|
+
selectedOrder: number;
|
|
2274
|
+
subjectHash: string;
|
|
2275
|
+
admissionDecision: "completed";
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
export interface ReviewStatusContinuationV1 {
|
|
2279
|
+
operation: typeof REVIEW_STATUS_CONTINUATION_OPERATION.STATUS;
|
|
2280
|
+
arguments: readonly ReviewTransitionArgumentV3[];
|
|
2281
|
+
selectorArguments?: readonly ReviewTransitionArgumentV3[];
|
|
2282
|
+
preconditions: readonly ReviewTransitionArgumentV3[];
|
|
2283
|
+
binding: ReviewStatusContinuationBindingV1;
|
|
2284
|
+
artifacts?: readonly ReviewStatusContinuationArtifactV1[];
|
|
2285
|
+
command?: string;
|
|
2286
|
+
raw: Record<string, unknown>;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION = "review.acknowledge-approved" as const;
|
|
2290
|
+
|
|
2291
|
+
// The exact ordered argument names the provider renders for one approved
|
|
2292
|
+
// acknowledgement. The list is closed and positional on the wire, so decoding
|
|
2293
|
+
// by position is what proves the relay is replaying the provider's own
|
|
2294
|
+
// invocation rather than one the host assembled.
|
|
2295
|
+
const REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS = ["cwd", "lineage", "target", "expected-revision", "token"] as const;
|
|
2296
|
+
|
|
2297
|
+
export interface ReviewApprovedAcknowledgementBindingV1 {
|
|
2298
|
+
lineageId: string;
|
|
2299
|
+
revision: string;
|
|
2300
|
+
targetIdentity: string;
|
|
2301
|
+
repositoryContext?: string;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
export interface ReviewApprovedAcknowledgementV1 {
|
|
2305
|
+
operation: typeof REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION;
|
|
2306
|
+
command: string;
|
|
2307
|
+
arguments: readonly ReviewTransitionArgumentV3[];
|
|
2308
|
+
preconditions: readonly ReviewTransitionArgumentV3[];
|
|
2309
|
+
binding: ReviewApprovedAcknowledgementBindingV1;
|
|
2310
|
+
raw: Record<string, unknown>;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
export interface ReviewApprovedAcknowledgementExecuteExpectationV1 { cwd?: string; lineageId?: string; targetIdentity?: string; revision?: string; }
|
|
2314
|
+
|
|
2315
|
+
export type ReviewApprovedAcknowledgementExecuteTokensV1 = readonly [string, string, string, string, string];
|
|
2316
|
+
|
|
2317
|
+
interface ReviewApprovedAcknowledgementExecuteShapeV1 { tokens: ReviewApprovedAcknowledgementExecuteTokensV1; values: readonly [string, string, string, string, string]; lineageId: string; targetIdentity: string; revision: string; }
|
|
2318
|
+
|
|
2319
|
+
function assertReviewApprovedAcknowledgementExecuteShapeV1(execute: ReviewNextTransitionExecuteV3): ReviewApprovedAcknowledgementExecuteShapeV1 {
|
|
2320
|
+
if (execute.operation !== REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION) throw new TypeError(`acknowledgement.execute.operation must be ${REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION}`);
|
|
2321
|
+
if (execute.command === undefined) throw new TypeError("acknowledgement.execute.command is required");
|
|
2322
|
+
text(execute.command, "acknowledgement.execute.command", { minimum: 1, pattern: /^gentle-ai review acknowledge-approved(?: |$)/ });
|
|
2323
|
+
if (execute.arguments.length !== REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS.length) throw new TypeError(`acknowledgement.execute.arguments must carry exactly ${REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS.length} provider-issued arguments`);
|
|
2324
|
+
const values = execute.arguments.map((argument, index) => { const name = REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS[index]!; if (argument.name !== name) throw new TypeError(`acknowledgement.execute.arguments[${index}].name must be ${name}`); const value = nonempty(argument.value, `acknowledgement.execute.arguments[${index}].value`); if (nonempty(argument.token, `acknowledgement.execute.arguments[${index}].token`) !== `--${name}=${value}`) throw new TypeError(`acknowledgement.execute.arguments[${index}].token must exactly match ${name}`); return value; });
|
|
2325
|
+
if (execute.preconditions.length !== 1 || execute.preconditions[0]?.name !== "state" || execute.preconditions[0]?.value !== "approved") throw new TypeError("acknowledgement.execute.preconditions must be the single approved state precondition");
|
|
2326
|
+
return { tokens: execute.arguments.map((argument) => argument.token!) as ReviewApprovedAcknowledgementExecuteTokensV1, values: values as readonly [string, string, string, string, string], lineageId: lineage(execute.binding.lineageId, "acknowledgement.execute.binding.lineage_id"), targetIdentity: sha256(execute.binding.targetIdentity, "acknowledgement.execute.binding.target_identity"), revision: sha256(execute.binding.revision, "acknowledgement.execute.binding.revision") };
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Proves that an approved-acknowledgement execute vector is the exact closed,
|
|
2331
|
+
* provider-issued burn invocation. Callers can additionally bind its values to
|
|
2332
|
+
* their own current STATUS before invoking the returned tokens.
|
|
2333
|
+
*/
|
|
2334
|
+
export function assertReviewApprovedAcknowledgementExecuteV1(
|
|
2335
|
+
execute: ReviewNextTransitionExecuteV3,
|
|
2336
|
+
expected: ReviewApprovedAcknowledgementExecuteExpectationV1 = {},
|
|
2337
|
+
): ReviewApprovedAcknowledgementExecuteTokensV1 {
|
|
2338
|
+
const shape = assertReviewApprovedAcknowledgementExecuteShapeV1(execute);
|
|
2339
|
+
if (shape.values[1] !== shape.lineageId) throw new TypeError("acknowledgement lineage argument does not match its binding");
|
|
2340
|
+
if (shape.values[2] !== shape.targetIdentity) throw new TypeError("acknowledgement target argument does not match its binding");
|
|
2341
|
+
if (shape.values[3] !== shape.revision) throw new TypeError("acknowledgement revision argument does not match its binding");
|
|
2342
|
+
if (expected.cwd !== undefined && shape.values[0] !== expected.cwd) throw new TypeError("acknowledgement.execute.cwd does not match the current target");
|
|
2343
|
+
if (expected.lineageId !== undefined && shape.lineageId !== expected.lineageId) throw new TypeError("acknowledgement.execute.lineage does not match the current target");
|
|
2344
|
+
if (expected.targetIdentity !== undefined && shape.targetIdentity !== expected.targetIdentity) throw new TypeError("acknowledgement.execute.target does not match the current target");
|
|
2345
|
+
if (expected.revision !== undefined && shape.revision !== expected.revision) throw new TypeError("acknowledgement.execute.revision does not match the current target"); return shape.tokens;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// ---------------------------------------------------------------------------
|
|
2349
|
+
// review-acknowledged/v1 — the answer the burn prints (gentle-ai #3947)
|
|
2350
|
+
// ---------------------------------------------------------------------------
|
|
2351
|
+
|
|
2352
|
+
// Until gentle-ai #3947 a successful `review acknowledge-approved` burned its
|
|
2353
|
+
// authority in silence and a host could only infer the burn from a later
|
|
2354
|
+
// STATUS offering a fresh START. From main commit bc9f74d2 the command prints
|
|
2355
|
+
// exactly this envelope on success. Like result-artifact/v2 it carries no
|
|
2356
|
+
// `contract`: the schema constant plus the slash-form operation are its whole
|
|
2357
|
+
// identity, so requireIdentity (which demands the contract pair) does not
|
|
2358
|
+
// apply. Every published release up to v2.5.0-rc.3 still prints nothing; the
|
|
2359
|
+
// native client owns that distinction, this decoder only ever sees bytes.
|
|
2360
|
+
export const REVIEW_ACKNOWLEDGED_SCHEMA = "gentle-ai.review-acknowledged/v1" as const;
|
|
2361
|
+
const REVIEW_ACKNOWLEDGED_OPERATION = "review/acknowledge-approved" as const;
|
|
2362
|
+
|
|
2363
|
+
export interface ReviewAcknowledgedV1 {
|
|
2364
|
+
schema: typeof REVIEW_ACKNOWLEDGED_SCHEMA;
|
|
2365
|
+
operation: typeof REVIEW_ACKNOWLEDGED_OPERATION;
|
|
2366
|
+
action: "acknowledged";
|
|
2367
|
+
lineageId: string;
|
|
2368
|
+
targetIdentity: string;
|
|
2369
|
+
consumedRevision: string;
|
|
2370
|
+
authority: "burned";
|
|
2371
|
+
raw: Record<string, unknown>;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
/** The binding the caller already holds from STATUS; the envelope must name exactly that burn. */
|
|
2375
|
+
export interface ReviewAcknowledgedExpectationV1 {
|
|
2376
|
+
lineageId?: string;
|
|
2377
|
+
targetIdentity?: string;
|
|
2378
|
+
revision?: string;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
export function decodeReviewAcknowledgedV1(value: unknown, expected: ReviewAcknowledgedExpectationV1 = {}): ReviewAcknowledgedV1 {
|
|
2382
|
+
if (record(value, "acknowledged").schema !== REVIEW_ACKNOWLEDGED_SCHEMA) throw new TypeError(`schema must be ${REVIEW_ACKNOWLEDGED_SCHEMA}`);
|
|
2383
|
+
const body = exactRecord(value, "acknowledged", ["schema", "operation", "action", "lineage_id", "target_identity", "consumed_revision", "authority"]);
|
|
2384
|
+
if (body.operation !== REVIEW_ACKNOWLEDGED_OPERATION) throw new TypeError(`acknowledged.operation must be ${REVIEW_ACKNOWLEDGED_OPERATION}`);
|
|
2385
|
+
if (body.action !== "acknowledged") throw new TypeError("acknowledged.action must be acknowledged");
|
|
2386
|
+
if (body.authority !== "burned") throw new TypeError("acknowledged.authority must be burned");
|
|
2387
|
+
const decoded: ReviewAcknowledgedV1 = {
|
|
2388
|
+
schema: REVIEW_ACKNOWLEDGED_SCHEMA,
|
|
2389
|
+
operation: REVIEW_ACKNOWLEDGED_OPERATION,
|
|
2390
|
+
action: "acknowledged",
|
|
2391
|
+
lineageId: lineage(body.lineage_id, "acknowledged.lineage_id"),
|
|
2392
|
+
targetIdentity: sha256(body.target_identity, "acknowledged.target_identity"),
|
|
2393
|
+
consumedRevision: sha256(body.consumed_revision, "acknowledged.consumed_revision"),
|
|
2394
|
+
authority: "burned",
|
|
2395
|
+
raw: body,
|
|
2396
|
+
};
|
|
2397
|
+
if (expected.lineageId !== undefined && decoded.lineageId !== expected.lineageId) throw new TypeError("acknowledged.lineage_id does not name the acknowledged lineage");
|
|
2398
|
+
if (expected.targetIdentity !== undefined && decoded.targetIdentity !== expected.targetIdentity) throw new TypeError("acknowledged.target_identity does not name the acknowledged target");
|
|
2399
|
+
if (expected.revision !== undefined && decoded.consumedRevision !== expected.revision) throw new TypeError("acknowledged.consumed_revision does not name the consumed revision");
|
|
2400
|
+
return decoded;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
export interface ReviewLastEventClosureV1 {
|
|
2404
|
+
schema: typeof REVIEW_LAST_EVENT_CLOSURE_SCHEMA;
|
|
2405
|
+
operation: ReviewLastEventClosureOperation;
|
|
2406
|
+
lineageId: string;
|
|
2407
|
+
state: ReviewLastEventClosureState;
|
|
2408
|
+
storeRevision: string;
|
|
2409
|
+
action?: string;
|
|
2410
|
+
targetIdentity?: string;
|
|
2411
|
+
requestHash?: string;
|
|
2412
|
+
correctionLines?: number;
|
|
2413
|
+
advisoryFindings?: ReviewAdvisoryFindingsV1;
|
|
2414
|
+
statusContinuation?: ReviewStatusContinuationV1;
|
|
2415
|
+
acknowledgement?: ReviewApprovedAcknowledgementV1;
|
|
2416
|
+
// Set when the provider sent an acknowledgement this decoder could not
|
|
2417
|
+
// read. It is deliberately distinct from an absent one: the host must be
|
|
2418
|
+
// able to tell "approved, nothing to run" from "approved, and the thing
|
|
2419
|
+
// that ends this is present but unreadable here".
|
|
2420
|
+
acknowledgementUndecodable?: true;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
export interface ReviewLastEventClosureBinding {
|
|
2424
|
+
lineageId: string;
|
|
2425
|
+
targetIdentity?: string;
|
|
2426
|
+
requestHash?: string;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function decodeReviewStatusContinuationArtifactV1(value: unknown, label: string): ReviewStatusContinuationArtifactV1 {
|
|
2430
|
+
const artifact = exactRecord(value, label, ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"]);
|
|
2431
|
+
if (artifact.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError(`${label}.schema must be gentle-ai.review-result-artifact/v2`);
|
|
2432
|
+
if (artifact.capability !== "review.native_result_artifact") throw new TypeError(`${label}.capability must be review.native_result_artifact`);
|
|
2433
|
+
if (artifact.admission_decision !== "completed") throw new TypeError(`${label}.admission_decision must be completed`);
|
|
2434
|
+
return {
|
|
2435
|
+
schema: "gentle-ai.review-result-artifact/v2",
|
|
2436
|
+
capability: "review.native_result_artifact",
|
|
2437
|
+
sha256: sha256(artifact.sha256, `${label}.sha256`),
|
|
2438
|
+
lineageId: lineage(artifact.lineage_id, `${label}.lineage_id`),
|
|
2439
|
+
targetIdentity: sha256(artifact.target_identity, `${label}.target_identity`),
|
|
2440
|
+
lens: nonempty(artifact.lens, `${label}.lens`),
|
|
2441
|
+
selectedOrder: integer(artifact.selected_order, `${label}.selected_order`, 0),
|
|
2442
|
+
subjectHash: sha256(artifact.subject_hash, `${label}.subject_hash`),
|
|
2443
|
+
admissionDecision: "completed",
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// decodeReviewApprovedAcknowledgementV1 retains the closure-specific wire
|
|
2448
|
+
// boundary with the shared shape validator; its semantic binding stays outside
|
|
2449
|
+
// the defensive closure catch in the exported assertion.
|
|
2450
|
+
function decodeReviewApprovedAcknowledgementV1(value: unknown, label: string): ReviewApprovedAcknowledgementV1 {
|
|
2451
|
+
const body = exactRecord(value, label, ["operation", "command", "arguments", "preconditions", "binding"]);
|
|
2452
|
+
const argumentsList = decodeTransitionArguments(body.arguments, `${label}.arguments`);
|
|
2453
|
+
const preconditions = decodeTransitionArguments(body.preconditions, `${label}.preconditions`);
|
|
2454
|
+
const sourceBinding = exactRecord(body.binding, `${label}.binding`, ["lineage_id", "revision", "target_identity"], ["repository_context"]);
|
|
2455
|
+
const repositoryContext = sourceBinding.repository_context === undefined
|
|
2456
|
+
? undefined
|
|
2457
|
+
: text(sourceBinding.repository_context, `${label}.binding.repository_context`, { pattern: /^rctx[12]_[0-9a-f]{64}$/ });
|
|
2458
|
+
const acknowledgement = {
|
|
2459
|
+
operation: body.operation,
|
|
2460
|
+
command: body.command,
|
|
2461
|
+
arguments: argumentsList,
|
|
2462
|
+
preconditions,
|
|
2463
|
+
binding: {
|
|
2464
|
+
lineageId: sourceBinding.lineage_id,
|
|
2465
|
+
revision: sourceBinding.revision,
|
|
2466
|
+
targetIdentity: sourceBinding.target_identity,
|
|
2467
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
2468
|
+
},
|
|
2469
|
+
} as unknown as ReviewNextTransitionExecuteV3;
|
|
2470
|
+
const shape = assertReviewApprovedAcknowledgementExecuteShapeV1(acknowledgement);
|
|
2471
|
+
return {
|
|
2472
|
+
operation: REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION,
|
|
2473
|
+
command: acknowledgement.command!,
|
|
2474
|
+
arguments: argumentsList,
|
|
2475
|
+
preconditions,
|
|
2476
|
+
binding: {
|
|
2477
|
+
lineageId: shape.lineageId,
|
|
2478
|
+
revision: shape.revision,
|
|
2479
|
+
targetIdentity: shape.targetIdentity,
|
|
2480
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
2481
|
+
},
|
|
2482
|
+
raw: body,
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
function decodeReviewStatusContinuationV1(value: unknown): ReviewStatusContinuationV1 {
|
|
2487
|
+
const body = exactRecord(value, "last_event_closure.status_continuation", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
|
|
2488
|
+
if (body.operation !== REVIEW_STATUS_CONTINUATION_OPERATION.STATUS) throw new TypeError("last_event_closure.status_continuation.operation must be review.status");
|
|
2489
|
+
const argumentsList = decodeTransitionArguments(body.arguments, "last_event_closure.status_continuation.arguments");
|
|
2490
|
+
if (argumentsList.some((argument) => argument.token === undefined)) throw new TypeError("last_event_closure.status_continuation.arguments require exact tokens");
|
|
2491
|
+
const preconditions = decodeTransitionArguments(body.preconditions, "last_event_closure.status_continuation.preconditions");
|
|
2492
|
+
if (preconditions.length === 0) throw new TypeError("last_event_closure.status_continuation.preconditions requires at least one entry");
|
|
2493
|
+
const sourceBinding = exactRecord(body.binding, "last_event_closure.status_continuation.binding", ["target_identity"], ["lineage_id", "revision", "repository_context"]);
|
|
2494
|
+
const lineageId = sourceBinding.lineage_id === undefined ? undefined : lineage(sourceBinding.lineage_id, "last_event_closure.status_continuation.binding.lineage_id");
|
|
2495
|
+
const revision = sourceBinding.revision === undefined ? undefined : sha256(sourceBinding.revision, "last_event_closure.status_continuation.binding.revision");
|
|
2496
|
+
const repositoryContext = sourceBinding.repository_context === undefined
|
|
2497
|
+
? undefined
|
|
2498
|
+
: text(sourceBinding.repository_context, "last_event_closure.status_continuation.binding.repository_context", { pattern: /^rctx[12]_[0-9a-f]{64}$/ });
|
|
2499
|
+
const selectorArguments = body.selector_arguments === undefined
|
|
2500
|
+
? undefined
|
|
2501
|
+
: decodeTransitionArguments(body.selector_arguments, "last_event_closure.status_continuation.selector_arguments");
|
|
2502
|
+
const artifacts = body.artifacts === undefined
|
|
2503
|
+
? undefined
|
|
2504
|
+
: array(body.artifacts, "last_event_closure.status_continuation.artifacts", decodeReviewStatusContinuationArtifactV1);
|
|
2505
|
+
const command = body.command === undefined
|
|
2506
|
+
? undefined
|
|
2507
|
+
: text(body.command, "last_event_closure.status_continuation.command", { minimum: 1, pattern: /^gentle-ai review [a-z][a-z-]*/ });
|
|
2508
|
+
return {
|
|
2509
|
+
operation: REVIEW_STATUS_CONTINUATION_OPERATION.STATUS,
|
|
2510
|
+
arguments: argumentsList,
|
|
2511
|
+
...(selectorArguments === undefined ? {} : { selectorArguments }),
|
|
2512
|
+
preconditions,
|
|
2513
|
+
binding: {
|
|
2514
|
+
targetIdentity: sha256(sourceBinding.target_identity, "last_event_closure.status_continuation.binding.target_identity"),
|
|
2515
|
+
...(lineageId === undefined ? {} : { lineageId }),
|
|
2516
|
+
...(revision === undefined ? {} : { revision }),
|
|
2517
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
2518
|
+
},
|
|
2519
|
+
...(artifacts === undefined ? {} : { artifacts }),
|
|
2520
|
+
...(command === undefined ? {} : { command }),
|
|
2521
|
+
raw: body,
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventClosureV1 {
|
|
2526
|
+
const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "advisory_findings", "status_continuation", "acknowledgement"]);
|
|
2527
|
+
if (body.schema !== REVIEW_LAST_EVENT_CLOSURE_SCHEMA) throw new TypeError(`last_event_closure.schema must be ${REVIEW_LAST_EVENT_CLOSURE_SCHEMA}`);
|
|
2528
|
+
const operation = enumeration(body.operation, Object.values(REVIEW_LAST_EVENT_CLOSURE_OPERATION), "last_event_closure.operation") as ReviewLastEventClosureOperation;
|
|
2529
|
+
const state = enumeration(body.state, REVIEW_LAST_EVENT_TERMINAL_STATES, "last_event_closure.state") as ReviewLastEventClosureState;
|
|
2530
|
+
const shared = {
|
|
2531
|
+
schema: REVIEW_LAST_EVENT_CLOSURE_SCHEMA,
|
|
2532
|
+
operation,
|
|
2533
|
+
lineageId: lineage(body.lineage_id, "last_event_closure.lineage_id"),
|
|
2534
|
+
state,
|
|
2535
|
+
storeRevision: sha256(body.store_revision, "last_event_closure.store_revision"),
|
|
2536
|
+
};
|
|
2537
|
+
if (operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_CORRECTION_PLAN) {
|
|
2538
|
+
if (body.action !== undefined || body.advisory_findings !== undefined || body.status_continuation !== undefined) throw new TypeError("last_event_closure correction-plan cannot carry action, advisory_findings, or status_continuation");
|
|
2539
|
+
if (state !== "correction_required") throw new TypeError("last_event_closure correction-plan requires correction_required state");
|
|
2540
|
+
return {
|
|
2541
|
+
...shared,
|
|
2542
|
+
targetIdentity: sha256(body.target_identity, "last_event_closure.target_identity"),
|
|
2543
|
+
requestHash: sha256(body.request_hash, "last_event_closure.request_hash"),
|
|
2544
|
+
correctionLines: integer(body.correction_lines, "last_event_closure.correction_lines", 1, 200),
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
if (body.target_identity !== undefined || body.request_hash !== undefined || body.correction_lines !== undefined) throw new TypeError("last_event_closure terminal capture cannot carry correction-plan fields");
|
|
2548
|
+
const requiresStatusContinuation = state === "correction_required" && (
|
|
2549
|
+
operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_RESULT || operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_REFUTER
|
|
2550
|
+
);
|
|
2551
|
+
if (requiresStatusContinuation && body.status_continuation === undefined) throw new TypeError("last_event_closure requires status_continuation for correction-required result or refuter capture");
|
|
2552
|
+
if (!requiresStatusContinuation && body.status_continuation !== undefined) throw new TypeError("last_event_closure status_continuation is only valid for correction-required result or refuter capture");
|
|
2553
|
+
const statusContinuation = body.status_continuation === undefined ? undefined : decodeReviewStatusContinuationV1(body.status_continuation);
|
|
2554
|
+
if (statusContinuation !== undefined) {
|
|
2555
|
+
if (statusContinuation.binding.lineageId !== shared.lineageId) {
|
|
2556
|
+
throw new TypeError("last_event_closure status continuation lineage does not match its enclosing closure");
|
|
2557
|
+
}
|
|
2558
|
+
if (statusContinuation.binding.revision !== shared.storeRevision) {
|
|
2559
|
+
throw new TypeError("last_event_closure status continuation revision does not match its enclosing closure");
|
|
2560
|
+
}
|
|
2561
|
+
const lineageArguments = statusContinuation.arguments.filter((argument) => argument.name === "lineage");
|
|
2562
|
+
if (lineageArguments.length !== 1 || lineageArguments[0]?.value !== shared.lineageId || lineageArguments[0]?.token !== `--lineage=${shared.lineageId}`) {
|
|
2563
|
+
throw new TypeError("last_event_closure status continuation lineage argument does not match its enclosing closure");
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
// Only an approved closure may carry the continuation that burns its
|
|
2567
|
+
// authority: any other state offering an acknowledgement would be inviting
|
|
2568
|
+
// a burn the provider never authorized.
|
|
2569
|
+
//
|
|
2570
|
+
// It stays optional because the pinned installer binary predates it. A
|
|
2571
|
+
// provider that emits one is decoded strictly; a provider that does not is
|
|
2572
|
+
// still the contract this build ships against. Requiring it belongs with
|
|
2573
|
+
// the pin bump that makes it always present.
|
|
2574
|
+
// Decoded defensively on purpose. This decoder pins the acknowledgement to a
|
|
2575
|
+
// closed positional shape, and the same patch that added it already shows
|
|
2576
|
+
// what strictness costs when the provider moves: rejecting one omitempty
|
|
2577
|
+
// field made every targeted-validation STATUS that carried it undecodable.
|
|
2578
|
+
// Here the blast radius would be worse than a lost field. Throwing would
|
|
2579
|
+
// fail the whole approved closure, so the host would lose the approval
|
|
2580
|
+
// outcome AND the only invocation that burns the authority, leaving the
|
|
2581
|
+
// lineage approved and un-burnable at once. An unreadable continuation
|
|
2582
|
+
// degrades to a flag; everything else about the approval still arrives.
|
|
2583
|
+
let acknowledgement: ReviewApprovedAcknowledgementV1 | undefined;
|
|
2584
|
+
let acknowledgementUndecodable = false;
|
|
2585
|
+
if (body.acknowledgement !== undefined) {
|
|
2586
|
+
try {
|
|
2587
|
+
acknowledgement = decodeReviewApprovedAcknowledgementV1(body.acknowledgement, "last_event_closure.acknowledgement");
|
|
2588
|
+
} catch {
|
|
2589
|
+
acknowledgement = undefined;
|
|
2590
|
+
acknowledgementUndecodable = true;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (acknowledgementUndecodable && state !== "approved") {
|
|
2594
|
+
throw new TypeError("last_event_closure acknowledgement requires approved state");
|
|
2595
|
+
}
|
|
2596
|
+
if (acknowledgement !== undefined) {
|
|
2597
|
+
if (state !== "approved") throw new TypeError("last_event_closure acknowledgement requires approved state");
|
|
2598
|
+
assertReviewApprovedAcknowledgementExecuteV1(acknowledgement, { lineageId: shared.lineageId, revision: shared.storeRevision });
|
|
2599
|
+
}
|
|
2600
|
+
const action = nonempty(body.action, "last_event_closure.action");
|
|
2601
|
+
const advisoryFindings = body.advisory_findings === undefined
|
|
2602
|
+
? undefined
|
|
2603
|
+
: decodeReviewAdvisoryFindingsV1(body.advisory_findings, "last_event_closure.advisory_findings");
|
|
2604
|
+
if (advisoryFindings !== undefined && state !== "approved") throw new TypeError("last_event_closure advisory_findings requires approved state");
|
|
2605
|
+
return {
|
|
2606
|
+
...shared,
|
|
2607
|
+
action,
|
|
2608
|
+
...(advisoryFindings === undefined ? {} : { advisoryFindings }),
|
|
2609
|
+
...(statusContinuation === undefined ? {} : { statusContinuation }),
|
|
2610
|
+
...(acknowledgement === undefined ? {} : { acknowledgement }),
|
|
2611
|
+
...(acknowledgementUndecodable ? { acknowledgementUndecodable: true as const } : {}),
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
export function assertReviewLastEventClosureBinding(
|
|
2616
|
+
closure: ReviewLastEventClosureV1,
|
|
2617
|
+
binding: ReviewLastEventClosureBinding,
|
|
2618
|
+
): void {
|
|
2619
|
+
if (closure.lineageId !== binding.lineageId) throw new TypeError("last_event_closure lineage does not match its provider binding");
|
|
2620
|
+
if (binding.targetIdentity !== undefined && closure.targetIdentity !== undefined && closure.targetIdentity !== binding.targetIdentity) {
|
|
2621
|
+
throw new TypeError("last_event_closure target does not match its provider binding");
|
|
2622
|
+
}
|
|
2623
|
+
if (binding.targetIdentity !== undefined && closure.statusContinuation !== undefined && closure.statusContinuation.binding.targetIdentity !== binding.targetIdentity) {
|
|
2624
|
+
throw new TypeError("last_event_closure status continuation target does not match its provider binding");
|
|
2625
|
+
}
|
|
2626
|
+
if (binding.requestHash !== undefined && closure.requestHash !== undefined && closure.requestHash !== binding.requestHash) {
|
|
2627
|
+
throw new TypeError("last_event_closure request hash does not match its provider binding");
|
|
2628
|
+
}
|
|
2629
|
+
}
|