gentle-pi 2.1.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -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,38 +74,49 @@ 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));
|
|
92
|
+
// Typed failures are accepted only for the supported start, status, repair,
|
|
93
|
+
// and last-event capture operations. Retired lifecycle verbs fail closed.
|
|
94
|
+
const FAILURE_OPERATIONS = Object.freeze([
|
|
95
|
+
...REQUIRED_OPERATIONS,
|
|
96
|
+
"review.capture-result",
|
|
97
|
+
"review.capture-correction-plan",
|
|
98
|
+
"review.capture-refuter",
|
|
99
|
+
"review.capture-validation",
|
|
100
|
+
] as const);
|
|
96
101
|
const REQUIRED_GATES = Object.freeze(["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] as const);
|
|
97
102
|
const REQUIRED_PROJECTIONS = Object.freeze(Object.values(REVIEW_PROJECTION));
|
|
98
|
-
|
|
103
|
+
// The schema floor shared by every accepted capabilities identity. Each minor
|
|
104
|
+
// then adds its own capabilities/consent/status identities below: the provider
|
|
105
|
+
// swaps those three advertisements as the protocol minor advances (v2.1 moved
|
|
106
|
+
// consent to v3; v2.2 moved status to v5), ground-truthed against the vendored
|
|
107
|
+
// capabilities[-v2.1|-v2.2].schema.json contracts on gentle-ai main and a live
|
|
108
|
+
// v2.2 capture from a main-line dev build.
|
|
109
|
+
const REQUIRED_SCHEMAS_COMMON = Object.freeze([
|
|
99
110
|
"gentle-ai.review-admitted-result/v2",
|
|
100
111
|
"gentle-ai.review-artifact-subject/v2",
|
|
101
112
|
"gentle-ai.review-authority-repair-assessment/v1",
|
|
102
113
|
"gentle-ai.review-authority-status/v1",
|
|
103
114
|
"gentle-ai.review-gate-request/v1",
|
|
104
|
-
"gentle-ai.review-integration.capabilities/v2",
|
|
105
|
-
"gentle-ai.review-integration.consent/v2",
|
|
106
115
|
"gentle-ai.review-integration.failure/v2",
|
|
107
116
|
"gentle-ai.review-final-verification-incident/v1",
|
|
108
117
|
"gentle-ai.review-integration.operation/v2",
|
|
109
118
|
"gentle-ai.review-integration.projection/v1",
|
|
110
119
|
"gentle-ai.review-integration.repair/v2",
|
|
111
|
-
"gentle-ai.review-integration.start/v3",
|
|
112
|
-
"gentle-ai.review-integration.status/v3",
|
|
113
120
|
"gentle-ai.review-receipt/v1",
|
|
114
121
|
"gentle-ai.review-receipt/v2",
|
|
115
122
|
"gentle-ai.review-result-artifact/v2",
|
|
@@ -119,6 +126,64 @@ const REQUIRED_SCHEMAS = Object.freeze([
|
|
|
119
126
|
"https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
120
127
|
"https://gentle-ai.dev/schema/review/validator/v1",
|
|
121
128
|
] as const);
|
|
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({
|
|
162
|
+
"gentle-ai.review-integration.capabilities/v2": Object.freeze({
|
|
163
|
+
protocolMinor: 0,
|
|
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"]),
|
|
165
|
+
}),
|
|
166
|
+
"gentle-ai.review-integration.capabilities/v2.1": Object.freeze({
|
|
167
|
+
protocolMinor: 1,
|
|
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"]),
|
|
169
|
+
}),
|
|
170
|
+
"gentle-ai.review-integration.capabilities/v2.2": Object.freeze({
|
|
171
|
+
protocolMinor: 2,
|
|
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
|
+
});
|
|
122
187
|
const OPTIONAL_FEATURE_NAMES = Object.freeze([
|
|
123
188
|
"base_ref_workspace_overlay",
|
|
124
189
|
"bounded_process_waits",
|
|
@@ -175,7 +240,6 @@ type ReviewLens = (typeof REVIEW_LENSES)[number];
|
|
|
175
240
|
type RiskReasonCode = (typeof RISK_REASON_CODES)[number];
|
|
176
241
|
type RiskSignal = (typeof RISK_SIGNALS)[number];
|
|
177
242
|
type ReviewStatusAction = (typeof STATUS_ACTIONS)[number];
|
|
178
|
-
type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
|
|
179
243
|
|
|
180
244
|
export interface ReviewFeatureV2 {
|
|
181
245
|
name: (typeof FEATURE_NAMES)[number];
|
|
@@ -235,8 +299,19 @@ export interface ReviewRepositoryContextV2 {
|
|
|
235
299
|
handle: string;
|
|
236
300
|
revision: string;
|
|
237
301
|
targetIdentity: string;
|
|
302
|
+
// Optional additive members on the same start/v3 identity (gentle-ai main,
|
|
303
|
+
// Go `ReviewRepositoryContextReference` with `omitempty`): the compact
|
|
304
|
+
// effects event this context applied and its recorded outcome. Ground
|
|
305
|
+
// truth is the captured granted start from a 2.4.0-main binary
|
|
306
|
+
// (tests/fixtures/devbinary/start-v3-consent-granted.captured.json); the
|
|
307
|
+
// published start.schema.json is narrower than the emitter here.
|
|
308
|
+
eventId?: string;
|
|
309
|
+
outcome?: ReviewRepositoryContextOutcomeV1;
|
|
238
310
|
}
|
|
239
311
|
|
|
312
|
+
const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] as const;
|
|
313
|
+
export type ReviewRepositoryContextOutcomeV1 = (typeof REPOSITORY_CONTEXT_OUTCOMES)[number];
|
|
314
|
+
|
|
240
315
|
export interface ReviewStartV3 {
|
|
241
316
|
contract: typeof REVIEW_INTEGRATION_CONTRACT;
|
|
242
317
|
action: StartAction;
|
|
@@ -260,6 +335,11 @@ export interface ReviewStartV3 {
|
|
|
260
335
|
raw: Readonly<Record<string, unknown>>;
|
|
261
336
|
}
|
|
262
337
|
|
|
338
|
+
export interface ReviewStartV4 extends Omit<ReviewStartV3, "action"> {
|
|
339
|
+
action: "created" | "replayed" | "closed" | "blocked-scope-action";
|
|
340
|
+
nextTransition?: ReviewNextTransitionV3;
|
|
341
|
+
}
|
|
342
|
+
|
|
263
343
|
export interface ReviewProjectionDescriptorV1 {
|
|
264
344
|
schema: "gentle-ai.review-integration.projection/v1";
|
|
265
345
|
kind: ReviewProjectionKind;
|
|
@@ -307,28 +387,116 @@ export interface ReviewTransitionArgumentV3 {
|
|
|
307
387
|
export interface ReviewNextTransitionExecuteV3 {
|
|
308
388
|
operation: string;
|
|
309
389
|
arguments: readonly ReviewTransitionArgumentV3[];
|
|
390
|
+
selectorArguments?: readonly ReviewTransitionArgumentV3[];
|
|
310
391
|
preconditions: readonly ReviewTransitionArgumentV3[];
|
|
311
392
|
binding: { targetIdentity: string; lineageId?: string; revision?: string };
|
|
312
393
|
command?: string;
|
|
313
394
|
}
|
|
314
395
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
396
|
+
// Provider-owned completing form for a host-mediated capture slot
|
|
397
|
+
// (gentle-pi#311 P4). The provider issues the exact operation and argument
|
|
398
|
+
// tokens that submit the captured bytes; the host substitutes only the
|
|
399
|
+
// artifact location into the declared {{value}} slot and never synthesizes
|
|
400
|
+
// or filters the form itself.
|
|
401
|
+
export interface ReviewCaptureSubmissionValueV1 {
|
|
402
|
+
slot: string;
|
|
403
|
+
domain: string;
|
|
404
|
+
substitutionLocation: number;
|
|
405
|
+
/**
|
|
406
|
+
* status/v5 only: the artifact schema the substituted value must satisfy.
|
|
407
|
+
* NEW optional member carried by the singular wire `value` form the live
|
|
408
|
+
* 2.4.0-main binary emits for the materialize capture-result slot
|
|
409
|
+
* (captured 2026-08-16 from 2.4.0-main.b1afef46); the legacy `values`
|
|
410
|
+
* array rows never carry it and existing consumers stay untouched.
|
|
411
|
+
*/
|
|
412
|
+
schema?: string;
|
|
413
|
+
minimum?: number;
|
|
414
|
+
maximum?: number;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface ReviewCaptureSubmissionV1 {
|
|
418
|
+
operationToken: string;
|
|
419
|
+
argumentTokens: readonly string[];
|
|
420
|
+
values: readonly ReviewCaptureSubmissionValueV1[];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// The two Go-owned non-lens provider role capture operations (gentle-pi#311
|
|
424
|
+
// P4-roles; provider side gentle-ai#3264). Their collect inputs are
|
|
425
|
+
// self-contained authority-advancing vectors: binding tokens plus
|
|
426
|
+
// `--agent=pi --execute=true`, with NO submission descriptor. The known set
|
|
427
|
+
// is closed — an unknown role capture operation is never executed.
|
|
428
|
+
export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
|
|
429
|
+
CAPTURE_REFUTER: "review.capture-refuter",
|
|
430
|
+
CAPTURE_VALIDATION: "review.capture-validation",
|
|
431
|
+
} as const;
|
|
432
|
+
export type ReviewProviderRoleCaptureOperation = (typeof REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION)[keyof typeof REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION];
|
|
433
|
+
export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.values(REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION));
|
|
434
|
+
|
|
435
|
+
// status/v5 structural surfaces. Pi has no consumer for these yet; they are
|
|
436
|
+
// decoded strictly (exact key sets per the vendored status-v5.schema.json and
|
|
437
|
+
// correction-plan-request.schema.json on gentle-ai main) and carried through
|
|
438
|
+
// so a v5 provider payload is never rejected for being newer than v3.
|
|
439
|
+
export interface ReviewForecastStepV1 {
|
|
440
|
+
step: 1;
|
|
328
441
|
kind: "execute" | "collect" | "stop";
|
|
329
442
|
reasonCode: string;
|
|
330
|
-
|
|
331
|
-
|
|
443
|
+
description: string;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface ReviewForecastV1 {
|
|
447
|
+
horizon: "partial" | "terminal";
|
|
448
|
+
steps: readonly ReviewForecastStepV1[];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface ReviewProviderTaskV1 {
|
|
452
|
+
agent: "review-refuter" | "review-validator";
|
|
453
|
+
role: "refuter" | "targeted-validator";
|
|
454
|
+
prompt: string;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export interface ReviewCorrectionPlanFindingV1 {
|
|
458
|
+
id: string;
|
|
459
|
+
lens: "risk" | "resilience" | "readability" | "reliability";
|
|
460
|
+
location: string;
|
|
461
|
+
severity: "BLOCKER" | "CRITICAL";
|
|
462
|
+
claim: string;
|
|
463
|
+
proofRefs: readonly string[];
|
|
464
|
+
evidence: string;
|
|
465
|
+
evidenceClass: "deterministic" | "inferential";
|
|
466
|
+
causalDisposition: "introduced" | "behavior-activated" | "worsened";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface ReviewCorrectionPlanRequestV1 {
|
|
470
|
+
schema: "gentle-ai.review-correction-plan-request/v1";
|
|
471
|
+
requestHash: string;
|
|
472
|
+
lineageId: string;
|
|
473
|
+
expectedRevision: string;
|
|
474
|
+
targetIdentity: string;
|
|
475
|
+
correctionBudget: number;
|
|
476
|
+
fixFindingIds: readonly string[];
|
|
477
|
+
findings: readonly ReviewCorrectionPlanFindingV1[];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export interface ReviewTargetedValidationFindingV1 {
|
|
481
|
+
id: string;
|
|
482
|
+
lens: "risk" | "resilience" | "readability" | "reliability";
|
|
483
|
+
location: string;
|
|
484
|
+
severity: "BLOCKER" | "CRITICAL";
|
|
485
|
+
claim: string;
|
|
486
|
+
proofRefs: readonly string[];
|
|
487
|
+
evidenceClass: "deterministic" | "inferential";
|
|
488
|
+
causalDisposition: "introduced" | "behavior-activated" | "worsened";
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export interface ReviewTargetedValidationClassificationV1 {
|
|
492
|
+
findingId: string;
|
|
493
|
+
// The provider's FindingEvidence carries severity as an omitempty field, so
|
|
494
|
+
// a classification may or may not name one. Rejecting it made every
|
|
495
|
+
// targeted-validation STATUS that carried one undecodable.
|
|
496
|
+
severity?: string;
|
|
497
|
+
class: "deterministic" | "inferential";
|
|
498
|
+
causalDisposition: "introduced" | "behavior-activated" | "worsened";
|
|
499
|
+
proof: string;
|
|
332
500
|
}
|
|
333
501
|
|
|
334
502
|
export interface ReviewTargetedValidationRequestV1 {
|
|
@@ -338,6 +506,9 @@ export interface ReviewTargetedValidationRequestV1 {
|
|
|
338
506
|
expectedRevision: string;
|
|
339
507
|
targetIdentity: string;
|
|
340
508
|
fixFindingIds: readonly string[];
|
|
509
|
+
policyContent: string;
|
|
510
|
+
fixFindings: readonly ReviewTargetedValidationFindingV1[];
|
|
511
|
+
fixClassifications: readonly ReviewTargetedValidationClassificationV1[];
|
|
341
512
|
projection: ReviewProjection;
|
|
342
513
|
correctionCandidateTree: string;
|
|
343
514
|
correctionTargetIdentity: string;
|
|
@@ -345,21 +516,37 @@ export interface ReviewTargetedValidationRequestV1 {
|
|
|
345
516
|
correctionPathsDigest: string;
|
|
346
517
|
}
|
|
347
518
|
|
|
348
|
-
export interface
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
519
|
+
export interface ReviewCollectInputV3 {
|
|
520
|
+
name: string;
|
|
521
|
+
schema: string;
|
|
522
|
+
captureOperation: string;
|
|
523
|
+
arguments: readonly ReviewTransitionArgumentV3[];
|
|
524
|
+
artifactSubject?: ReviewArtifactSubjectV2;
|
|
525
|
+
baseTree?: string;
|
|
526
|
+
candidateTree?: string;
|
|
527
|
+
changedPathManifest?: readonly ChangedPathEntry[];
|
|
528
|
+
submission?: ReviewCaptureSubmissionV1;
|
|
529
|
+
/** status/v5 only: the self-contained external.run_provider_role task. */
|
|
530
|
+
providerTask?: ReviewProviderTaskV1;
|
|
531
|
+
/** status/v5 only: the provider-owned request for a Go-owned targeted validator. */
|
|
532
|
+
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export interface ReviewNextTransitionV3 {
|
|
536
|
+
kind: "execute" | "collect" | "stop";
|
|
537
|
+
reasonCode: string;
|
|
538
|
+
execute?: ReviewNextTransitionExecuteV3;
|
|
539
|
+
collect?: { inputs: readonly ReviewCollectInputV3[] };
|
|
540
|
+
/** status/v5 only: the bounded correction plan request. */
|
|
541
|
+
correctionRequest?: ReviewCorrectionPlanRequestV1;
|
|
356
542
|
}
|
|
357
543
|
|
|
358
544
|
export interface ReviewStatusV3 {
|
|
359
545
|
contract: typeof REVIEW_INTEGRATION_CONTRACT;
|
|
360
546
|
applicability: Exclude<ReviewAuthorityApplicability, "not_evaluated">;
|
|
361
547
|
authority?: ReviewStatusAuthorityV1;
|
|
362
|
-
|
|
548
|
+
/** status/v5 compatibility metadata; it is decoded but never used as routing authority. */
|
|
549
|
+
receipt?: ReviewStatusReceiptV1;
|
|
363
550
|
action: ReviewStatusAction;
|
|
364
551
|
actionDisposition?: ReviewStatusActionDisposition;
|
|
365
552
|
replayability: ReviewReplayability;
|
|
@@ -371,8 +558,17 @@ export interface ReviewStatusV3 {
|
|
|
371
558
|
repair: AuthorityRepairAssessmentV1;
|
|
372
559
|
candidates: readonly string[];
|
|
373
560
|
nextTransition?: ReviewNextTransitionV3;
|
|
561
|
+
/** status/v5 only: the descriptive, non-routing transition preview. */
|
|
562
|
+
forecast?: ReviewForecastV1;
|
|
563
|
+
/**
|
|
564
|
+
* status/v5 only: the opaque repository-context reference the live
|
|
565
|
+
* 2.4.0-main binary publishes once a reviewing lineage has bound one.
|
|
566
|
+
* NEW struct member — captured 2026-08-16 from 2.4.0-main.b1afef46; the
|
|
567
|
+
* published status-v5.schema.json omits it (capture is authoritative).
|
|
568
|
+
*/
|
|
569
|
+
repositoryContext?: ReviewRepositoryContextV2;
|
|
570
|
+
/** status/v5 only: the provider-owned request mirrored by the targeted-validator collect input. */
|
|
374
571
|
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
375
|
-
finalVerificationRetry?: ReviewFinalVerificationRetryV1;
|
|
376
572
|
raw: Readonly<Record<string, unknown>>;
|
|
377
573
|
}
|
|
378
574
|
|
|
@@ -403,6 +599,27 @@ export interface ReviewConsentV2 {
|
|
|
403
599
|
raw: Readonly<Record<string, unknown>>;
|
|
404
600
|
}
|
|
405
601
|
|
|
602
|
+
// consent/v3 (gentle-ai >= 2.3.0, capabilities/v2.1+): the same per-candidate
|
|
603
|
+
// blocking question with one net-new required member, the provider-fixed
|
|
604
|
+
// `agent` runtime binding. Everything shared with v2 keeps its exact shape so
|
|
605
|
+
// consumers of either identity read one structural surface.
|
|
606
|
+
export const REVIEW_CONSENT_AGENT_V3 = {
|
|
607
|
+
CLAUDE_CODE: "claude-code",
|
|
608
|
+
OPENCODE: "opencode",
|
|
609
|
+
CODEX: "codex",
|
|
610
|
+
PI: "pi",
|
|
611
|
+
} as const;
|
|
612
|
+
export type ReviewConsentAgentV3 = (typeof REVIEW_CONSENT_AGENT_V3)[keyof typeof REVIEW_CONSENT_AGENT_V3];
|
|
613
|
+
|
|
614
|
+
export interface ReviewConsentV3 extends Omit<ReviewConsentV2, "schema"> {
|
|
615
|
+
schema: "gentle-ai.review-integration.consent/v3";
|
|
616
|
+
agent: ReviewConsentAgentV3;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Either accepted consent identity. Consumers that relay the envelope (rather
|
|
620
|
+
// than decode it) accept both; each decoder still admits exactly one schema.
|
|
621
|
+
export type ReviewConsentEnvelope = ReviewConsentV2 | ReviewConsentV3;
|
|
622
|
+
|
|
406
623
|
const FAILURE_REQUIRED_INPUTS = [
|
|
407
624
|
"lineage_id",
|
|
408
625
|
"change",
|
|
@@ -413,12 +630,11 @@ const FAILURE_REQUIRED_INPUTS = [
|
|
|
413
630
|
"disposition",
|
|
414
631
|
"reason",
|
|
415
632
|
"actor",
|
|
416
|
-
"incident",
|
|
417
633
|
"maintainer_authorization",
|
|
418
634
|
"base_ref",
|
|
419
635
|
] as const;
|
|
420
636
|
export type ReviewFailureRequiredInputV2 = (typeof FAILURE_REQUIRED_INPUTS)[number];
|
|
421
|
-
const FAILURE_NEXT_ACTIONS = ["correct_request", "retry", "retry_with_bounded_backoff", "review.status", "review.
|
|
637
|
+
const FAILURE_NEXT_ACTIONS = ["correct_request", "retry", "retry_with_bounded_backoff", "review.status", "review.repair", "explicit-maintainer-action", "stop"] as const;
|
|
422
638
|
export type ReviewFailureNextActionV2 = (typeof FAILURE_NEXT_ACTIONS)[number];
|
|
423
639
|
// Known cause_category values: the vendored failure.schema.json enum plus
|
|
424
640
|
// "incomplete_store_entry", which the v2.1.8 emitter produces beyond that enum.
|
|
@@ -453,10 +669,12 @@ export interface ReviewFailureContextV2 {
|
|
|
453
669
|
bindingRevision?: ReviewFailureBindingRevisionV1;
|
|
454
670
|
}
|
|
455
671
|
|
|
672
|
+
export type ReviewFailureOperation = ReviewIntegrationOperation | "review.capture-result" | "review.capture-correction-plan" | "review.capture-refuter" | "review.capture-validation";
|
|
673
|
+
|
|
456
674
|
export interface ReviewFailureV2 {
|
|
457
675
|
schema: "gentle-ai.review-integration.failure/v2";
|
|
458
676
|
contract: typeof REVIEW_INTEGRATION_CONTRACT;
|
|
459
|
-
operation:
|
|
677
|
+
operation: ReviewFailureOperation;
|
|
460
678
|
phase: "preflight" | "pre_native" | "native_running" | "native_committed" | "reconciliation";
|
|
461
679
|
code: string;
|
|
462
680
|
message: string;
|
|
@@ -475,11 +693,32 @@ export interface ReviewFailureV2 {
|
|
|
475
693
|
raw: Readonly<Record<string, unknown>>;
|
|
476
694
|
}
|
|
477
695
|
|
|
478
|
-
export
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
696
|
+
export const REVIEW_ADVISORY_FINDING_SEVERITY = {
|
|
697
|
+
BLOCKER: "BLOCKER",
|
|
698
|
+
CRITICAL: "CRITICAL",
|
|
699
|
+
WARNING: "WARNING",
|
|
700
|
+
SUGGESTION: "SUGGESTION",
|
|
701
|
+
} as const;
|
|
702
|
+
export type ReviewAdvisoryFindingSeverity = (typeof REVIEW_ADVISORY_FINDING_SEVERITY)[keyof typeof REVIEW_ADVISORY_FINDING_SEVERITY];
|
|
703
|
+
|
|
704
|
+
export const REVIEW_ADVISORY_FINDING_DISPOSITION = {
|
|
705
|
+
INFORMATIONAL: "informational",
|
|
706
|
+
FOLLOW_UP: "follow_up",
|
|
707
|
+
REFUTED: "refuted",
|
|
708
|
+
} as const;
|
|
709
|
+
export type ReviewAdvisoryFindingDisposition = (typeof REVIEW_ADVISORY_FINDING_DISPOSITION)[keyof typeof REVIEW_ADVISORY_FINDING_DISPOSITION];
|
|
710
|
+
|
|
711
|
+
export interface ReviewAdvisoryFindingV1 {
|
|
712
|
+
id: string;
|
|
713
|
+
lens?: string;
|
|
714
|
+
location?: string;
|
|
715
|
+
severity: ReviewAdvisoryFindingSeverity;
|
|
716
|
+
disposition: ReviewAdvisoryFindingDisposition;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export interface ReviewAdvisoryFindingsV1 {
|
|
720
|
+
statement: string;
|
|
721
|
+
findings: readonly ReviewAdvisoryFindingV1[];
|
|
483
722
|
}
|
|
484
723
|
|
|
485
724
|
export interface AuthorityRepairAssessmentCandidateV1 {
|
|
@@ -689,10 +928,15 @@ function decodeOptionalFeature(value: unknown, label: string): { name: string; s
|
|
|
689
928
|
export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDigest: string): ReviewCapabilitiesV2 {
|
|
690
929
|
const requiredFields = ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"] as const;
|
|
691
930
|
const body = exactRecord(value, "capabilities", requiredFields, ["bootstrap"]);
|
|
692
|
-
|
|
931
|
+
// Additive minor acceptance: each accepted schema identity keeps its own
|
|
932
|
+
// exact protocol minor and required-schema floor; v2 stays exactly as it
|
|
933
|
+
// was, and an unknown identity is rejected before anything else.
|
|
934
|
+
const identity = CAPABILITIES_SCHEMA_IDENTITIES[typeof body.schema === "string" ? body.schema : ""];
|
|
935
|
+
if (identity === undefined) throw new TypeError(`schema must be one of ${Object.keys(CAPABILITIES_SCHEMA_IDENTITIES).join(", ")}`);
|
|
936
|
+
requireIdentity(body, body.schema as string);
|
|
693
937
|
|
|
694
938
|
const protocol = exactRecord(body.protocol, "capabilities.protocol", ["major", "minor"]);
|
|
695
|
-
if (protocol.major !== 2 || protocol.minor !==
|
|
939
|
+
if (protocol.major !== 2 || protocol.minor !== identity.protocolMinor) throw new TypeError("incompatible review integration protocol");
|
|
696
940
|
|
|
697
941
|
const packageIdentity = exactRecord(body.package, "capabilities.package", ["name", "version", "release_channel"]);
|
|
698
942
|
if (packageIdentity.name !== "gentle-ai") throw new TypeError("capabilities package identity mismatch");
|
|
@@ -712,20 +956,26 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
|
|
|
712
956
|
if (selfReportedDigest !== normalizedVerifiedDigest) throw new TypeError("review provider executable digest mismatch");
|
|
713
957
|
|
|
714
958
|
const advertisedOperations = stringArray(body.operations, "capabilities.operations", { minimum: REQUIRED_OPERATIONS.length, unique: true });
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
959
|
+
// Gates and projections are, like operations and schemas, a superset promise
|
|
960
|
+
// rather than an exact manifest: a compatible provider release may advertise
|
|
961
|
+
// an additional gate or projection name beyond the required floor. Decode as
|
|
962
|
+
// a plain string array (not `enumArray` against the known enum) so an
|
|
963
|
+
// unknown addition is not rejected before assertSupersetOf can even run.
|
|
964
|
+
const advertisedGates = stringArray(body.gates, "capabilities.gates", { minimum: REQUIRED_GATES.length, unique: true });
|
|
965
|
+
const advertisedProjections = stringArray(body.projections, "capabilities.projections", { minimum: REQUIRED_PROJECTIONS.length, unique: true });
|
|
966
|
+
const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: identity.requiredSchemas.length, unique: true });
|
|
718
967
|
assertSupersetOf(advertisedOperations, REQUIRED_OPERATIONS, "capabilities operations");
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
assertSupersetOf(advertisedSchemas,
|
|
968
|
+
assertSupersetOf(advertisedGates, REQUIRED_GATES, "capabilities gates");
|
|
969
|
+
assertSupersetOf(advertisedProjections, REQUIRED_PROJECTIONS, "capabilities projections");
|
|
970
|
+
assertSupersetOf(advertisedSchemas, identity.requiredSchemas, "capabilities schemas");
|
|
722
971
|
|
|
723
972
|
const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
|
|
724
|
-
const
|
|
725
|
-
const
|
|
973
|
+
const requiredMandatoryFeatures = identity.requiredMandatoryFeatures ?? REQUIRED_MANDATORY_FEATURES;
|
|
974
|
+
const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: requiredMandatoryFeatures.length });
|
|
975
|
+
const optional = array(features.optional, "capabilities.features.optional", (entry, label) => decodeOptionalFeature(entry, label), { minimum: identity.optionalFeatureFloor ?? 17, unique: true });
|
|
726
976
|
const mandatoryNames = mandatory.map((feature) => feature.name);
|
|
727
977
|
const optionalNames = optional.map((feature) => feature.name);
|
|
728
|
-
assertExactSet(mandatoryNames,
|
|
978
|
+
assertExactSet(mandatoryNames, requiredMandatoryFeatures, "mandatory capabilities");
|
|
729
979
|
if (new Set(optionalNames).size !== optionalNames.length) throw new TypeError("optional capabilities contain duplicate names");
|
|
730
980
|
if (optionalNames.some((name) => mandatoryNames.includes(name as ReviewFeatureV2["name"]))) throw new TypeError("mandatory and optional capabilities overlap");
|
|
731
981
|
if (mandatory.some((feature) => !feature.supported)) throw new TypeError("mandatory capability is unsupported");
|
|
@@ -764,9 +1014,9 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
|
|
|
764
1014
|
buildId,
|
|
765
1015
|
executableDigest: selfReportedDigest,
|
|
766
1016
|
operations: new Set(REQUIRED_OPERATIONS),
|
|
767
|
-
gates: new Set(
|
|
768
|
-
projections: new Set(
|
|
769
|
-
schemas: new Set(
|
|
1017
|
+
gates: new Set(REQUIRED_GATES),
|
|
1018
|
+
projections: new Set(REQUIRED_PROJECTIONS),
|
|
1019
|
+
schemas: new Set(identity.requiredSchemas),
|
|
770
1020
|
mandatoryFeatures: new Set(mandatoryNames),
|
|
771
1021
|
optionalFeatures: new Set(optional.filter((feature) => feature.supported && (FEATURE_NAMES as readonly string[]).includes(feature.name)).map((feature) => feature.name)),
|
|
772
1022
|
raw: body,
|
|
@@ -817,12 +1067,12 @@ function decodeChangedPathEntry(value: unknown, label: string): ChangedPathEntry
|
|
|
817
1067
|
// start/v3
|
|
818
1068
|
// ---------------------------------------------------------------------------
|
|
819
1069
|
|
|
820
|
-
export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
1070
|
+
export function decodeReviewStartV3(value: unknown, overlayIdentitySatisfiesRepositoryContext = false): ReviewStartV3 {
|
|
821
1071
|
const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] as const;
|
|
822
1072
|
const body = exactRecord(value, "start", [
|
|
823
1073
|
"schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
|
|
824
1074
|
"selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
|
|
825
|
-
], [...overlayFields, "changed_path_manifest", "repository_context"]);
|
|
1075
|
+
], [...overlayFields, "changed_path_manifest", "repository_context", "acknowledgement"]);
|
|
826
1076
|
requireIdentity(body, "gentle-ai.review-integration.start/v3", REVIEW_INTEGRATION_OPERATION.START);
|
|
827
1077
|
|
|
828
1078
|
// dependentRequired binds base_tree<->candidate_tree bidirectionally, and
|
|
@@ -851,19 +1101,24 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
|
851
1101
|
throw new TypeError("start with selected_lenses requires base_tree, candidate_tree, and changed_path_manifest");
|
|
852
1102
|
}
|
|
853
1103
|
|
|
854
|
-
const
|
|
1104
|
+
const reviewingCreatedOrResumed = (action === "created" || action === "resumed") && state === REVIEW_START_STATE.REVIEWING;
|
|
1105
|
+
// START/v4 may render the frozen target as the overlay identity instead of
|
|
1106
|
+
// repository_context; absence is only excused when that identity is present.
|
|
1107
|
+
const requiresRepositoryContext = reviewingCreatedOrResumed && !(overlayIdentitySatisfiesRepositoryContext && targetIdentity !== undefined);
|
|
855
1108
|
if (requiresRepositoryContext && body.repository_context === undefined) throw new TypeError("start.repository_context is required when action is created/resumed and state is reviewing");
|
|
856
|
-
if (!
|
|
1109
|
+
if (!reviewingCreatedOrResumed && body.repository_context !== undefined) throw new TypeError("start.repository_context is only valid when action is created/resumed and state is reviewing");
|
|
857
1110
|
|
|
858
1111
|
let repositoryContext: ReviewRepositoryContextV2 | undefined;
|
|
859
1112
|
if (body.repository_context !== undefined) {
|
|
860
|
-
const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"]);
|
|
1113
|
+
const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
|
|
861
1114
|
if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
|
|
862
1115
|
repositoryContext = {
|
|
863
1116
|
capability: "review.opaque_repository_context",
|
|
864
|
-
handle: text(source.handle, "start.repository_context.handle", { pattern: /^
|
|
1117
|
+
handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx[12]_[0-9a-f]{64}$/ }),
|
|
865
1118
|
revision: sha256(source.revision, "start.repository_context.revision"),
|
|
866
1119
|
targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
|
|
1120
|
+
...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "start.repository_context.event_id") }),
|
|
1121
|
+
...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "start.repository_context.outcome") }),
|
|
867
1122
|
};
|
|
868
1123
|
}
|
|
869
1124
|
|
|
@@ -904,6 +1159,78 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
|
904
1159
|
};
|
|
905
1160
|
}
|
|
906
1161
|
|
|
1162
|
+
function assertReviewStartV4StatusBinding(execute: ReviewNextTransitionExecuteV3, start: ReviewStartV3): void {
|
|
1163
|
+
const expectedTargetIdentity = start.repositoryContext?.targetIdentity ?? start.targetIdentity;
|
|
1164
|
+
const targetIdentityConflicts = expectedTargetIdentity === undefined || execute.binding.targetIdentity !== expectedTargetIdentity
|
|
1165
|
+
|| (start.targetIdentity !== undefined && start.targetIdentity !== expectedTargetIdentity)
|
|
1166
|
+
|| start.artifactSubjects.some((subject) => subject.targetIdentity !== expectedTargetIdentity);
|
|
1167
|
+
if (targetIdentityConflicts) throw new TypeError("START/v4 status target identity binding conflicts with frozen START");
|
|
1168
|
+
if (execute.binding.lineageId !== undefined && execute.binding.lineageId !== start.lineageId) {
|
|
1169
|
+
throw new TypeError("START/v4 status lineage binding conflicts with frozen START");
|
|
1170
|
+
}
|
|
1171
|
+
const optionalSelectors: Readonly<Record<string, string | undefined>> = {
|
|
1172
|
+
"base-ref": start.baseTree,
|
|
1173
|
+
"base-tree": start.baseTree,
|
|
1174
|
+
projection: start.projection,
|
|
1175
|
+
target: expectedTargetIdentity,
|
|
1176
|
+
};
|
|
1177
|
+
const argumentsByName = new Map<string, string>();
|
|
1178
|
+
for (const argument of execute.arguments) {
|
|
1179
|
+
if (argumentsByName.has(argument.name)) throw new TypeError(`START/v4 status arguments duplicate ${argument.name} binding`);
|
|
1180
|
+
argumentsByName.set(argument.name, argument.value);
|
|
1181
|
+
if (Object.hasOwn(optionalSelectors, argument.name) && argument.value !== optionalSelectors[argument.name]) throw new TypeError(`START/v4 status ${argument.name} binding conflicts with frozen START`);
|
|
1182
|
+
}
|
|
1183
|
+
if (argumentsByName.get("lineage") !== start.lineageId) throw new TypeError("START/v4 status requires exactly one lineage binding for the frozen START");
|
|
1184
|
+
if (execute.arguments.find((argument) => argument.name === "lineage")?.token !== `--lineage=${start.lineageId}`) throw new TypeError("START/v4 status lineage binding conflicts with frozen START");
|
|
1185
|
+
for (const [name, value] of [["contract", REVIEW_INTEGRATION_CONTRACT], ["next-transition", "true"], ["agent", "pi"]]) {
|
|
1186
|
+
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`);
|
|
1187
|
+
}
|
|
1188
|
+
const selectorArguments = execute.selectorArguments;
|
|
1189
|
+
if (selectorArguments === undefined || selectorArguments.length === 0) throw new TypeError("START/v4 status selector arguments are required");
|
|
1190
|
+
const selectorsByName = new Map<string, ReviewTransitionArgumentV3>();
|
|
1191
|
+
for (const selector of selectorArguments) {
|
|
1192
|
+
const full = execute.arguments.find((argument) => argument.name === selector.name);
|
|
1193
|
+
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`);
|
|
1194
|
+
selectorsByName.set(selector.name, selector);
|
|
1195
|
+
}
|
|
1196
|
+
const selector = (name: string, value: string): void => {
|
|
1197
|
+
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`);
|
|
1198
|
+
};
|
|
1199
|
+
const baseRef = argumentsByName.get("base-ref");
|
|
1200
|
+
if (start.targetMode === "base-workspace-overlay") {
|
|
1201
|
+
if (baseRef !== start.baseTree || argumentsByName.has("committed-only")) throw new TypeError("START/v4 status workspace overlay binding conflicts with frozen START");
|
|
1202
|
+
selector("base-ref", start.baseTree!); selector("workspace-overlay", "true");
|
|
1203
|
+
} else if (baseRef !== undefined) {
|
|
1204
|
+
if (baseRef !== start.baseTree || argumentsByName.has("workspace-overlay")) throw new TypeError("START/v4 status committed range binding conflicts with frozen START");
|
|
1205
|
+
selector("base-ref", start.baseTree!); selector("committed-only", "true");
|
|
1206
|
+
} else {
|
|
1207
|
+
if (argumentsByName.has("committed-only") || argumentsByName.has("workspace-overlay")) throw new TypeError("START/v4 status current projection binding conflicts with frozen START");
|
|
1208
|
+
selector("projection", start.projection);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
export function decodeReviewStartV4(value: unknown): ReviewStartV4 {
|
|
1213
|
+
const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] as const;
|
|
1214
|
+
const body = exactRecord(value, "start", [
|
|
1215
|
+
"schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
|
|
1216
|
+
"selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
|
|
1217
|
+
], [...overlayFields, "changed_path_manifest", "repository_context", "acknowledgement", "next_transition"]);
|
|
1218
|
+
requireIdentity(body, "gentle-ai.review-integration.start/v4", REVIEW_INTEGRATION_OPERATION.START);
|
|
1219
|
+
const action = enumeration(body.action, ["created", "replayed", "closed", "blocked-scope-action"] as const, "start.action");
|
|
1220
|
+
const v3Action = action === "replayed" ? "resumed" : action;
|
|
1221
|
+
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
|
|
1222
|
+
const reviewing = (action === "created" || action === "replayed") && body.state === REVIEW_START_STATE.REVIEWING;
|
|
1223
|
+
if (reviewing && nextTransition?.kind !== "execute") throw new TypeError("reviewing created/replayed START requires next_transition.execute");
|
|
1224
|
+
if (reviewing && nextTransition.execute?.operation !== "review.status") throw new TypeError("reviewing created/replayed START next_transition.execute.operation must be review.status");
|
|
1225
|
+
const closedApprovedZeroLens = action === "closed" && body.state === REVIEW_START_STATE.APPROVED && Array.isArray(body.selected_lenses) && body.selected_lenses.length === 0;
|
|
1226
|
+
if (closedApprovedZeroLens && nextTransition !== undefined) throw new TypeError("closed approved zero-lens START cannot carry next_transition");
|
|
1227
|
+
const v3Body = { ...body };
|
|
1228
|
+
delete v3Body.next_transition;
|
|
1229
|
+
const decoded = decodeReviewStartV3({ ...v3Body, schema: "gentle-ai.review-integration.start/v3", action: v3Action }, true);
|
|
1230
|
+
if (reviewing) assertReviewStartV4StatusBinding(nextTransition!.execute!, decoded);
|
|
1231
|
+
return { ...decoded, action, ...(nextTransition === undefined ? {} : { nextTransition }), raw: body };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
907
1234
|
// ---------------------------------------------------------------------------
|
|
908
1235
|
// projection/v1 — reused verbatim; the v2 capabilities schema still advertises
|
|
909
1236
|
// gentle-ai.review-integration.projection/v1, so renaming this decoder would
|
|
@@ -1002,20 +1329,175 @@ export function decodeAuthorityRepairAssessmentV1(value: unknown): AuthorityRepa
|
|
|
1002
1329
|
}
|
|
1003
1330
|
|
|
1004
1331
|
// ---------------------------------------------------------------------------
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1332
|
+
// next-transition/v3 — net-new; unlike v1's decodeNextTransition (void),
|
|
1333
|
+
// this decoder returns a typed value so callers can read the manifest-bound
|
|
1334
|
+
// collect inputs and the execute binding.
|
|
1007
1335
|
// ---------------------------------------------------------------------------
|
|
1008
1336
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1337
|
+
const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.status", "review.recover", "review.repair", "review.acknowledge-approved"] as const;
|
|
1338
|
+
|
|
1339
|
+
function decodeTransitionArguments(value: unknown, label: string): readonly ReviewTransitionArgumentV3[] {
|
|
1340
|
+
return array(value, label, (entry, entryLabel) => {
|
|
1341
|
+
const argument = exactRecord(entry, entryLabel, ["name", "value"], ["token"]);
|
|
1342
|
+
const name = text(argument.name, `${entryLabel}.name`, { minimum: 1, pattern: /^[a-z0-9_-]+$/ });
|
|
1343
|
+
const argumentValue = text(argument.value, `${entryLabel}.value`, { minimum: 1 });
|
|
1344
|
+
const token = argument.token === undefined ? undefined : text(argument.token, `${entryLabel}.token`, { minimum: 1 });
|
|
1345
|
+
return { name, value: argumentValue, ...(token === undefined ? {} : { token }) };
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function decodeCaptureSubmission(value: unknown, label: string, v5: boolean, v6: boolean, captureOperation: string): ReviewCaptureSubmissionV1 {
|
|
1350
|
+
// status/v6 adds the provider-owned intended-untracked selection form.
|
|
1351
|
+
if (v6 && captureOperation === "external.select_intended_untracked" && typeof value === "object" && value !== null && (value as Record<string, unknown>).operation_token === "status") {
|
|
1352
|
+
const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
|
|
1353
|
+
const expectedTokens = [
|
|
1354
|
+
"--contract=gentle-ai.review-integration/v2", "--next-transition=true", "--agent=pi",
|
|
1355
|
+
"--projection=workspace", "--intended-untracked-selection={{value}}",
|
|
1356
|
+
] as const;
|
|
1357
|
+
const operationToken = enumeration(submission.operation_token, ["status"] as const, `${label}.operation_token`);
|
|
1358
|
+
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: expectedTokens.length, maximum: expectedTokens.length });
|
|
1359
|
+
if (argumentTokens.some((token, index) => token !== expectedTokens[index])) throw new TypeError(`${label}.argument_tokens substitution binding is invalid`);
|
|
1360
|
+
const row = exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"]);
|
|
1361
|
+
return {
|
|
1362
|
+
operationToken,
|
|
1363
|
+
argumentTokens,
|
|
1364
|
+
values: [{
|
|
1365
|
+
slot: enumeration(row.slot, ["intended_untracked_selection"] as const, `${label}.value.slot`),
|
|
1366
|
+
domain: enumeration(row.domain, ["schema_bound_json"] as const, `${label}.value.domain`),
|
|
1367
|
+
schema: enumeration(row.schema, ["gentle-ai.review-intended-untracked-selection/v1"] as const, `${label}.value.schema`),
|
|
1368
|
+
substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 4, 4),
|
|
1369
|
+
}],
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
// status/v5: the live 2.4.0-main binary emits the materialize
|
|
1373
|
+
// capture-result submission as a SINGULAR `value` object carrying a
|
|
1374
|
+
// `schema` key (captured 2026-08-16 from 2.4.0-main.b1afef46; the
|
|
1375
|
+
// emitter has been singular since gentle-ai f1a95179). The form is
|
|
1376
|
+
// closed to that exact captured shape and normalizes into the typed
|
|
1377
|
+
// one-entry values array the host relay already consumes. A payload
|
|
1378
|
+
// carrying both wire forms at once matches no captured shape and falls
|
|
1379
|
+
// through to the legacy decoder, which rejects the unknown `value` key.
|
|
1380
|
+
if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
|
|
1381
|
+
const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
|
|
1382
|
+
const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan"] as const, `${label}.operation_token`);
|
|
1383
|
+
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
|
|
1384
|
+
const row = operationToken === "capture-result"
|
|
1385
|
+
? exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"])
|
|
1386
|
+
: exactRecord(submission.value, `${label}.value`, ["slot", "domain", "minimum", "maximum", "substitution_location"]);
|
|
1387
|
+
return {
|
|
1388
|
+
operationToken,
|
|
1389
|
+
argumentTokens,
|
|
1390
|
+
values: [{
|
|
1391
|
+
slot: operationToken === "capture-result"
|
|
1392
|
+
? enumeration(row.slot, ["reviewer_result"] as const, `${label}.value.slot`)
|
|
1393
|
+
: enumeration(row.slot, ["correction_lines"] as const, `${label}.value.slot`),
|
|
1394
|
+
domain: nonempty(row.domain, `${label}.value.domain`),
|
|
1395
|
+
...(row.schema === undefined ? {} : { schema: nonempty(row.schema, `${label}.value.schema`) }),
|
|
1396
|
+
...(row.minimum === undefined ? {} : { minimum: integer(row.minimum, `${label}.value.minimum`, 1, 200) }),
|
|
1397
|
+
...(row.maximum === undefined ? {} : { maximum: integer(row.maximum, `${label}.value.maximum`, 1, 200) }),
|
|
1398
|
+
substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
|
|
1399
|
+
}],
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
|
|
1403
|
+
const operationToken = text(submission.operation_token, `${label}.operation_token`, { minimum: 1, pattern: /^[a-z0-9-]+$/ });
|
|
1404
|
+
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
|
|
1405
|
+
const values = array(submission.values, `${label}.values`, (entry, entryLabel) => {
|
|
1406
|
+
const row = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"]);
|
|
1407
|
+
return {
|
|
1408
|
+
slot: nonempty(row.slot, `${entryLabel}.slot`),
|
|
1409
|
+
domain: nonempty(row.domain, `${entryLabel}.domain`),
|
|
1410
|
+
substitutionLocation: integer(row.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
|
|
1411
|
+
};
|
|
1412
|
+
}, { minimum: 1 });
|
|
1413
|
+
return { operationToken, argumentTokens, values };
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// v5-only structural decoders, exact to the vendored status-v5.schema.json
|
|
1417
|
+
// and correction-plan-request.schema.json on gentle-ai main.
|
|
1418
|
+
|
|
1419
|
+
function decodeProviderTask(value: unknown, label: string): ReviewProviderTaskV1 {
|
|
1420
|
+
const task = exactRecord(value, label, ["agent", "role", "prompt"]);
|
|
1421
|
+
return {
|
|
1422
|
+
agent: enumeration(task.agent, ["review-refuter", "review-validator"] as const, `${label}.agent`),
|
|
1423
|
+
role: enumeration(task.role, ["refuter", "targeted-validator"] as const, `${label}.role`),
|
|
1424
|
+
prompt: nonempty(task.prompt, `${label}.prompt`),
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function decodeCorrectionPlanRequestV1(value: unknown, label: string): ReviewCorrectionPlanRequestV1 {
|
|
1429
|
+
const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "correction_budget", "fix_finding_ids", "findings"]);
|
|
1430
|
+
if (body.schema !== "gentle-ai.review-correction-plan-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan-request/v1`);
|
|
1431
|
+
return {
|
|
1432
|
+
schema: "gentle-ai.review-correction-plan-request/v1",
|
|
1433
|
+
requestHash: sha256(body.request_hash, `${label}.request_hash`),
|
|
1434
|
+
lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
|
|
1435
|
+
expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
|
|
1436
|
+
targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
|
|
1437
|
+
correctionBudget: integer(body.correction_budget, `${label}.correction_budget`, 1, 200),
|
|
1438
|
+
fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
|
|
1439
|
+
findings: array(body.findings, `${label}.findings`, (entry, entryLabel) => {
|
|
1440
|
+
const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence", "evidence_class", "causal_disposition"]);
|
|
1441
|
+
return {
|
|
1442
|
+
id: nonempty(finding.id, `${entryLabel}.id`),
|
|
1443
|
+
lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] as const, `${entryLabel}.lens`),
|
|
1444
|
+
location: nonempty(finding.location, `${entryLabel}.location`),
|
|
1445
|
+
severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] as const, `${entryLabel}.severity`),
|
|
1446
|
+
claim: nonempty(finding.claim, `${entryLabel}.claim`),
|
|
1447
|
+
proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
|
|
1448
|
+
evidence: nonempty(finding.evidence, `${entryLabel}.evidence`),
|
|
1449
|
+
evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] as const, `${entryLabel}.evidence_class`),
|
|
1450
|
+
causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
|
|
1451
|
+
};
|
|
1452
|
+
}, { minimum: 1 }),
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
export function decodeReviewTargetedValidationRequestV1(value: unknown, label = "targeted_validation_request"): ReviewTargetedValidationRequestV1 {
|
|
1457
|
+
const body = exactRecord(value, label, [
|
|
1458
|
+
"schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "fix_finding_ids", "policy_content", "fix_findings", "fix_classifications",
|
|
1459
|
+
"projection", "correction_candidate_tree", "correction_target_identity", "correction_paths", "correction_paths_digest",
|
|
1460
|
+
]);
|
|
1011
1461
|
if (body.schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
|
|
1462
|
+
const fixFindingIds = stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true });
|
|
1463
|
+
const fixFindings = array(body.fix_findings, `${label}.fix_findings`, (entry, entryLabel): ReviewTargetedValidationFindingV1 => {
|
|
1464
|
+
const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence_class", "causal_disposition"]);
|
|
1465
|
+
return {
|
|
1466
|
+
id: nonempty(finding.id, `${entryLabel}.id`),
|
|
1467
|
+
lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] as const, `${entryLabel}.lens`),
|
|
1468
|
+
location: nonempty(finding.location, `${entryLabel}.location`),
|
|
1469
|
+
severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] as const, `${entryLabel}.severity`),
|
|
1470
|
+
claim: nonempty(finding.claim, `${entryLabel}.claim`),
|
|
1471
|
+
proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
|
|
1472
|
+
evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] as const, `${entryLabel}.evidence_class`),
|
|
1473
|
+
causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
|
|
1474
|
+
};
|
|
1475
|
+
}, { minimum: 1 });
|
|
1476
|
+
const fixClassifications = array(body.fix_classifications, `${label}.fix_classifications`, (entry, entryLabel): ReviewTargetedValidationClassificationV1 => {
|
|
1477
|
+
const classification = exactRecord(entry, entryLabel, ["finding_id", "class", "causal_disposition", "proof"], ["severity"]);
|
|
1478
|
+
const severity = classification.severity === undefined ? undefined : nonempty(classification.severity, `${entryLabel}.severity`);
|
|
1479
|
+
return {
|
|
1480
|
+
findingId: nonempty(classification.finding_id, `${entryLabel}.finding_id`),
|
|
1481
|
+
...(severity === undefined ? {} : { severity }),
|
|
1482
|
+
class: enumeration(classification.class, ["deterministic", "inferential"] as const, `${entryLabel}.class`),
|
|
1483
|
+
causalDisposition: enumeration(classification.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
|
|
1484
|
+
proof: nonempty(classification.proof, `${entryLabel}.proof`),
|
|
1485
|
+
};
|
|
1486
|
+
}, { minimum: 1 });
|
|
1487
|
+
const findingIds = fixFindings.map((finding) => finding.id);
|
|
1488
|
+
const classificationFindingIds = fixClassifications.map((classification) => classification.findingId);
|
|
1489
|
+
assertExactSet(findingIds, fixFindingIds, `${label}.fix_findings`);
|
|
1490
|
+
assertExactSet(classificationFindingIds, fixFindingIds, `${label}.fix_classifications`);
|
|
1012
1491
|
return {
|
|
1013
1492
|
schema: "gentle-ai.review-targeted-validation-request/v1",
|
|
1014
1493
|
requestHash: sha256(body.request_hash, `${label}.request_hash`),
|
|
1015
1494
|
lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
|
|
1016
1495
|
expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
|
|
1017
1496
|
targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
|
|
1018
|
-
fixFindingIds
|
|
1497
|
+
fixFindingIds,
|
|
1498
|
+
policyContent: nonempty(body.policy_content, `${label}.policy_content`),
|
|
1499
|
+
fixFindings,
|
|
1500
|
+
fixClassifications,
|
|
1019
1501
|
projection: enumeration(body.projection, REQUIRED_PROJECTIONS, `${label}.projection`),
|
|
1020
1502
|
correctionCandidateTree: gitTree(body.correction_candidate_tree, `${label}.correction_candidate_tree`),
|
|
1021
1503
|
correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
|
|
@@ -1024,36 +1506,121 @@ function decodeTargetedValidationRequestV1(value: unknown, label: string): Revie
|
|
|
1024
1506
|
};
|
|
1025
1507
|
}
|
|
1026
1508
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
});
|
|
1509
|
+
export function decodeReviewForecastV1(value: unknown, label = "status.forecast"): ReviewForecastV1 {
|
|
1510
|
+
const body = exactRecord(value, label, ["horizon", "steps"]);
|
|
1511
|
+
const horizon = enumeration(body.horizon, ["partial", "terminal"] as const, `${label}.horizon`);
|
|
1512
|
+
const steps = array(body.steps, `${label}.steps`, (entry, entryLabel) => {
|
|
1513
|
+
const step = exactRecord(entry, entryLabel, ["step", "kind", "reason_code", "description"]);
|
|
1514
|
+
if (step.step !== 1) throw new TypeError(`${entryLabel}.step must be 1`);
|
|
1515
|
+
return {
|
|
1516
|
+
step: 1 as const,
|
|
1517
|
+
kind: enumeration(step.kind, ["execute", "collect", "stop"] as const, `${entryLabel}.kind`),
|
|
1518
|
+
reasonCode: text(step.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ }),
|
|
1519
|
+
description: nonempty(step.description, `${entryLabel}.description`),
|
|
1520
|
+
};
|
|
1521
|
+
}, { minimum: 1, maximum: 1 });
|
|
1522
|
+
// The published horizon-to-step invariant: a stop head is terminal, any
|
|
1523
|
+
// other head is partial.
|
|
1524
|
+
if ((horizon === "terminal") !== (steps[0]!.kind === "stop")) throw new TypeError(`${label}.horizon does not match its step kind`);
|
|
1525
|
+
return { horizon, steps };
|
|
1043
1526
|
}
|
|
1044
1527
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1528
|
+
// The two v5 reason codes whose transitions must carry the correction plan
|
|
1529
|
+
// request; every other reason code must not.
|
|
1530
|
+
const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required", "corrected_candidate_unavailable"] as const);
|
|
1531
|
+
// v5 capture operations that must carry a submission descriptor.
|
|
1532
|
+
const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["review.capture-correction-plan"] as const);
|
|
1533
|
+
|
|
1534
|
+
function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: boolean): ReviewCollectInputV3 {
|
|
1535
|
+
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"] : [])]);
|
|
1047
1536
|
const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1048
1537
|
const schema = nonempty(input.schema, `${label}.schema`);
|
|
1049
1538
|
const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
|
|
1050
1539
|
const argumentsList = decodeTransitionArguments(input.arguments, `${label}.arguments`);
|
|
1051
1540
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1541
|
+
// v5-only surfaces (vendored status-v5.schema.json): the provider role task
|
|
1542
|
+
// rides exactly the external.run_provider_role vector, and the finalize/
|
|
1543
|
+
// capture-evidence submission descriptor forms ride their own vectors.
|
|
1544
|
+
if (v5) {
|
|
1545
|
+
if (captureOperation === "external.run_provider_role") {
|
|
1546
|
+
if (input.provider_task === undefined) throw new TypeError(`${label}.provider_task is required for external.run_provider_role`);
|
|
1547
|
+
} else if (input.provider_task !== undefined) {
|
|
1548
|
+
throw new TypeError(`${label}.provider_task is only valid for external.run_provider_role`);
|
|
1549
|
+
}
|
|
1550
|
+
if ((V5_SUBMISSION_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
|
|
1551
|
+
if (input.submission === undefined) throw new TypeError(`${label}.submission is required for ${captureOperation}`);
|
|
1552
|
+
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`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
const intendedUntracked = v6
|
|
1557
|
+
&& name === "intended_untracked_selection"
|
|
1558
|
+
&& schema === "gentle-ai.review-intended-untracked-selection/v1"
|
|
1559
|
+
&& captureOperation === "external.select_intended_untracked";
|
|
1560
|
+
if (captureOperation === "external.select_intended_untracked" && (name !== "intended_untracked_selection" || schema !== "gentle-ai.review-intended-untracked-selection/v1")) {
|
|
1561
|
+
throw new TypeError(`${label} external.select_intended_untracked requires the intended_untracked_selection binding`);
|
|
1562
|
+
}
|
|
1563
|
+
if (intendedUntracked) {
|
|
1564
|
+
const expectedNames = ["target_identity", "projection", "base_tree", "candidate_tree", "eligible_paths_json", "expected_untracked_inventory"] as const;
|
|
1565
|
+
if (argumentsList.length !== expectedNames.length || argumentsList.some((argument, index) => argument.name !== expectedNames[index] || argument.token !== undefined)) {
|
|
1566
|
+
throw new TypeError(`${label}.arguments must preserve the exact intended-untracked metadata binding`);
|
|
1567
|
+
}
|
|
1568
|
+
sha256(argumentsList[0]!.value, `${label}.arguments[0].value`);
|
|
1569
|
+
enumeration(argumentsList[1]!.value, ["workspace"] as const, `${label}.arguments[1].value`);
|
|
1570
|
+
gitTree(argumentsList[2]!.value, `${label}.arguments[2].value`);
|
|
1571
|
+
gitTree(argumentsList[3]!.value, `${label}.arguments[3].value`);
|
|
1572
|
+
const eligiblePathsJson = argumentsList[4]!.value;
|
|
1573
|
+
let eligiblePaths: unknown;
|
|
1574
|
+
try { eligiblePaths = JSON.parse(eligiblePathsJson); } catch { throw new TypeError(`${label}.arguments[4].value must be canonical JSON`); }
|
|
1575
|
+
if (!Array.isArray(eligiblePaths) || eligiblePaths.length === 0 || eligiblePaths.some((path) => {
|
|
1576
|
+
if (typeof path !== "string" || path.length === 0 || path.startsWith("/") || /^[A-Za-z]:\//.test(path) || path.includes("\\") || path.includes("\0")) return true;
|
|
1577
|
+
const segments = path.split("/");
|
|
1578
|
+
return segments.some((segment) => segment === "" || segment === "." || segment === "..");
|
|
1579
|
+
}) || new Set(eligiblePaths).size !== eligiblePaths.length || JSON.stringify(eligiblePaths) !== eligiblePathsJson) {
|
|
1580
|
+
throw new TypeError(`${label}.arguments[4].value must contain unique canonical repository-relative paths`);
|
|
1581
|
+
}
|
|
1582
|
+
sha256(argumentsList[5]!.value, `${label}.arguments[5].value`);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
|
|
1586
|
+
// operations render SELF-CONTAINED authority-advancing vectors. Executing
|
|
1587
|
+
// the exact rendered tokens makes Go materialize the role prompt, run its
|
|
1588
|
+
// own locked-down pi subprocess, and admit the raw verdict — so a
|
|
1589
|
+
// submission descriptor (the host-mediated completing form) on one of
|
|
1590
|
+
// these inputs would hand the caller a way to author the verdict and is
|
|
1591
|
+
// rejected as a provider contract violation.
|
|
1592
|
+
if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
|
|
1593
|
+
throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
|
|
1594
|
+
}
|
|
1595
|
+
if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION && schema !== "https://gentle-ai.dev/schema/review/validator/v1") {
|
|
1596
|
+
throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/validator/v1`);
|
|
1597
|
+
}
|
|
1598
|
+
const targetedValidatorInput = v5
|
|
1599
|
+
&& name === "provider_targeted_validator"
|
|
1600
|
+
&& captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION
|
|
1601
|
+
&& schema === "https://gentle-ai.dev/schema/review/validator/v1";
|
|
1602
|
+
if (input.validation_request !== undefined && !targetedValidatorInput) {
|
|
1603
|
+
throw new TypeError(`${label}.validation_request is only valid for the v5 provider-targeted-validator capture input`);
|
|
1604
|
+
}
|
|
1605
|
+
if (targetedValidatorInput && input.validation_request === undefined) {
|
|
1606
|
+
throw new TypeError(`${label}.validation_request is required for the v5 provider-targeted-validator capture input`);
|
|
1607
|
+
}
|
|
1608
|
+
const validationRequest = input.validation_request === undefined
|
|
1609
|
+
? undefined
|
|
1610
|
+
: decodeReviewTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`);
|
|
1611
|
+
if (validationRequest !== undefined) {
|
|
1612
|
+
const providerArgument = (argumentName: string): string => {
|
|
1613
|
+
const matches = argumentsList.filter((argument) => argument.name === argumentName);
|
|
1614
|
+
if (matches.length !== 1) throw new TypeError(`${label}.arguments must carry exactly one ${argumentName} binding`);
|
|
1615
|
+
return matches[0]!.value;
|
|
1616
|
+
};
|
|
1617
|
+
if (providerArgument("lineage") !== validationRequest.lineageId) throw new TypeError(`${label}.arguments lineage must bind validation_request.lineage_id`);
|
|
1618
|
+
if (providerArgument("expected-revision") !== validationRequest.expectedRevision) throw new TypeError(`${label}.arguments expected-revision must bind validation_request.expected_revision`);
|
|
1619
|
+
if (providerArgument("target") !== validationRequest.correctionTargetIdentity) throw new TypeError(`${label}.arguments target must bind validation_request.correction_target_identity`);
|
|
1620
|
+
if (providerArgument("request-hash") !== validationRequest.requestHash) throw new TypeError(`${label}.arguments request-hash must bind validation_request.request_hash`);
|
|
1621
|
+
}
|
|
1622
|
+
if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
|
|
1623
|
+
throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
|
|
1057
1624
|
}
|
|
1058
1625
|
|
|
1059
1626
|
if (captureOperation === "review.capture-result") {
|
|
@@ -1064,6 +1631,17 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
|
|
|
1064
1631
|
} else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
|
|
1065
1632
|
throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
|
|
1066
1633
|
}
|
|
1634
|
+
const submissionOperations: readonly string[] = v5
|
|
1635
|
+
? ["review.capture-result", "review.capture-correction-plan", ...(v6 ? ["external.select_intended_untracked"] : [])]
|
|
1636
|
+
: ["review.capture-result"];
|
|
1637
|
+
if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
|
|
1638
|
+
throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
|
|
1639
|
+
}
|
|
1640
|
+
if (intendedUntracked && input.submission === undefined) throw new TypeError(`${label}.submission is required for external.select_intended_untracked`);
|
|
1641
|
+
|
|
1642
|
+
const submission = input.submission === undefined
|
|
1643
|
+
? undefined
|
|
1644
|
+
: decodeCaptureSubmission(input.submission, `${label}.submission`, v5, v6, captureOperation);
|
|
1067
1645
|
|
|
1068
1646
|
return {
|
|
1069
1647
|
name,
|
|
@@ -1074,15 +1652,29 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
|
|
|
1074
1652
|
...(input.base_tree === undefined ? {} : { baseTree: gitTree(input.base_tree, `${label}.base_tree`) }),
|
|
1075
1653
|
...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
|
|
1076
1654
|
...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
|
|
1077
|
-
...(
|
|
1655
|
+
...(submission === undefined ? {} : { submission }),
|
|
1656
|
+
...(v5 && input.provider_task !== undefined ? { providerTask: decodeProviderTask(input.provider_task, `${label}.provider_task`) } : {}),
|
|
1657
|
+
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
1078
1658
|
};
|
|
1079
1659
|
}
|
|
1080
1660
|
|
|
1081
|
-
export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransitionV3 {
|
|
1082
|
-
const
|
|
1661
|
+
export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean; v6?: boolean } = {}): ReviewNextTransitionV3 {
|
|
1662
|
+
const v6 = options.v6 === true;
|
|
1663
|
+
const v5 = options.v5 === true || v6;
|
|
1664
|
+
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
|
|
1083
1665
|
const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
|
|
1084
1666
|
const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1085
1667
|
|
|
1668
|
+
// status/v5: the bounded correction plan request rides exactly its two
|
|
1669
|
+
// reason codes and never any other (vendored status-v5.schema.json).
|
|
1670
|
+
let correctionRequest: ReviewCorrectionPlanRequestV1 | undefined;
|
|
1671
|
+
if (v5) {
|
|
1672
|
+
const required = (CORRECTION_REQUEST_REASON_CODES as readonly string[]).includes(reasonCode);
|
|
1673
|
+
if (required && transition.correction_request === undefined) throw new TypeError(`next_transition.correction_request is required for ${reasonCode}`);
|
|
1674
|
+
if (!required && transition.correction_request !== undefined) throw new TypeError(`next_transition.correction_request is only valid for ${CORRECTION_REQUEST_REASON_CODES.join(", ")}`);
|
|
1675
|
+
if (transition.correction_request !== undefined) correctionRequest = decodeCorrectionPlanRequestV1(transition.correction_request, "next_transition.correction_request");
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1086
1678
|
if (kind === "execute") {
|
|
1087
1679
|
// `command` is an optional, ready-to-paste rendering of `arguments` (the
|
|
1088
1680
|
// exact same binding as a single shell-ready string) — observed live
|
|
@@ -1098,6 +1690,7 @@ export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransiti
|
|
|
1098
1690
|
const execute = exactRecord(transition.execute, "next_transition.execute", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
|
|
1099
1691
|
const operation = enumeration(execute.operation, NEXT_TRANSITION_OPERATIONS, "next_transition.execute.operation");
|
|
1100
1692
|
const argumentsList = decodeTransitionArguments(execute.arguments, "next_transition.execute.arguments");
|
|
1693
|
+
const selectorArguments = execute.selector_arguments === undefined ? undefined : decodeTransitionArguments(execute.selector_arguments, "next_transition.execute.selector_arguments");
|
|
1101
1694
|
const preconditions = decodeTransitionArguments(execute.preconditions, "next_transition.execute.preconditions");
|
|
1102
1695
|
// The schema gives `binding` no declared properties and no required list:
|
|
1103
1696
|
// it is an OPEN object. Closing it here made Pi stricter than the
|
|
@@ -1108,17 +1701,19 @@ export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransiti
|
|
|
1108
1701
|
const lineageId = binding.lineage_id === undefined ? undefined : lineage(binding.lineage_id, "next_transition.execute.binding.lineage_id");
|
|
1109
1702
|
const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
|
|
1110
1703
|
const command = execute.command === undefined ? undefined : nonempty(execute.command, "next_transition.execute.command");
|
|
1704
|
+
const decodedExecute: ReviewNextTransitionExecuteV3 = { operation, arguments: argumentsList, ...(selectorArguments === undefined ? {} : { selectorArguments }), preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) };
|
|
1705
|
+
if (operation === "review.acknowledge-approved") assertReviewApprovedAcknowledgementExecuteV1(decodedExecute);
|
|
1111
1706
|
if (transition.collect !== undefined) throw new TypeError("next_transition.collect is incompatible with execute");
|
|
1112
|
-
return { kind, reasonCode, execute:
|
|
1707
|
+
return { kind, reasonCode, execute: decodedExecute, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1113
1708
|
}
|
|
1114
1709
|
if (kind === "collect") {
|
|
1115
1710
|
const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
|
|
1116
|
-
const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label), { minimum: 1 });
|
|
1711
|
+
const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6), { minimum: 1 });
|
|
1117
1712
|
if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
|
|
1118
|
-
return { kind, reasonCode, collect: { inputs } };
|
|
1713
|
+
return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1119
1714
|
}
|
|
1120
1715
|
if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
|
|
1121
|
-
return { kind, reasonCode };
|
|
1716
|
+
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1122
1717
|
}
|
|
1123
1718
|
|
|
1124
1719
|
// ---------------------------------------------------------------------------
|
|
@@ -1129,8 +1724,8 @@ export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransiti
|
|
|
1129
1724
|
// status-v2.schema.json action_eligibility definition v2 also $refs.
|
|
1130
1725
|
// ---------------------------------------------------------------------------
|
|
1131
1726
|
|
|
1132
|
-
const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.
|
|
1133
|
-
const FORBIDDEN_ACTIONS = ["review.abandon", "review.
|
|
1727
|
+
const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.recover", "review.repair"] as const;
|
|
1728
|
+
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;
|
|
1134
1729
|
|
|
1135
1730
|
function decodeEligibility(value: unknown, label: string): void {
|
|
1136
1731
|
const eligibility = exactRecord(value, label, ["allowed_actions", "forbidden_actions"]);
|
|
@@ -1139,14 +1734,14 @@ function decodeEligibility(value: unknown, label: string): void {
|
|
|
1139
1734
|
const selected = enumeration(action.action, ELIGIBLE_ACTIONS, `${entryLabel}.action`);
|
|
1140
1735
|
text(action.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1141
1736
|
array(action.required_inputs, `${entryLabel}.required_inputs`, (input, inputLabel) => text(input, inputLabel, { minimum: 1, pattern: /^[a-z0-9_]+$/ }), { unique: true });
|
|
1142
|
-
if (selected === "review.recover"
|
|
1737
|
+
if (selected === "review.recover") {
|
|
1143
1738
|
const binding = exactRecord(action.binding, `${entryLabel}.binding`, ["lineage_id", "revision", "target_identity"]);
|
|
1144
1739
|
enumeration(action.disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), `${entryLabel}.disposition`);
|
|
1145
1740
|
lineage(binding.lineage_id, `${entryLabel}.binding.lineage_id`);
|
|
1146
1741
|
sha256(binding.revision, `${entryLabel}.binding.revision`);
|
|
1147
1742
|
sha256(binding.target_identity, `${entryLabel}.binding.target_identity`);
|
|
1148
1743
|
} else if (action.disposition !== undefined || action.binding !== undefined) {
|
|
1149
|
-
throw new TypeError(`${entryLabel} recovery fields require review.recover
|
|
1744
|
+
throw new TypeError(`${entryLabel} recovery fields require review.recover`);
|
|
1150
1745
|
}
|
|
1151
1746
|
return action;
|
|
1152
1747
|
}, { minimum: 1, maximum: 1 });
|
|
@@ -1158,36 +1753,33 @@ function decodeEligibility(value: unknown, label: string): void {
|
|
|
1158
1753
|
});
|
|
1159
1754
|
}
|
|
1160
1755
|
|
|
1161
|
-
function decodeFinalVerificationRetry(value: unknown, label: string): ReviewFinalVerificationRetryV1 {
|
|
1162
|
-
const body = exactRecord(value, label, ["incident_schema", "incident_class", "validating_revision", "target_identity", "failed_evidence_hash", "finalize_request_digest"], ["failed_evidence_record_digest"]);
|
|
1163
|
-
if (body.incident_schema !== "gentle-ai.review-final-verification-incident/v1") throw new TypeError(`${label}.incident_schema is unsupported`);
|
|
1164
|
-
if (body.incident_class !== "procedural_tooling_failure") throw new TypeError(`${label}.incident_class is unsupported`);
|
|
1165
|
-
return {
|
|
1166
|
-
incidentSchema: "gentle-ai.review-final-verification-incident/v1",
|
|
1167
|
-
incidentClass: "procedural_tooling_failure",
|
|
1168
|
-
validatingRevision: sha256(body.validating_revision, `${label}.validating_revision`),
|
|
1169
|
-
targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
|
|
1170
|
-
failedEvidenceHash: sha256(body.failed_evidence_hash, `${label}.failed_evidence_hash`),
|
|
1171
|
-
...(body.failed_evidence_record_digest === undefined ? {} : { failedEvidenceRecordDigest: sha256(body.failed_evidence_record_digest, `${label}.failed_evidence_record_digest`) }),
|
|
1172
|
-
finalizeRequestDigest: sha256(body.finalize_request_digest, `${label}.finalize_request_digest`),
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
1756
|
// ---------------------------------------------------------------------------
|
|
1177
1757
|
// status/v3
|
|
1178
1758
|
// ---------------------------------------------------------------------------
|
|
1179
1759
|
|
|
1180
1760
|
export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
1761
|
+
// Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
|
|
1762
|
+
// against a live capture and the vendored status-v5.schema.json) is the v3
|
|
1763
|
+
// key set plus the optional forecast and the v5-only next_transition
|
|
1764
|
+
// surfaces. status/v6 adds the intended-untracked selection; v3 keeps
|
|
1765
|
+
// rejecting every v5/v6-only field.
|
|
1766
|
+
const schema = typeof value === "object" && value !== null ? (value as Record<string, unknown>).schema : undefined;
|
|
1767
|
+
const v6 = schema === "gentle-ai.review-integration.status/v6";
|
|
1768
|
+
const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
|
|
1181
1769
|
const body = exactRecord(value, "status", [
|
|
1182
|
-
"schema", "contract", "operation", "applicability", "
|
|
1183
|
-
], ["authority", "frozen", "
|
|
1184
|
-
requireIdentity(body, "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1770
|
+
"schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
|
|
1771
|
+
], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : [])]);
|
|
1772
|
+
requireIdentity(body, v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1185
1773
|
|
|
1186
1774
|
const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1775
|
+
let receipt: ReviewStatusReceiptV1 | undefined;
|
|
1776
|
+
if (body.receipt !== undefined) {
|
|
1777
|
+
const source = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
|
|
1778
|
+
receipt = {
|
|
1779
|
+
status: enumeration(source.status, RECEIPT_STATUSES, "status.receipt.status"),
|
|
1780
|
+
...(source.identity === undefined ? {} : { identity: sha256(source.identity, "status.receipt.identity") }),
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1191
1783
|
let authority: ReviewStatusAuthorityV1 | undefined;
|
|
1192
1784
|
if (body.authority !== undefined) {
|
|
1193
1785
|
const source = exactRecord(body.authority, "status.authority", ["version", "lineage_id", "state", "generation", "revision"]);
|
|
@@ -1215,66 +1807,68 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1215
1807
|
}
|
|
1216
1808
|
if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
|
|
1217
1809
|
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");
|
|
1218
|
-
if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 &&
|
|
1810
|
+
if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && receipt !== undefined && receipt.status !== "expected_missing" && receipt.status !== "present") {
|
|
1811
|
+
throw new TypeError("legacy status receipt is incompatible");
|
|
1812
|
+
}
|
|
1219
1813
|
|
|
1220
1814
|
const action = enumeration(body.action, STATUS_ACTIONS, "status.action");
|
|
1221
1815
|
const actionDisposition = body.action_disposition === undefined ? undefined : enumeration(body.action_disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), "status.action_disposition");
|
|
1222
|
-
if (
|
|
1223
|
-
if (action !== "recover" &&
|
|
1816
|
+
if (action === "recover" && actionDisposition === undefined) throw new TypeError("recover status requires action_disposition");
|
|
1817
|
+
if (action !== "recover" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover action");
|
|
1224
1818
|
if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
|
|
1225
|
-
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
if (
|
|
1233
|
-
|
|
1234
|
-
if (replayability !== REVIEW_REPLAYABILITY.STATUS_REQUIRED) throw new TypeError("reconcile_finalize status requires status_required replayability");
|
|
1235
|
-
reconciliation = { required: true };
|
|
1236
|
-
} else if (body.reconciliation !== undefined) {
|
|
1237
|
-
throw new TypeError("status.reconciliation is only valid for the reconcile_finalize action");
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
let validationRequest: ReviewTargetedValidationRequestV1 | undefined;
|
|
1241
|
-
if (body.validation_request !== undefined) {
|
|
1242
|
-
if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.state !== "correction_required") {
|
|
1243
|
-
throw new TypeError("status.validation_request requires current_target applicability and correction_required authority state");
|
|
1819
|
+
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6 });
|
|
1820
|
+
const validationRequest = v5 && body.validation_request !== undefined
|
|
1821
|
+
? decodeReviewTargetedValidationRequestV1(body.validation_request, "status.validation_request")
|
|
1822
|
+
: undefined;
|
|
1823
|
+
if (validationRequest !== undefined) {
|
|
1824
|
+
const collectInputs = nextTransition?.kind === "collect" ? nextTransition.collect?.inputs ?? [] : [];
|
|
1825
|
+
const matchingInputs = collectInputs.filter((input) => input.validationRequest !== undefined);
|
|
1826
|
+
if (matchingInputs.length !== 1 || canonicalJson(matchingInputs[0]!.validationRequest) !== canonicalJson(validationRequest)) {
|
|
1827
|
+
throw new TypeError("status.validation_request must exactly match the provider-targeted-validator collect input");
|
|
1244
1828
|
}
|
|
1245
|
-
validationRequest = decodeTargetedValidationRequestV1(body.validation_request, "status.validation_request");
|
|
1246
1829
|
}
|
|
1830
|
+
// status/v5 dependentRequired: a forecast previews the transition head, so
|
|
1831
|
+
// it can only accompany an actual next_transition.
|
|
1832
|
+
const forecast = v5 && body.forecast !== undefined ? decodeReviewForecastV1(body.forecast) : undefined;
|
|
1833
|
+
if (forecast !== undefined && nextTransition === undefined) throw new TypeError("status.forecast requires status.next_transition");
|
|
1834
|
+
const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
|
|
1247
1835
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1836
|
+
// status/v5 additive top-level repository context reference (captured
|
|
1837
|
+
// 2026-08-16 from 2.4.0-main.b1afef46; missing from the published
|
|
1838
|
+
// status-v5.schema.json, so the live capture is authoritative). Same
|
|
1839
|
+
// reference shape as start/v3's repository_context.
|
|
1840
|
+
let repositoryContext: ReviewRepositoryContextV2 | undefined;
|
|
1841
|
+
if (body.repository_context !== undefined) {
|
|
1842
|
+
const source = exactRecord(body.repository_context, "status.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
|
|
1843
|
+
if (source.capability !== "review.opaque_repository_context") throw new TypeError("status.repository_context.capability is unsupported");
|
|
1844
|
+
repositoryContext = {
|
|
1845
|
+
capability: "review.opaque_repository_context",
|
|
1846
|
+
handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx[12]_[0-9a-f]{64}$/ }),
|
|
1847
|
+
revision: sha256(source.revision, "status.repository_context.revision"),
|
|
1848
|
+
targetIdentity: sha256(source.target_identity, "status.repository_context.target_identity"),
|
|
1849
|
+
...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "status.repository_context.event_id") }),
|
|
1850
|
+
...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "status.repository_context.outcome") }),
|
|
1851
|
+
};
|
|
1258
1852
|
}
|
|
1259
1853
|
|
|
1260
1854
|
return {
|
|
1261
1855
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
1262
1856
|
applicability,
|
|
1263
1857
|
...(authority === undefined ? {} : { authority }),
|
|
1264
|
-
receipt,
|
|
1858
|
+
...(receipt === undefined ? {} : { receipt }),
|
|
1265
1859
|
action,
|
|
1266
1860
|
...(actionDisposition === undefined ? {} : { actionDisposition }),
|
|
1267
1861
|
replayability,
|
|
1268
1862
|
...(frozen === undefined ? {} : { frozen }),
|
|
1269
|
-
...(reconciliation === undefined ? {} : { reconciliation }),
|
|
1270
1863
|
targetIdentity: sha256(body.target_identity, "status.target_identity"),
|
|
1271
1864
|
...(body.authority_target_identity === undefined ? {} : { authorityTargetIdentity: sha256(body.authority_target_identity, "status.authority_target_identity") }),
|
|
1272
1865
|
projection: decodeReviewProjectionV1(body.projection),
|
|
1273
1866
|
repair: decodeAuthorityRepairAssessmentV1(body.repair),
|
|
1274
1867
|
candidates: stringArray(body.candidates, "status.candidates", { unique: true, pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ }),
|
|
1275
1868
|
...(nextTransition === undefined ? {} : { nextTransition }),
|
|
1869
|
+
...(forecast === undefined ? {} : { forecast }),
|
|
1870
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
1276
1871
|
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
1277
|
-
...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
|
|
1278
1872
|
raw: body,
|
|
1279
1873
|
};
|
|
1280
1874
|
}
|
|
@@ -1295,11 +1889,10 @@ function decodeConsentChoice(value: unknown, label: string, answer: "granted" |
|
|
|
1295
1889
|
};
|
|
1296
1890
|
}
|
|
1297
1891
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
|
|
1892
|
+
// The semantic surface shared verbatim by both accepted consent identities.
|
|
1893
|
+
// Every label and guard predates consent/v3, so the v2 decode stays
|
|
1894
|
+
// byte-identical (test-locked) while v3 adds only its own identity gate.
|
|
1895
|
+
function decodeConsentSemantics(body: Record<string, unknown>): Omit<ReviewConsentV2, "schema" | "raw"> {
|
|
1303
1896
|
if (body.action !== "consent_required") throw new TypeError("consent.action must be consent_required");
|
|
1304
1897
|
if (body.blocking !== true) throw new TypeError("consent.blocking must be true");
|
|
1305
1898
|
|
|
@@ -1316,7 +1909,6 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
|
|
|
1316
1909
|
if (offPathSource.command !== "gentle-ai review mode disable") throw new TypeError("consent.off_path.command is unsupported");
|
|
1317
1910
|
|
|
1318
1911
|
return {
|
|
1319
|
-
schema: "gentle-ai.review-integration.consent/v2",
|
|
1320
1912
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
1321
1913
|
operation: "review.start",
|
|
1322
1914
|
action: "consent_required",
|
|
@@ -1332,6 +1924,40 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
|
|
|
1332
1924
|
riskEvidence: stringArray(body.risk_evidence, "consent.risk_evidence"),
|
|
1333
1925
|
choices: [granted, declined],
|
|
1334
1926
|
offPath: { note: nonempty(offPathSource.note, "consent.off_path.note"), command: "gentle-ai review mode disable" },
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const CONSENT_KEYS_V2 = Object.freeze([
|
|
1931
|
+
"schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
|
|
1932
|
+
] as const);
|
|
1933
|
+
|
|
1934
|
+
export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
|
|
1935
|
+
const body = exactRecord(value, "consent", CONSENT_KEYS_V2);
|
|
1936
|
+
requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
|
|
1937
|
+
return {
|
|
1938
|
+
schema: "gentle-ai.review-integration.consent/v2",
|
|
1939
|
+
...decodeConsentSemantics(body),
|
|
1940
|
+
raw: body,
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// consent/v3 (gentle-ai >= 2.3.0): the v2 surface plus the required, fixed
|
|
1945
|
+
// `agent` runtime binding. Ground truth is the captured envelope from a
|
|
1946
|
+
// 2.4.0-main binary (tests/fixtures/devbinary/consent-v3.captured.json) plus
|
|
1947
|
+
// gentle-ai main contracts/review-integration/v2/schemas/consent-v3.schema.json.
|
|
1948
|
+
// The choice-invocation shape deliberately stays the shared v2 pattern: the
|
|
1949
|
+
// published v3 schema demands an `--agent claude-code` token that the live
|
|
1950
|
+
// emitter omits when the caller declared no --agent, so the capture is
|
|
1951
|
+
// authoritative and Pi replays whichever provider-owned invocation arrived.
|
|
1952
|
+
export function decodeReviewConsentV3(value: unknown, expectedAgent?: ReviewConsentAgentV3): ReviewConsentV3 {
|
|
1953
|
+
const body = exactRecord(value, "consent", [...CONSENT_KEYS_V2, "agent"]);
|
|
1954
|
+
requireIdentity(body, "gentle-ai.review-integration.consent/v3", "review.start");
|
|
1955
|
+
const agent = enumeration(body.agent, Object.values(REVIEW_CONSENT_AGENT_V3), "consent.agent") as ReviewConsentAgentV3;
|
|
1956
|
+
if (expectedAgent !== undefined && agent !== expectedAgent) throw new TypeError("consent.agent does not match the expected runtime binding");
|
|
1957
|
+
return {
|
|
1958
|
+
schema: "gentle-ai.review-integration.consent/v3",
|
|
1959
|
+
agent,
|
|
1960
|
+
...decodeConsentSemantics(body),
|
|
1335
1961
|
raw: body,
|
|
1336
1962
|
};
|
|
1337
1963
|
}
|
|
@@ -1395,7 +2021,7 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
|
|
|
1395
2021
|
"schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
|
|
1396
2022
|
], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
|
|
1397
2023
|
requireIdentity(body, "gentle-ai.review-integration.failure/v2");
|
|
1398
|
-
const operation = enumeration(body.operation,
|
|
2024
|
+
const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
|
|
1399
2025
|
|
|
1400
2026
|
if (body.progress_identity !== undefined) {
|
|
1401
2027
|
if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
|
|
@@ -1432,56 +2058,21 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
|
|
|
1432
2058
|
// operation/v2
|
|
1433
2059
|
// ---------------------------------------------------------------------------
|
|
1434
2060
|
|
|
1435
|
-
export function
|
|
1436
|
-
const body = exactRecord(value,
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
decodeTargetedValidationRequestV1(result.validation_request, "operation.result.validation_request");
|
|
1451
|
-
if (result.state !== "correction_required") throw new TypeError("operation.result.validation_request requires state correction_required");
|
|
1452
|
-
}
|
|
1453
|
-
if (result.escalation !== undefined) nonempty(result.escalation, "operation.result.escalation");
|
|
1454
|
-
} else if (operation === REVIEW_INTEGRATION_OPERATION.VALIDATE) {
|
|
1455
|
-
result = exactRecord(body.result, "operation.result", ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
|
|
1456
|
-
if (result.schema !== "gentle-ai.review-gate-result/v1") throw new TypeError("operation.result does not match review.validate");
|
|
1457
|
-
enumeration(result.result, ["allow", "scope-changed", "invalidated", "escalated"] as const, "operation.result.result");
|
|
1458
|
-
boolean(result.allowed, "operation.result.allowed");
|
|
1459
|
-
nonempty(result.action, "operation.result.action");
|
|
1460
|
-
nonempty(result.reason, "operation.result.reason");
|
|
1461
|
-
record(result.context, "operation.result.context");
|
|
1462
|
-
if (result.delivery !== undefined && result.delivery !== "disabled/unmanaged") throw new TypeError("operation.result.delivery is unsupported");
|
|
1463
|
-
} else if (operation === REVIEW_INTEGRATION_OPERATION.BIND_SDD) {
|
|
1464
|
-
result = exactRecord(body.result, "operation.result", ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
|
|
1465
|
-
if (result.schema !== "gentle-ai.sdd-review-binding/v1") throw new TypeError("operation.result does not match review.bind_sdd");
|
|
1466
|
-
sha256(result.revision, "operation.result.revision");
|
|
1467
|
-
nonempty(result.change, "operation.result.change");
|
|
1468
|
-
nonempty(result.lineage, "operation.result.lineage");
|
|
1469
|
-
sha256(result.authority_revision, "operation.result.authority_revision");
|
|
1470
|
-
sha256(result.receipt_hash, "operation.result.receipt_hash");
|
|
1471
|
-
record(result.gate_context, "operation.result.gate_context");
|
|
1472
|
-
} else {
|
|
1473
|
-
result = exactRecord(body.result, "operation.result", ["operation", "predecessor_lineage_id", "predecessor_revision", "lineage_id", "state", "store_revision", "target_identity", "incident_digest", "recovery_disposition"]);
|
|
1474
|
-
if (result.operation !== "review.retry_final_verification") throw new TypeError("operation.result does not match review.retry_final_verification");
|
|
1475
|
-
lineage(result.predecessor_lineage_id, "operation.result.predecessor_lineage_id");
|
|
1476
|
-
sha256(result.predecessor_revision, "operation.result.predecessor_revision");
|
|
1477
|
-
lineage(result.lineage_id, "operation.result.lineage_id");
|
|
1478
|
-
if (result.state !== "validating") throw new TypeError("operation.result.state must be validating");
|
|
1479
|
-
sha256(result.store_revision, "operation.result.store_revision");
|
|
1480
|
-
sha256(result.target_identity, "operation.result.target_identity");
|
|
1481
|
-
sha256(result.incident_digest, "operation.result.incident_digest");
|
|
1482
|
-
if (result.recovery_disposition !== "final_verification_retry") throw new TypeError("operation.result.recovery_disposition must be final_verification_retry");
|
|
1483
|
-
}
|
|
1484
|
-
return { contract: REVIEW_INTEGRATION_CONTRACT, operation, result, raw: body };
|
|
2061
|
+
export function decodeReviewAdvisoryFindingsV1(value: unknown, label: string): ReviewAdvisoryFindingsV1 {
|
|
2062
|
+
const body = exactRecord(value, label, ["statement", "findings"]);
|
|
2063
|
+
return {
|
|
2064
|
+
statement: nonempty(body.statement, `${label}.statement`),
|
|
2065
|
+
findings: array(body.findings, `${label}.findings`, (entry, itemLabel) => {
|
|
2066
|
+
const finding = exactRecord(entry, itemLabel, ["id", "severity", "disposition"], ["lens", "location"]);
|
|
2067
|
+
return {
|
|
2068
|
+
id: nonempty(finding.id, `${itemLabel}.id`),
|
|
2069
|
+
...(finding.lens === undefined ? {} : { lens: nonempty(finding.lens, `${itemLabel}.lens`) }),
|
|
2070
|
+
...(finding.location === undefined ? {} : { location: nonempty(finding.location, `${itemLabel}.location`) }),
|
|
2071
|
+
severity: enumeration(finding.severity, Object.values(REVIEW_ADVISORY_FINDING_SEVERITY), `${itemLabel}.severity`),
|
|
2072
|
+
disposition: enumeration(finding.disposition, Object.values(REVIEW_ADVISORY_FINDING_DISPOSITION), `${itemLabel}.disposition`),
|
|
2073
|
+
};
|
|
2074
|
+
}, { minimum: 1 }),
|
|
2075
|
+
};
|
|
1485
2076
|
}
|
|
1486
2077
|
|
|
1487
2078
|
// ---------------------------------------------------------------------------
|
|
@@ -1565,3 +2156,445 @@ export function decodeReviewRepairV2(value: unknown): ReviewRepairV2 {
|
|
|
1565
2156
|
raw: body,
|
|
1566
2157
|
};
|
|
1567
2158
|
}
|
|
2159
|
+
|
|
2160
|
+
// ---------------------------------------------------------------------------
|
|
2161
|
+
// result-artifact/v2 — the `review capture-result` admission answer
|
|
2162
|
+
// ---------------------------------------------------------------------------
|
|
2163
|
+
|
|
2164
|
+
// The provider's admitted-reviewer-result envelope, printed by `review
|
|
2165
|
+
// capture-result` when a reviewer result is admitted and re-discovered by
|
|
2166
|
+
// STATUS artifact discovery. Unlike the negotiated envelopes above it carries
|
|
2167
|
+
// no `contract`/`operation` identity pair — the schema constant plus the
|
|
2168
|
+
// capability constant are its complete identity (vendored ground truth:
|
|
2169
|
+
// contracts/review-integration/v1/schemas/result-artifact-v2.schema.json,
|
|
2170
|
+
// confirmed against a live 2.4.0-main capture; the v2.2.3 pinned emitter
|
|
2171
|
+
// shares the exact same struct). Exactly one locator is present: a
|
|
2172
|
+
// provider-owned store `path`, or the opaque `rart1_` `reference` minted for
|
|
2173
|
+
// repository-context captures.
|
|
2174
|
+
export interface ReviewResultArtifactV2 {
|
|
2175
|
+
schema: "gentle-ai.review-result-artifact/v2";
|
|
2176
|
+
capability: "review.native_result_artifact";
|
|
2177
|
+
sha256: string;
|
|
2178
|
+
lineageId: string;
|
|
2179
|
+
targetIdentity: string;
|
|
2180
|
+
lens: (typeof REVIEW_LENSES)[number];
|
|
2181
|
+
selectedOrder: number;
|
|
2182
|
+
subjectHash: string;
|
|
2183
|
+
admissionDecision: "completed";
|
|
2184
|
+
path?: string;
|
|
2185
|
+
reference?: string;
|
|
2186
|
+
raw: Record<string, unknown>;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
export function decodeReviewResultArtifactV2(value: unknown): ReviewResultArtifactV2 {
|
|
2190
|
+
const body = exactRecord(value, "result_artifact", ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"], ["path", "reference"]);
|
|
2191
|
+
if (body.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError("result_artifact.schema must be gentle-ai.review-result-artifact/v2");
|
|
2192
|
+
if (body.capability !== "review.native_result_artifact") throw new TypeError("result_artifact.capability must be review.native_result_artifact");
|
|
2193
|
+
if (body.admission_decision !== "completed") throw new TypeError("result_artifact.admission_decision must be completed");
|
|
2194
|
+
if ((body.path === undefined) === (body.reference === undefined)) throw new TypeError("result_artifact must carry exactly one of path or reference");
|
|
2195
|
+
return {
|
|
2196
|
+
schema: "gentle-ai.review-result-artifact/v2",
|
|
2197
|
+
capability: "review.native_result_artifact",
|
|
2198
|
+
sha256: sha256(body.sha256, "result_artifact.sha256"),
|
|
2199
|
+
lineageId: lineage(body.lineage_id, "result_artifact.lineage_id"),
|
|
2200
|
+
targetIdentity: sha256(body.target_identity, "result_artifact.target_identity"),
|
|
2201
|
+
lens: enumeration(body.lens, REVIEW_LENSES, "result_artifact.lens"),
|
|
2202
|
+
selectedOrder: integer(body.selected_order, "result_artifact.selected_order", 0, 3),
|
|
2203
|
+
subjectHash: sha256(body.subject_hash, "result_artifact.subject_hash"),
|
|
2204
|
+
admissionDecision: "completed",
|
|
2205
|
+
...(body.path === undefined ? {} : { path: nonempty(body.path, "result_artifact.path") }),
|
|
2206
|
+
...(body.reference === undefined ? {} : { reference: text(body.reference, "result_artifact.reference", { pattern: /^rart1_[0-9a-f]{64}$/ }) }),
|
|
2207
|
+
raw: body,
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// Terminal capture answers are emitted directly by the native provider. Pi
|
|
2212
|
+
// validates their closed shape and identity, then treats all lifecycle meaning
|
|
2213
|
+
// as opaque provider-owned state.
|
|
2214
|
+
export const REVIEW_LAST_EVENT_CLOSURE_SCHEMA = "gentle-ai.review-last-event-closure/v1";
|
|
2215
|
+
export const REVIEW_LAST_EVENT_CLOSURE_OPERATION = {
|
|
2216
|
+
CAPTURE_RESULT: "review/capture-result",
|
|
2217
|
+
CAPTURE_CORRECTION_PLAN: "review.capture-correction-plan",
|
|
2218
|
+
CAPTURE_REFUTER: "review.capture-refuter",
|
|
2219
|
+
CAPTURE_VALIDATION: "review/capture-validation",
|
|
2220
|
+
} as const;
|
|
2221
|
+
export type ReviewLastEventClosureOperation = (typeof REVIEW_LAST_EVENT_CLOSURE_OPERATION)[keyof typeof REVIEW_LAST_EVENT_CLOSURE_OPERATION];
|
|
2222
|
+
|
|
2223
|
+
const REVIEW_LAST_EVENT_TERMINAL_STATES = ["approved", "correction_required", "escalated"] as const;
|
|
2224
|
+
export type ReviewLastEventClosureState = (typeof REVIEW_LAST_EVENT_TERMINAL_STATES)[number];
|
|
2225
|
+
|
|
2226
|
+
const REVIEW_STATUS_CONTINUATION_OPERATION = {
|
|
2227
|
+
STATUS: "review.status",
|
|
2228
|
+
} as const;
|
|
2229
|
+
|
|
2230
|
+
export interface ReviewStatusContinuationBindingV1 {
|
|
2231
|
+
targetIdentity: string;
|
|
2232
|
+
lineageId?: string;
|
|
2233
|
+
revision?: string;
|
|
2234
|
+
repositoryContext?: string;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
export interface ReviewStatusContinuationArtifactV1 {
|
|
2238
|
+
schema: "gentle-ai.review-result-artifact/v2";
|
|
2239
|
+
capability: "review.native_result_artifact";
|
|
2240
|
+
sha256: string;
|
|
2241
|
+
lineageId: string;
|
|
2242
|
+
targetIdentity: string;
|
|
2243
|
+
lens: string;
|
|
2244
|
+
selectedOrder: number;
|
|
2245
|
+
subjectHash: string;
|
|
2246
|
+
admissionDecision: "completed";
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
export interface ReviewStatusContinuationV1 {
|
|
2250
|
+
operation: typeof REVIEW_STATUS_CONTINUATION_OPERATION.STATUS;
|
|
2251
|
+
arguments: readonly ReviewTransitionArgumentV3[];
|
|
2252
|
+
selectorArguments?: readonly ReviewTransitionArgumentV3[];
|
|
2253
|
+
preconditions: readonly ReviewTransitionArgumentV3[];
|
|
2254
|
+
binding: ReviewStatusContinuationBindingV1;
|
|
2255
|
+
artifacts?: readonly ReviewStatusContinuationArtifactV1[];
|
|
2256
|
+
command?: string;
|
|
2257
|
+
raw: Record<string, unknown>;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION = "review.acknowledge-approved" as const;
|
|
2261
|
+
|
|
2262
|
+
// The exact ordered argument names the provider renders for one approved
|
|
2263
|
+
// acknowledgement. The list is closed and positional on the wire, so decoding
|
|
2264
|
+
// by position is what proves the relay is replaying the provider's own
|
|
2265
|
+
// invocation rather than one the host assembled.
|
|
2266
|
+
const REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS = ["cwd", "lineage", "target", "expected-revision", "token"] as const;
|
|
2267
|
+
|
|
2268
|
+
export interface ReviewApprovedAcknowledgementBindingV1 {
|
|
2269
|
+
lineageId: string;
|
|
2270
|
+
revision: string;
|
|
2271
|
+
targetIdentity: string;
|
|
2272
|
+
repositoryContext?: string;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
export interface ReviewApprovedAcknowledgementV1 {
|
|
2276
|
+
operation: typeof REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION;
|
|
2277
|
+
command: string;
|
|
2278
|
+
arguments: readonly ReviewTransitionArgumentV3[];
|
|
2279
|
+
preconditions: readonly ReviewTransitionArgumentV3[];
|
|
2280
|
+
binding: ReviewApprovedAcknowledgementBindingV1;
|
|
2281
|
+
raw: Record<string, unknown>;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
export interface ReviewApprovedAcknowledgementExecuteExpectationV1 { cwd?: string; lineageId?: string; targetIdentity?: string; revision?: string; }
|
|
2285
|
+
|
|
2286
|
+
export type ReviewApprovedAcknowledgementExecuteTokensV1 = readonly [string, string, string, string, string];
|
|
2287
|
+
|
|
2288
|
+
interface ReviewApprovedAcknowledgementExecuteShapeV1 { tokens: ReviewApprovedAcknowledgementExecuteTokensV1; values: readonly [string, string, string, string, string]; lineageId: string; targetIdentity: string; revision: string; }
|
|
2289
|
+
|
|
2290
|
+
function assertReviewApprovedAcknowledgementExecuteShapeV1(execute: ReviewNextTransitionExecuteV3): ReviewApprovedAcknowledgementExecuteShapeV1 {
|
|
2291
|
+
if (execute.operation !== REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION) throw new TypeError(`acknowledgement.execute.operation must be ${REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION}`);
|
|
2292
|
+
if (execute.command === undefined) throw new TypeError("acknowledgement.execute.command is required");
|
|
2293
|
+
text(execute.command, "acknowledgement.execute.command", { minimum: 1, pattern: /^gentle-ai review acknowledge-approved(?: |$)/ });
|
|
2294
|
+
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`);
|
|
2295
|
+
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; });
|
|
2296
|
+
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");
|
|
2297
|
+
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") };
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* Proves that an approved-acknowledgement execute vector is the exact closed,
|
|
2302
|
+
* provider-issued burn invocation. Callers can additionally bind its values to
|
|
2303
|
+
* their own current STATUS before invoking the returned tokens.
|
|
2304
|
+
*/
|
|
2305
|
+
export function assertReviewApprovedAcknowledgementExecuteV1(
|
|
2306
|
+
execute: ReviewNextTransitionExecuteV3,
|
|
2307
|
+
expected: ReviewApprovedAcknowledgementExecuteExpectationV1 = {},
|
|
2308
|
+
): ReviewApprovedAcknowledgementExecuteTokensV1 {
|
|
2309
|
+
const shape = assertReviewApprovedAcknowledgementExecuteShapeV1(execute);
|
|
2310
|
+
if (shape.values[1] !== shape.lineageId) throw new TypeError("acknowledgement lineage argument does not match its binding");
|
|
2311
|
+
if (shape.values[2] !== shape.targetIdentity) throw new TypeError("acknowledgement target argument does not match its binding");
|
|
2312
|
+
if (shape.values[3] !== shape.revision) throw new TypeError("acknowledgement revision argument does not match its binding");
|
|
2313
|
+
if (expected.cwd !== undefined && shape.values[0] !== expected.cwd) throw new TypeError("acknowledgement.execute.cwd does not match the current target");
|
|
2314
|
+
if (expected.lineageId !== undefined && shape.lineageId !== expected.lineageId) throw new TypeError("acknowledgement.execute.lineage does not match the current target");
|
|
2315
|
+
if (expected.targetIdentity !== undefined && shape.targetIdentity !== expected.targetIdentity) throw new TypeError("acknowledgement.execute.target does not match the current target");
|
|
2316
|
+
if (expected.revision !== undefined && shape.revision !== expected.revision) throw new TypeError("acknowledgement.execute.revision does not match the current target"); return shape.tokens;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// ---------------------------------------------------------------------------
|
|
2320
|
+
// review-acknowledged/v1 — the answer the burn prints (gentle-ai #3947)
|
|
2321
|
+
// ---------------------------------------------------------------------------
|
|
2322
|
+
|
|
2323
|
+
// Until gentle-ai #3947 a successful `review acknowledge-approved` burned its
|
|
2324
|
+
// authority in silence and a host could only infer the burn from a later
|
|
2325
|
+
// STATUS offering a fresh START. From main commit bc9f74d2 the command prints
|
|
2326
|
+
// exactly this envelope on success. Like result-artifact/v2 it carries no
|
|
2327
|
+
// `contract`: the schema constant plus the slash-form operation are its whole
|
|
2328
|
+
// identity, so requireIdentity (which demands the contract pair) does not
|
|
2329
|
+
// apply. Every published release up to v2.5.0-rc.3 still prints nothing; the
|
|
2330
|
+
// native client owns that distinction, this decoder only ever sees bytes.
|
|
2331
|
+
export const REVIEW_ACKNOWLEDGED_SCHEMA = "gentle-ai.review-acknowledged/v1" as const;
|
|
2332
|
+
const REVIEW_ACKNOWLEDGED_OPERATION = "review/acknowledge-approved" as const;
|
|
2333
|
+
|
|
2334
|
+
export interface ReviewAcknowledgedV1 {
|
|
2335
|
+
schema: typeof REVIEW_ACKNOWLEDGED_SCHEMA;
|
|
2336
|
+
operation: typeof REVIEW_ACKNOWLEDGED_OPERATION;
|
|
2337
|
+
action: "acknowledged";
|
|
2338
|
+
lineageId: string;
|
|
2339
|
+
targetIdentity: string;
|
|
2340
|
+
consumedRevision: string;
|
|
2341
|
+
authority: "burned";
|
|
2342
|
+
raw: Record<string, unknown>;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
/** The binding the caller already holds from STATUS; the envelope must name exactly that burn. */
|
|
2346
|
+
export interface ReviewAcknowledgedExpectationV1 {
|
|
2347
|
+
lineageId?: string;
|
|
2348
|
+
targetIdentity?: string;
|
|
2349
|
+
revision?: string;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
export function decodeReviewAcknowledgedV1(value: unknown, expected: ReviewAcknowledgedExpectationV1 = {}): ReviewAcknowledgedV1 {
|
|
2353
|
+
if (record(value, "acknowledged").schema !== REVIEW_ACKNOWLEDGED_SCHEMA) throw new TypeError(`schema must be ${REVIEW_ACKNOWLEDGED_SCHEMA}`);
|
|
2354
|
+
const body = exactRecord(value, "acknowledged", ["schema", "operation", "action", "lineage_id", "target_identity", "consumed_revision", "authority"]);
|
|
2355
|
+
if (body.operation !== REVIEW_ACKNOWLEDGED_OPERATION) throw new TypeError(`acknowledged.operation must be ${REVIEW_ACKNOWLEDGED_OPERATION}`);
|
|
2356
|
+
if (body.action !== "acknowledged") throw new TypeError("acknowledged.action must be acknowledged");
|
|
2357
|
+
if (body.authority !== "burned") throw new TypeError("acknowledged.authority must be burned");
|
|
2358
|
+
const decoded: ReviewAcknowledgedV1 = {
|
|
2359
|
+
schema: REVIEW_ACKNOWLEDGED_SCHEMA,
|
|
2360
|
+
operation: REVIEW_ACKNOWLEDGED_OPERATION,
|
|
2361
|
+
action: "acknowledged",
|
|
2362
|
+
lineageId: lineage(body.lineage_id, "acknowledged.lineage_id"),
|
|
2363
|
+
targetIdentity: sha256(body.target_identity, "acknowledged.target_identity"),
|
|
2364
|
+
consumedRevision: sha256(body.consumed_revision, "acknowledged.consumed_revision"),
|
|
2365
|
+
authority: "burned",
|
|
2366
|
+
raw: body,
|
|
2367
|
+
};
|
|
2368
|
+
if (expected.lineageId !== undefined && decoded.lineageId !== expected.lineageId) throw new TypeError("acknowledged.lineage_id does not name the acknowledged lineage");
|
|
2369
|
+
if (expected.targetIdentity !== undefined && decoded.targetIdentity !== expected.targetIdentity) throw new TypeError("acknowledged.target_identity does not name the acknowledged target");
|
|
2370
|
+
if (expected.revision !== undefined && decoded.consumedRevision !== expected.revision) throw new TypeError("acknowledged.consumed_revision does not name the consumed revision");
|
|
2371
|
+
return decoded;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
export interface ReviewLastEventClosureV1 {
|
|
2375
|
+
schema: typeof REVIEW_LAST_EVENT_CLOSURE_SCHEMA;
|
|
2376
|
+
operation: ReviewLastEventClosureOperation;
|
|
2377
|
+
lineageId: string;
|
|
2378
|
+
state: ReviewLastEventClosureState;
|
|
2379
|
+
storeRevision: string;
|
|
2380
|
+
action?: string;
|
|
2381
|
+
targetIdentity?: string;
|
|
2382
|
+
requestHash?: string;
|
|
2383
|
+
correctionLines?: number;
|
|
2384
|
+
advisoryFindings?: ReviewAdvisoryFindingsV1;
|
|
2385
|
+
statusContinuation?: ReviewStatusContinuationV1;
|
|
2386
|
+
acknowledgement?: ReviewApprovedAcknowledgementV1;
|
|
2387
|
+
// Set when the provider sent an acknowledgement this decoder could not
|
|
2388
|
+
// read. It is deliberately distinct from an absent one: the host must be
|
|
2389
|
+
// able to tell "approved, nothing to run" from "approved, and the thing
|
|
2390
|
+
// that ends this is present but unreadable here".
|
|
2391
|
+
acknowledgementUndecodable?: true;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
export interface ReviewLastEventClosureBinding {
|
|
2395
|
+
lineageId: string;
|
|
2396
|
+
targetIdentity?: string;
|
|
2397
|
+
requestHash?: string;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function decodeReviewStatusContinuationArtifactV1(value: unknown, label: string): ReviewStatusContinuationArtifactV1 {
|
|
2401
|
+
const artifact = exactRecord(value, label, ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"]);
|
|
2402
|
+
if (artifact.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError(`${label}.schema must be gentle-ai.review-result-artifact/v2`);
|
|
2403
|
+
if (artifact.capability !== "review.native_result_artifact") throw new TypeError(`${label}.capability must be review.native_result_artifact`);
|
|
2404
|
+
if (artifact.admission_decision !== "completed") throw new TypeError(`${label}.admission_decision must be completed`);
|
|
2405
|
+
return {
|
|
2406
|
+
schema: "gentle-ai.review-result-artifact/v2",
|
|
2407
|
+
capability: "review.native_result_artifact",
|
|
2408
|
+
sha256: sha256(artifact.sha256, `${label}.sha256`),
|
|
2409
|
+
lineageId: lineage(artifact.lineage_id, `${label}.lineage_id`),
|
|
2410
|
+
targetIdentity: sha256(artifact.target_identity, `${label}.target_identity`),
|
|
2411
|
+
lens: nonempty(artifact.lens, `${label}.lens`),
|
|
2412
|
+
selectedOrder: integer(artifact.selected_order, `${label}.selected_order`, 0),
|
|
2413
|
+
subjectHash: sha256(artifact.subject_hash, `${label}.subject_hash`),
|
|
2414
|
+
admissionDecision: "completed",
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// decodeReviewApprovedAcknowledgementV1 retains the closure-specific wire
|
|
2419
|
+
// boundary with the shared shape validator; its semantic binding stays outside
|
|
2420
|
+
// the defensive closure catch in the exported assertion.
|
|
2421
|
+
function decodeReviewApprovedAcknowledgementV1(value: unknown, label: string): ReviewApprovedAcknowledgementV1 {
|
|
2422
|
+
const body = exactRecord(value, label, ["operation", "command", "arguments", "preconditions", "binding"]);
|
|
2423
|
+
const argumentsList = decodeTransitionArguments(body.arguments, `${label}.arguments`);
|
|
2424
|
+
const preconditions = decodeTransitionArguments(body.preconditions, `${label}.preconditions`);
|
|
2425
|
+
const sourceBinding = exactRecord(body.binding, `${label}.binding`, ["lineage_id", "revision", "target_identity"], ["repository_context"]);
|
|
2426
|
+
const repositoryContext = sourceBinding.repository_context === undefined
|
|
2427
|
+
? undefined
|
|
2428
|
+
: text(sourceBinding.repository_context, `${label}.binding.repository_context`, { pattern: /^rctx[12]_[0-9a-f]{64}$/ });
|
|
2429
|
+
const acknowledgement = {
|
|
2430
|
+
operation: body.operation,
|
|
2431
|
+
command: body.command,
|
|
2432
|
+
arguments: argumentsList,
|
|
2433
|
+
preconditions,
|
|
2434
|
+
binding: {
|
|
2435
|
+
lineageId: sourceBinding.lineage_id,
|
|
2436
|
+
revision: sourceBinding.revision,
|
|
2437
|
+
targetIdentity: sourceBinding.target_identity,
|
|
2438
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
2439
|
+
},
|
|
2440
|
+
} as unknown as ReviewNextTransitionExecuteV3;
|
|
2441
|
+
const shape = assertReviewApprovedAcknowledgementExecuteShapeV1(acknowledgement);
|
|
2442
|
+
return {
|
|
2443
|
+
operation: REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION,
|
|
2444
|
+
command: acknowledgement.command!,
|
|
2445
|
+
arguments: argumentsList,
|
|
2446
|
+
preconditions,
|
|
2447
|
+
binding: {
|
|
2448
|
+
lineageId: shape.lineageId,
|
|
2449
|
+
revision: shape.revision,
|
|
2450
|
+
targetIdentity: shape.targetIdentity,
|
|
2451
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
2452
|
+
},
|
|
2453
|
+
raw: body,
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function decodeReviewStatusContinuationV1(value: unknown): ReviewStatusContinuationV1 {
|
|
2458
|
+
const body = exactRecord(value, "last_event_closure.status_continuation", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
|
|
2459
|
+
if (body.operation !== REVIEW_STATUS_CONTINUATION_OPERATION.STATUS) throw new TypeError("last_event_closure.status_continuation.operation must be review.status");
|
|
2460
|
+
const argumentsList = decodeTransitionArguments(body.arguments, "last_event_closure.status_continuation.arguments");
|
|
2461
|
+
if (argumentsList.some((argument) => argument.token === undefined)) throw new TypeError("last_event_closure.status_continuation.arguments require exact tokens");
|
|
2462
|
+
const preconditions = decodeTransitionArguments(body.preconditions, "last_event_closure.status_continuation.preconditions");
|
|
2463
|
+
if (preconditions.length === 0) throw new TypeError("last_event_closure.status_continuation.preconditions requires at least one entry");
|
|
2464
|
+
const sourceBinding = exactRecord(body.binding, "last_event_closure.status_continuation.binding", ["target_identity"], ["lineage_id", "revision", "repository_context"]);
|
|
2465
|
+
const lineageId = sourceBinding.lineage_id === undefined ? undefined : lineage(sourceBinding.lineage_id, "last_event_closure.status_continuation.binding.lineage_id");
|
|
2466
|
+
const revision = sourceBinding.revision === undefined ? undefined : sha256(sourceBinding.revision, "last_event_closure.status_continuation.binding.revision");
|
|
2467
|
+
const repositoryContext = sourceBinding.repository_context === undefined
|
|
2468
|
+
? undefined
|
|
2469
|
+
: text(sourceBinding.repository_context, "last_event_closure.status_continuation.binding.repository_context", { pattern: /^rctx[12]_[0-9a-f]{64}$/ });
|
|
2470
|
+
const selectorArguments = body.selector_arguments === undefined
|
|
2471
|
+
? undefined
|
|
2472
|
+
: decodeTransitionArguments(body.selector_arguments, "last_event_closure.status_continuation.selector_arguments");
|
|
2473
|
+
const artifacts = body.artifacts === undefined
|
|
2474
|
+
? undefined
|
|
2475
|
+
: array(body.artifacts, "last_event_closure.status_continuation.artifacts", decodeReviewStatusContinuationArtifactV1);
|
|
2476
|
+
const command = body.command === undefined
|
|
2477
|
+
? undefined
|
|
2478
|
+
: text(body.command, "last_event_closure.status_continuation.command", { minimum: 1, pattern: /^gentle-ai review [a-z][a-z-]*/ });
|
|
2479
|
+
return {
|
|
2480
|
+
operation: REVIEW_STATUS_CONTINUATION_OPERATION.STATUS,
|
|
2481
|
+
arguments: argumentsList,
|
|
2482
|
+
...(selectorArguments === undefined ? {} : { selectorArguments }),
|
|
2483
|
+
preconditions,
|
|
2484
|
+
binding: {
|
|
2485
|
+
targetIdentity: sha256(sourceBinding.target_identity, "last_event_closure.status_continuation.binding.target_identity"),
|
|
2486
|
+
...(lineageId === undefined ? {} : { lineageId }),
|
|
2487
|
+
...(revision === undefined ? {} : { revision }),
|
|
2488
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
2489
|
+
},
|
|
2490
|
+
...(artifacts === undefined ? {} : { artifacts }),
|
|
2491
|
+
...(command === undefined ? {} : { command }),
|
|
2492
|
+
raw: body,
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventClosureV1 {
|
|
2497
|
+
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"]);
|
|
2498
|
+
if (body.schema !== REVIEW_LAST_EVENT_CLOSURE_SCHEMA) throw new TypeError(`last_event_closure.schema must be ${REVIEW_LAST_EVENT_CLOSURE_SCHEMA}`);
|
|
2499
|
+
const operation = enumeration(body.operation, Object.values(REVIEW_LAST_EVENT_CLOSURE_OPERATION), "last_event_closure.operation") as ReviewLastEventClosureOperation;
|
|
2500
|
+
const state = enumeration(body.state, REVIEW_LAST_EVENT_TERMINAL_STATES, "last_event_closure.state") as ReviewLastEventClosureState;
|
|
2501
|
+
const shared = {
|
|
2502
|
+
schema: REVIEW_LAST_EVENT_CLOSURE_SCHEMA,
|
|
2503
|
+
operation,
|
|
2504
|
+
lineageId: lineage(body.lineage_id, "last_event_closure.lineage_id"),
|
|
2505
|
+
state,
|
|
2506
|
+
storeRevision: sha256(body.store_revision, "last_event_closure.store_revision"),
|
|
2507
|
+
};
|
|
2508
|
+
if (operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_CORRECTION_PLAN) {
|
|
2509
|
+
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");
|
|
2510
|
+
if (state !== "correction_required") throw new TypeError("last_event_closure correction-plan requires correction_required state");
|
|
2511
|
+
return {
|
|
2512
|
+
...shared,
|
|
2513
|
+
targetIdentity: sha256(body.target_identity, "last_event_closure.target_identity"),
|
|
2514
|
+
requestHash: sha256(body.request_hash, "last_event_closure.request_hash"),
|
|
2515
|
+
correctionLines: integer(body.correction_lines, "last_event_closure.correction_lines", 1, 200),
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
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");
|
|
2519
|
+
const requiresStatusContinuation = state === "correction_required" && (
|
|
2520
|
+
operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_RESULT || operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_REFUTER
|
|
2521
|
+
);
|
|
2522
|
+
if (requiresStatusContinuation && body.status_continuation === undefined) throw new TypeError("last_event_closure requires status_continuation for correction-required result or refuter capture");
|
|
2523
|
+
if (!requiresStatusContinuation && body.status_continuation !== undefined) throw new TypeError("last_event_closure status_continuation is only valid for correction-required result or refuter capture");
|
|
2524
|
+
const statusContinuation = body.status_continuation === undefined ? undefined : decodeReviewStatusContinuationV1(body.status_continuation);
|
|
2525
|
+
if (statusContinuation !== undefined) {
|
|
2526
|
+
if (statusContinuation.binding.lineageId !== shared.lineageId) {
|
|
2527
|
+
throw new TypeError("last_event_closure status continuation lineage does not match its enclosing closure");
|
|
2528
|
+
}
|
|
2529
|
+
if (statusContinuation.binding.revision !== shared.storeRevision) {
|
|
2530
|
+
throw new TypeError("last_event_closure status continuation revision does not match its enclosing closure");
|
|
2531
|
+
}
|
|
2532
|
+
const lineageArguments = statusContinuation.arguments.filter((argument) => argument.name === "lineage");
|
|
2533
|
+
if (lineageArguments.length !== 1 || lineageArguments[0]?.value !== shared.lineageId || lineageArguments[0]?.token !== `--lineage=${shared.lineageId}`) {
|
|
2534
|
+
throw new TypeError("last_event_closure status continuation lineage argument does not match its enclosing closure");
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
// Only an approved closure may carry the continuation that burns its
|
|
2538
|
+
// authority: any other state offering an acknowledgement would be inviting
|
|
2539
|
+
// a burn the provider never authorized.
|
|
2540
|
+
//
|
|
2541
|
+
// It stays optional because the pinned installer binary predates it. A
|
|
2542
|
+
// provider that emits one is decoded strictly; a provider that does not is
|
|
2543
|
+
// still the contract this build ships against. Requiring it belongs with
|
|
2544
|
+
// the pin bump that makes it always present.
|
|
2545
|
+
// Decoded defensively on purpose. This decoder pins the acknowledgement to a
|
|
2546
|
+
// closed positional shape, and the same patch that added it already shows
|
|
2547
|
+
// what strictness costs when the provider moves: rejecting one omitempty
|
|
2548
|
+
// field made every targeted-validation STATUS that carried it undecodable.
|
|
2549
|
+
// Here the blast radius would be worse than a lost field. Throwing would
|
|
2550
|
+
// fail the whole approved closure, so the host would lose the approval
|
|
2551
|
+
// outcome AND the only invocation that burns the authority, leaving the
|
|
2552
|
+
// lineage approved and un-burnable at once. An unreadable continuation
|
|
2553
|
+
// degrades to a flag; everything else about the approval still arrives.
|
|
2554
|
+
let acknowledgement: ReviewApprovedAcknowledgementV1 | undefined;
|
|
2555
|
+
let acknowledgementUndecodable = false;
|
|
2556
|
+
if (body.acknowledgement !== undefined) {
|
|
2557
|
+
try {
|
|
2558
|
+
acknowledgement = decodeReviewApprovedAcknowledgementV1(body.acknowledgement, "last_event_closure.acknowledgement");
|
|
2559
|
+
} catch {
|
|
2560
|
+
acknowledgement = undefined;
|
|
2561
|
+
acknowledgementUndecodable = true;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
if (acknowledgementUndecodable && state !== "approved") {
|
|
2565
|
+
throw new TypeError("last_event_closure acknowledgement requires approved state");
|
|
2566
|
+
}
|
|
2567
|
+
if (acknowledgement !== undefined) {
|
|
2568
|
+
if (state !== "approved") throw new TypeError("last_event_closure acknowledgement requires approved state");
|
|
2569
|
+
assertReviewApprovedAcknowledgementExecuteV1(acknowledgement, { lineageId: shared.lineageId, revision: shared.storeRevision });
|
|
2570
|
+
}
|
|
2571
|
+
const action = nonempty(body.action, "last_event_closure.action");
|
|
2572
|
+
const advisoryFindings = body.advisory_findings === undefined
|
|
2573
|
+
? undefined
|
|
2574
|
+
: decodeReviewAdvisoryFindingsV1(body.advisory_findings, "last_event_closure.advisory_findings");
|
|
2575
|
+
if (advisoryFindings !== undefined && state !== "approved") throw new TypeError("last_event_closure advisory_findings requires approved state");
|
|
2576
|
+
return {
|
|
2577
|
+
...shared,
|
|
2578
|
+
action,
|
|
2579
|
+
...(advisoryFindings === undefined ? {} : { advisoryFindings }),
|
|
2580
|
+
...(statusContinuation === undefined ? {} : { statusContinuation }),
|
|
2581
|
+
...(acknowledgement === undefined ? {} : { acknowledgement }),
|
|
2582
|
+
...(acknowledgementUndecodable ? { acknowledgementUndecodable: true as const } : {}),
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
export function assertReviewLastEventClosureBinding(
|
|
2587
|
+
closure: ReviewLastEventClosureV1,
|
|
2588
|
+
binding: ReviewLastEventClosureBinding,
|
|
2589
|
+
): void {
|
|
2590
|
+
if (closure.lineageId !== binding.lineageId) throw new TypeError("last_event_closure lineage does not match its provider binding");
|
|
2591
|
+
if (binding.targetIdentity !== undefined && closure.targetIdentity !== undefined && closure.targetIdentity !== binding.targetIdentity) {
|
|
2592
|
+
throw new TypeError("last_event_closure target does not match its provider binding");
|
|
2593
|
+
}
|
|
2594
|
+
if (binding.targetIdentity !== undefined && closure.statusContinuation !== undefined && closure.statusContinuation.binding.targetIdentity !== binding.targetIdentity) {
|
|
2595
|
+
throw new TypeError("last_event_closure status continuation target does not match its provider binding");
|
|
2596
|
+
}
|
|
2597
|
+
if (binding.requestHash !== undefined && closure.requestHash !== undefined && closure.requestHash !== binding.requestHash) {
|
|
2598
|
+
throw new TypeError("last_event_closure request hash does not match its provider binding");
|
|
2599
|
+
}
|
|
2600
|
+
}
|