gentle-pi 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +70 -12
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -0,0 +1,704 @@
1
+ // gentle-pi provider contract bundle verifier (gentle-pi#311 P1/P2).
2
+ //
3
+ // Consumes the data-only `gentle-ai-review-provider-contract-<semver>.tar.gz`
4
+ // release bundle (or its extracted tree) and verifies it fail-closed before
5
+ // any layout, role, schema, or capability inside it is trusted:
6
+ //
7
+ // - exact eight-entry inventory (README.md, manifest.json, 3 schemas, 3 vectors);
8
+ // - strict manifest decode: single JSON object, no duplicate keys, no unknown fields;
9
+ // - per-file SHA-256 agreement between the manifest and the actual bytes;
10
+ // - pinned transport capability and supported contract major (1);
11
+ // - exactly the three provider roles `lens`, `refuter`, `targeted-validator`;
12
+ // - only known mandatory capabilities;
13
+ // - `runtimes` REQUIRED from contract 1.1.0 and it must register `pi`
14
+ // (a 1.0.x manifest without `runtimes` is a valid bundle, but the relay
15
+ // must then treat the `pi` runtime identity as unregistered);
16
+ // - confined in-memory archive reading: regular tar entries only, canonical
17
+ // relative paths, no symlinks, no path escapes, safe modes, bounded sizes.
18
+ //
19
+ // Gentle AI remains the sole review authority. This module never derives
20
+ // admission semantics from the bundle; it only proves the bundle is the exact
21
+ // artifact the provider published before generated baselines are built from it.
22
+
23
+ import { createHash } from "node:crypto";
24
+ import { lstatSync, readFileSync, readdirSync } from "node:fs";
25
+ import { posix as posixPath, join as joinPath } from "node:path";
26
+ import { gunzipSync } from "node:zlib";
27
+
28
+ export const PROVIDER_CONTRACT_BUNDLE_SCHEMA = "gentle-ai.review-provider-contract-bundle/v1";
29
+ export const PROVIDER_TRANSPORT_CAPABILITY = "gentle-ai.provider-transport/v1";
30
+ export const PI_RUNTIME_IDENTITY = "pi";
31
+ export const SUPPORTED_PROVIDER_CONTRACT_MAJOR = 1;
32
+ // `runtimes` became part of the manifest in contract 1.1.0.
33
+ export const RUNTIMES_REQUIRED_SINCE_MINOR = 1;
34
+ // Exactly the provider roles gentle-pi supports, in the manifest's strict order.
35
+ export const PROVIDER_CONTRACT_ROLE_IDS = ["lens", "refuter", "targeted-validator"] as const;
36
+ // Every mandatory capability a role may declare. An unknown mandatory
37
+ // capability means this gentle-pi build cannot satisfy the role: fail closed.
38
+ export const KNOWN_MANDATORY_CAPABILITIES: readonly string[] = [PROVIDER_TRANSPORT_CAPABILITY];
39
+
40
+ const MAX_ARCHIVE_BYTES = 8 * 1024 * 1024;
41
+ const MAX_FILE_BYTES = 4 * 1024 * 1024;
42
+ const MAX_BUNDLE_BYTES = 16 * 1024 * 1024;
43
+ const MAX_MANIFEST_BYTES = 64 * 1024;
44
+ const BUNDLE_ENTRY_COUNT = 8;
45
+ // setuid/setgid/sticky, any execute bit, or world-writable content is never
46
+ // acceptable for a data-only contract bundle.
47
+ const FORBIDDEN_MODE_BITS = 0o7113;
48
+
49
+ const SEMVER_PATTERN = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
50
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
51
+ const RUNTIME_IDENTITY_PATTERN = /^[a-z][a-z0-9-]*$/;
52
+
53
+ export const PROVIDER_CONTRACT_BUNDLE_INVALID = "invalid-provider-contract-bundle";
54
+
55
+ export class ProviderContractBundleError extends Error {
56
+ readonly code = PROVIDER_CONTRACT_BUNDLE_INVALID;
57
+ constructor(message: string) {
58
+ super(`${PROVIDER_CONTRACT_BUNDLE_INVALID}: ${message}`);
59
+ this.name = "ProviderContractBundleError";
60
+ }
61
+ }
62
+
63
+ function invalid(message: string): never {
64
+ throw new ProviderContractBundleError(message);
65
+ }
66
+
67
+ export interface VerifiedProviderRole {
68
+ readonly id: string;
69
+ readonly requestSchemaId: string;
70
+ readonly resultSchemaId: string;
71
+ readonly requiredCapabilities: readonly string[];
72
+ readonly schemaPath: string;
73
+ readonly schemaSha256: string;
74
+ readonly vectorPath: string;
75
+ readonly vectorSha256: string;
76
+ }
77
+
78
+ export interface VerifiedProviderContractBundle {
79
+ readonly contractSemver: string;
80
+ readonly major: number;
81
+ readonly minor: number;
82
+ readonly patch: number;
83
+ readonly transportCapability: string;
84
+ /** Present exactly when the manifest declares the runtime registry (>= 1.1.0). */
85
+ readonly runtimes: readonly string[] | undefined;
86
+ readonly roles: readonly VerifiedProviderRole[];
87
+ /** Canonical path -> exact verified bytes for all eight entries. */
88
+ readonly entries: ReadonlyMap<string, Buffer>;
89
+ /** Canonical path -> SHA-256 of the exact verified bytes. */
90
+ readonly entrySha256: ReadonlyMap<string, string>;
91
+ /** Deterministic digest over the sorted `<sha256> <path>` inventory. */
92
+ readonly treeSha256: string;
93
+ }
94
+
95
+ export interface PiRuntimeRegistration {
96
+ readonly registered: boolean;
97
+ readonly reason?: string;
98
+ }
99
+
100
+ function sha256Hex(bytes: Buffer): string {
101
+ return createHash("sha256").update(bytes).digest("hex");
102
+ }
103
+
104
+ // --- strict JSON (single object, no duplicate keys, no trailing data) -------
105
+
106
+ class StrictJsonScanner {
107
+ private index = 0;
108
+ private readonly text: string;
109
+ constructor(text: string) {
110
+ this.text = text;
111
+ }
112
+
113
+ scanDocument(): unknown {
114
+ this.skipWhitespace();
115
+ if (this.peek() !== "{") invalid("manifest must be a single JSON object");
116
+ const value = this.scanValue();
117
+ this.skipWhitespace();
118
+ if (this.index !== this.text.length) invalid("manifest has trailing data after its JSON object");
119
+ return value;
120
+ }
121
+
122
+ private peek(): string {
123
+ if (this.index >= this.text.length) invalid("manifest JSON ends unexpectedly");
124
+ return this.text[this.index] as string;
125
+ }
126
+
127
+ private next(): string {
128
+ const character = this.peek();
129
+ this.index += 1;
130
+ return character;
131
+ }
132
+
133
+ private skipWhitespace(): void {
134
+ while (this.index < this.text.length && " \t\n\r".includes(this.text[this.index] as string)) this.index += 1;
135
+ }
136
+
137
+ private scanValue(): unknown {
138
+ this.skipWhitespace();
139
+ const character = this.peek();
140
+ if (character === "{") return this.scanObject();
141
+ if (character === "[") return this.scanArray();
142
+ if (character === '"') return this.scanString();
143
+ if (character === "-" || (character >= "0" && character <= "9")) return this.scanNumber();
144
+ if (this.text.startsWith("true", this.index)) { this.index += 4; return true; }
145
+ if (this.text.startsWith("false", this.index)) { this.index += 5; return false; }
146
+ if (this.text.startsWith("null", this.index)) { this.index += 4; return null; }
147
+ return invalid(`manifest JSON has an unexpected token at offset ${this.index}`);
148
+ }
149
+
150
+ private scanObject(): Record<string, unknown> {
151
+ this.next();
152
+ const value: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
153
+ this.skipWhitespace();
154
+ if (this.peek() === "}") { this.next(); return value; }
155
+ for (;;) {
156
+ this.skipWhitespace();
157
+ if (this.peek() !== '"') invalid("manifest JSON object key is not a string");
158
+ const key = this.scanString();
159
+ if (Object.prototype.hasOwnProperty.call(value, key)) invalid(`manifest JSON has duplicate key ${JSON.stringify(key)}`);
160
+ this.skipWhitespace();
161
+ if (this.next() !== ":") invalid("manifest JSON object is missing a colon");
162
+ value[key] = this.scanValue();
163
+ this.skipWhitespace();
164
+ const separator = this.next();
165
+ if (separator === "}") return value;
166
+ if (separator !== ",") invalid("manifest JSON object is malformed");
167
+ }
168
+ }
169
+
170
+ private scanArray(): unknown[] {
171
+ this.next();
172
+ const values: unknown[] = [];
173
+ this.skipWhitespace();
174
+ if (this.peek() === "]") { this.next(); return values; }
175
+ for (;;) {
176
+ values.push(this.scanValue());
177
+ this.skipWhitespace();
178
+ const separator = this.next();
179
+ if (separator === "]") return values;
180
+ if (separator !== ",") invalid("manifest JSON array is malformed");
181
+ }
182
+ }
183
+
184
+ private scanString(): string {
185
+ if (this.next() !== '"') invalid("manifest JSON string is malformed");
186
+ let value = "";
187
+ for (;;) {
188
+ const character = this.next();
189
+ if (character === '"') return value;
190
+ if (character === "\\") {
191
+ const escape = this.next();
192
+ if (escape === '"' || escape === "\\" || escape === "/") value += escape;
193
+ else if (escape === "b") value += "\b";
194
+ else if (escape === "f") value += "\f";
195
+ else if (escape === "n") value += "\n";
196
+ else if (escape === "r") value += "\r";
197
+ else if (escape === "t") value += "\t";
198
+ else if (escape === "u") {
199
+ const hex = this.text.slice(this.index, this.index + 4);
200
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) invalid("manifest JSON has an invalid unicode escape");
201
+ this.index += 4;
202
+ value += String.fromCharCode(Number.parseInt(hex, 16));
203
+ } else invalid("manifest JSON has an invalid escape sequence");
204
+ } else if (character.charCodeAt(0) < 0x20) {
205
+ invalid("manifest JSON string has an unescaped control character");
206
+ } else {
207
+ value += character;
208
+ }
209
+ }
210
+ }
211
+
212
+ private scanNumber(): number {
213
+ const start = this.index;
214
+ if (this.peek() === "-") this.index += 1;
215
+ while (this.index < this.text.length && /[0-9.eE+-]/.test(this.text[this.index] as string)) this.index += 1;
216
+ const literal = this.text.slice(start, this.index);
217
+ if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/.test(literal)) invalid("manifest JSON number is malformed");
218
+ return Number(literal);
219
+ }
220
+ }
221
+
222
+ function decodeStrictManifestObject(payload: Buffer): Record<string, unknown> {
223
+ if (payload.length > MAX_MANIFEST_BYTES) invalid("manifest.json exceeds its size limit");
224
+ if (payload.length >= 3 && payload[0] === 0xef && payload[1] === 0xbb && payload[2] === 0xbf) invalid("manifest.json must not carry a byte-order mark");
225
+ const value = new StrictJsonScanner(payload.toString("utf8")).scanDocument();
226
+ return value as Record<string, unknown>;
227
+ }
228
+
229
+ // --- manifest shape ----------------------------------------------------------
230
+
231
+ function requireExactKeys(value: Record<string, unknown>, allowed: readonly string[], required: readonly string[], where: string): void {
232
+ for (const key of Object.keys(value)) {
233
+ if (!allowed.includes(key)) invalid(`${where} has unknown field ${JSON.stringify(key)}`);
234
+ }
235
+ for (const key of required) {
236
+ if (!Object.prototype.hasOwnProperty.call(value, key)) invalid(`${where} is missing field ${JSON.stringify(key)}`);
237
+ }
238
+ }
239
+
240
+ function requireString(value: unknown, where: string): string {
241
+ if (typeof value !== "string" || value === "") invalid(`${where} must be a non-empty string`);
242
+ return value;
243
+ }
244
+
245
+ function requireObject(value: unknown, where: string): Record<string, unknown> {
246
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalid(`${where} must be a JSON object`);
247
+ return value as Record<string, unknown>;
248
+ }
249
+
250
+ interface ManifestFileReference {
251
+ readonly path: string;
252
+ readonly sha256: string;
253
+ }
254
+
255
+ function parseFileReference(value: unknown, where: string): ManifestFileReference {
256
+ const object = requireObject(value, where);
257
+ requireExactKeys(object, ["path", "sha256"], ["path", "sha256"], where);
258
+ const path = requireString(object.path, `${where}.path`);
259
+ const sha256 = requireString(object.sha256, `${where}.sha256`);
260
+ if (!isCanonicalBundlePath(path)) invalid(`${where}.path ${JSON.stringify(path)} is unsafe`);
261
+ if (!SHA256_PATTERN.test(sha256)) invalid(`${where}.sha256 is not a lowercase hex SHA-256`);
262
+ return { path, sha256 };
263
+ }
264
+
265
+ interface ManifestRole {
266
+ readonly id: string;
267
+ readonly requestSchemaId: string;
268
+ readonly resultSchemaId: string;
269
+ readonly requiredCapabilities: readonly string[];
270
+ readonly schema: ManifestFileReference;
271
+ readonly vector: ManifestFileReference;
272
+ }
273
+
274
+ function parseManifestRole(value: unknown, index: number): ManifestRole {
275
+ const where = `manifest role ${index}`;
276
+ const object = requireObject(value, where);
277
+ requireExactKeys(
278
+ object,
279
+ ["id", "request_schema_id", "result_schema_id", "required_capabilities", "schema", "vector"],
280
+ ["id", "request_schema_id", "result_schema_id", "required_capabilities", "schema", "vector"],
281
+ where,
282
+ );
283
+ const id = requireString(object.id, `${where}.id`);
284
+ if (!Array.isArray(object.required_capabilities) || object.required_capabilities.length === 0) {
285
+ invalid(`${where}.required_capabilities must be a non-empty array`);
286
+ }
287
+ const requiredCapabilities = object.required_capabilities.map((capability, capabilityIndex) =>
288
+ requireString(capability, `${where}.required_capabilities[${capabilityIndex}]`),
289
+ );
290
+ for (let position = 0; position < requiredCapabilities.length; position += 1) {
291
+ const capability = requiredCapabilities[position] as string;
292
+ if (position > 0 && (requiredCapabilities[position - 1] as string) >= capability) {
293
+ invalid(`${where}.required_capabilities must be strictly sorted and unique`);
294
+ }
295
+ if (!KNOWN_MANDATORY_CAPABILITIES.includes(capability)) {
296
+ invalid(`${where} declares unknown mandatory capability ${JSON.stringify(capability)}`);
297
+ }
298
+ }
299
+ return {
300
+ id,
301
+ requestSchemaId: requireString(object.request_schema_id, `${where}.request_schema_id`),
302
+ resultSchemaId: requireString(object.result_schema_id, `${where}.result_schema_id`),
303
+ requiredCapabilities,
304
+ schema: parseFileReference(object.schema, `${where}.schema`),
305
+ vector: parseFileReference(object.vector, `${where}.vector`),
306
+ };
307
+ }
308
+
309
+ interface ParsedManifest {
310
+ readonly contractSemver: string;
311
+ readonly major: number;
312
+ readonly minor: number;
313
+ readonly patch: number;
314
+ readonly transportCapability: string;
315
+ readonly runtimes: readonly string[] | undefined;
316
+ readonly readme: ManifestFileReference;
317
+ readonly roles: readonly ManifestRole[];
318
+ }
319
+
320
+ function parseManifest(payload: Buffer): ParsedManifest {
321
+ const object = decodeStrictManifestObject(payload);
322
+ requireExactKeys(
323
+ object,
324
+ ["schema", "contract_semver", "transport_capability", "runtimes", "readme", "roles"],
325
+ ["schema", "contract_semver", "transport_capability", "readme", "roles"],
326
+ "manifest",
327
+ );
328
+ if (object.schema !== PROVIDER_CONTRACT_BUNDLE_SCHEMA) {
329
+ invalid(`manifest schema must be ${JSON.stringify(PROVIDER_CONTRACT_BUNDLE_SCHEMA)}`);
330
+ }
331
+ const contractSemver = requireString(object.contract_semver, "manifest.contract_semver");
332
+ const semverMatch = SEMVER_PATTERN.exec(contractSemver);
333
+ if (!semverMatch) invalid(`manifest.contract_semver ${JSON.stringify(contractSemver)} is not exact MAJOR.MINOR.PATCH`);
334
+ const major = Number(semverMatch[1]);
335
+ const minor = Number(semverMatch[2]);
336
+ const patch = Number(semverMatch[3]);
337
+ if (major !== SUPPORTED_PROVIDER_CONTRACT_MAJOR) {
338
+ invalid(`unsupported provider contract major ${major}; gentle-pi supports major ${SUPPORTED_PROVIDER_CONTRACT_MAJOR} only`);
339
+ }
340
+ const transportCapability = requireString(object.transport_capability, "manifest.transport_capability");
341
+ if (transportCapability !== PROVIDER_TRANSPORT_CAPABILITY) {
342
+ invalid(`manifest.transport_capability must be pinned to ${JSON.stringify(PROVIDER_TRANSPORT_CAPABILITY)}`);
343
+ }
344
+
345
+ const runtimesRequired = minor >= RUNTIMES_REQUIRED_SINCE_MINOR;
346
+ let runtimes: readonly string[] | undefined;
347
+ if (Object.prototype.hasOwnProperty.call(object, "runtimes")) {
348
+ if (!runtimesRequired) {
349
+ invalid(`manifest.runtimes is not part of contract ${contractSemver}; it exists from 1.${RUNTIMES_REQUIRED_SINCE_MINOR}.0`);
350
+ }
351
+ if (!Array.isArray(object.runtimes) || object.runtimes.length === 0) invalid("manifest.runtimes must be a non-empty array");
352
+ const identities = object.runtimes.map((runtime, index) => requireString(runtime, `manifest.runtimes[${index}]`));
353
+ for (let position = 0; position < identities.length; position += 1) {
354
+ const identity = identities[position] as string;
355
+ if (!RUNTIME_IDENTITY_PATTERN.test(identity)) invalid(`manifest.runtimes[${position}] ${JSON.stringify(identity)} is not a runtime identity`);
356
+ if (position > 0 && (identities[position - 1] as string) >= identity) invalid("manifest.runtimes must be strictly sorted and unique");
357
+ }
358
+ runtimes = identities;
359
+ } else if (runtimesRequired) {
360
+ invalid(`manifest.runtimes is required from contract 1.${RUNTIMES_REQUIRED_SINCE_MINOR}.0 (got ${contractSemver} without it)`);
361
+ }
362
+ if (runtimes !== undefined && !runtimes.includes(PI_RUNTIME_IDENTITY)) {
363
+ // A registry that exists but omits `pi` means this runtime identity is
364
+ // not registered for the published contract: the relay must not trust it.
365
+ invalid(`manifest.runtimes does not register the ${JSON.stringify(PI_RUNTIME_IDENTITY)} runtime identity`);
366
+ }
367
+
368
+ if (!Array.isArray(object.roles)) invalid("manifest.roles must be an array");
369
+ const roles = object.roles.map((role, index) => parseManifestRole(role, index));
370
+ if (roles.length !== PROVIDER_CONTRACT_ROLE_IDS.length) {
371
+ invalid(`manifest declares ${roles.length} roles; exactly ${PROVIDER_CONTRACT_ROLE_IDS.length} are supported`);
372
+ }
373
+ roles.forEach((role, index) => {
374
+ if (role.id !== PROVIDER_CONTRACT_ROLE_IDS[index]) {
375
+ invalid(`manifest role ${index} is ${JSON.stringify(role.id)}; expected ${JSON.stringify(PROVIDER_CONTRACT_ROLE_IDS[index])}`);
376
+ }
377
+ });
378
+
379
+ return {
380
+ contractSemver,
381
+ major,
382
+ minor,
383
+ patch,
384
+ transportCapability,
385
+ runtimes,
386
+ readme: parseFileReference(object.readme, "manifest.readme"),
387
+ roles,
388
+ };
389
+ }
390
+
391
+ // --- entry-level verification -------------------------------------------------
392
+
393
+ export function isCanonicalBundlePath(name: string): boolean {
394
+ if (name === "" || name.includes("\\") || name.startsWith("/") || posixPath.normalize(name) !== name) return false;
395
+ for (const component of name.split("/")) {
396
+ if (component === "" || component === "." || component === "..") return false;
397
+ }
398
+ return true;
399
+ }
400
+
401
+ function jsonDeepEqual(left: unknown, right: unknown): boolean {
402
+ if (left === right) return true;
403
+ if (Array.isArray(left) && Array.isArray(right)) {
404
+ return left.length === right.length && left.every((value, index) => jsonDeepEqual(value, right[index]));
405
+ }
406
+ if (typeof left === "object" && left !== null && typeof right === "object" && right !== null && !Array.isArray(left) && !Array.isArray(right)) {
407
+ const leftKeys = Object.keys(left as Record<string, unknown>).sort();
408
+ const rightKeys = Object.keys(right as Record<string, unknown>).sort();
409
+ return (
410
+ leftKeys.length === rightKeys.length &&
411
+ leftKeys.every(
412
+ (key, index) =>
413
+ key === rightKeys[index] &&
414
+ jsonDeepEqual((left as Record<string, unknown>)[key], (right as Record<string, unknown>)[key]),
415
+ )
416
+ );
417
+ }
418
+ return false;
419
+ }
420
+
421
+ function verifyFileReference(entries: ReadonlyMap<string, Buffer>, reference: ManifestFileReference, expectedPath: string): Buffer {
422
+ if (reference.path !== expectedPath) invalid(`manifest references ${JSON.stringify(reference.path)}; expected ${JSON.stringify(expectedPath)}`);
423
+ const payload = entries.get(reference.path);
424
+ if (payload === undefined) invalid(`bundle is missing ${JSON.stringify(expectedPath)}`);
425
+ if (sha256Hex(payload) !== reference.sha256) invalid(`bundle entry ${JSON.stringify(expectedPath)} does not match its manifest SHA-256`);
426
+ return payload;
427
+ }
428
+
429
+ function verifyRoleArtifacts(entries: ReadonlyMap<string, Buffer>, role: ManifestRole): void {
430
+ const schemaPath = `schemas/${role.id}.schema.json`;
431
+ const vectorPath = `vectors/${role.id}.json`;
432
+ const schemaPayload = verifyFileReference(entries, role.schema, schemaPath);
433
+ const vectorPayload = verifyFileReference(entries, role.vector, vectorPath);
434
+
435
+ let schemaDocument: unknown;
436
+ try {
437
+ schemaDocument = JSON.parse(schemaPayload.toString("utf8"));
438
+ } catch {
439
+ invalid(`bundle schema ${JSON.stringify(schemaPath)} is not valid JSON`);
440
+ }
441
+ const schemaObject = requireObject(schemaDocument, `bundle schema ${JSON.stringify(schemaPath)}`);
442
+ if (schemaObject["$id"] !== role.resultSchemaId) {
443
+ invalid(`bundle schema ${JSON.stringify(schemaPath)} $id does not match the manifest result schema id`);
444
+ }
445
+ const examples = schemaObject.examples;
446
+ if (!Array.isArray(examples) || examples.length === 0) invalid(`bundle schema ${JSON.stringify(schemaPath)} has no canonical example`);
447
+
448
+ let vectorDocument: unknown;
449
+ try {
450
+ vectorDocument = JSON.parse(vectorPayload.toString("utf8"));
451
+ } catch {
452
+ invalid(`bundle vector ${JSON.stringify(vectorPath)} is not valid JSON`);
453
+ }
454
+ requireObject(vectorDocument, `bundle vector ${JSON.stringify(vectorPath)}`);
455
+ if (!vectorPayload.toString("utf8").endsWith("\n")) invalid(`bundle vector ${JSON.stringify(vectorPath)} must end with a newline`);
456
+ if (!jsonDeepEqual(vectorDocument, examples[0])) {
457
+ invalid(`bundle vector ${JSON.stringify(vectorPath)} does not equal the schema's canonical example`);
458
+ }
459
+ }
460
+
461
+ export function bundleTreeSha256(entrySha256: ReadonlyMap<string, string>): string {
462
+ const lines = [...entrySha256.entries()]
463
+ .sort(([leftPath], [rightPath]) => (leftPath < rightPath ? -1 : 1))
464
+ .map(([path, digest]) => `${digest} ${path}\n`);
465
+ return sha256Hex(Buffer.from(lines.join(""), "utf8"));
466
+ }
467
+
468
+ /** Verifies a complete in-memory bundle inventory. The single trust anchor. */
469
+ export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<string, Buffer>): VerifiedProviderContractBundle {
470
+ if (rawEntries.size !== BUNDLE_ENTRY_COUNT) {
471
+ invalid(`bundle inventory has ${rawEntries.size} files; exactly ${BUNDLE_ENTRY_COUNT} are expected`);
472
+ }
473
+ for (const [name, payload] of rawEntries) {
474
+ if (!isCanonicalBundlePath(name)) invalid(`bundle path ${JSON.stringify(name)} is unsafe`);
475
+ if (payload.length > MAX_FILE_BYTES) invalid(`bundle entry ${JSON.stringify(name)} exceeds its size limit`);
476
+ }
477
+ const manifestPayload = rawEntries.get("manifest.json");
478
+ if (manifestPayload === undefined) invalid("bundle is missing manifest.json");
479
+ const manifest = parseManifest(manifestPayload);
480
+
481
+ verifyFileReference(rawEntries, manifest.readme, "README.md");
482
+ const expected = new Set(["README.md", "manifest.json"]);
483
+ for (const role of manifest.roles) {
484
+ verifyRoleArtifacts(rawEntries, role);
485
+ expected.add(`schemas/${role.id}.schema.json`);
486
+ expected.add(`vectors/${role.id}.json`);
487
+ }
488
+ for (const name of rawEntries.keys()) {
489
+ if (!expected.has(name)) invalid(`bundle has unexpected file ${JSON.stringify(name)}`);
490
+ }
491
+
492
+ const entries = new Map<string, Buffer>();
493
+ const entrySha256 = new Map<string, string>();
494
+ for (const name of [...rawEntries.keys()].sort()) {
495
+ const payload = rawEntries.get(name) as Buffer;
496
+ entries.set(name, Buffer.from(payload));
497
+ entrySha256.set(name, sha256Hex(payload));
498
+ }
499
+
500
+ return {
501
+ contractSemver: manifest.contractSemver,
502
+ major: manifest.major,
503
+ minor: manifest.minor,
504
+ patch: manifest.patch,
505
+ transportCapability: manifest.transportCapability,
506
+ runtimes: manifest.runtimes,
507
+ roles: manifest.roles.map((role) => ({
508
+ id: role.id,
509
+ requestSchemaId: role.requestSchemaId,
510
+ resultSchemaId: role.resultSchemaId,
511
+ requiredCapabilities: [...role.requiredCapabilities],
512
+ schemaPath: role.schema.path,
513
+ schemaSha256: role.schema.sha256,
514
+ vectorPath: role.vector.path,
515
+ vectorSha256: role.vector.sha256,
516
+ })),
517
+ entries,
518
+ entrySha256,
519
+ treeSha256: bundleTreeSha256(entrySha256),
520
+ };
521
+ }
522
+
523
+ /**
524
+ * Reports whether the verified bundle registers the `pi` runtime identity.
525
+ *
526
+ * A 1.0.x bundle predates the runtime registry: it is a valid bundle, but the
527
+ * relay must treat `pi` as unregistered and must not trust the layout for
528
+ * host-relay work. From 1.1.0 the registry is mandatory and `pi` membership is
529
+ * already enforced during verification, so a verified >=1.1.0 bundle is
530
+ * always registered.
531
+ */
532
+ export function piRuntimeRegistration(bundle: VerifiedProviderContractBundle): PiRuntimeRegistration {
533
+ if (bundle.runtimes === undefined) {
534
+ return {
535
+ registered: false,
536
+ reason: `contract ${bundle.contractSemver} predates the runtime identity registry; the ${JSON.stringify(PI_RUNTIME_IDENTITY)} runtime identity is not registered and the relay must not trust this bundle`,
537
+ };
538
+ }
539
+ return { registered: bundle.runtimes.includes(PI_RUNTIME_IDENTITY) };
540
+ }
541
+
542
+ // --- extracted tree reading ---------------------------------------------------
543
+
544
+ function assertSafeDirectory(path: string): void {
545
+ const details = lstatSync(path);
546
+ if (details.isSymbolicLink() || !details.isDirectory()) invalid(`bundle directory ${JSON.stringify(path)} is not a real directory`);
547
+ }
548
+
549
+ /** Reads and verifies an already-extracted local bundle tree. */
550
+ export function verifyProviderContractBundleTree(directory: string): VerifiedProviderContractBundle {
551
+ assertSafeDirectory(directory);
552
+ const entries = new Map<string, Buffer>();
553
+ const walk = (current: string, relative: string): void => {
554
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
555
+ const absolute = joinPath(current, entry.name);
556
+ const relativeName = relative === "" ? entry.name : `${relative}/${entry.name}`;
557
+ const details = lstatSync(absolute);
558
+ if (details.isSymbolicLink()) invalid(`bundle tree entry ${JSON.stringify(relativeName)} is a symlink`);
559
+ if (details.isDirectory()) {
560
+ walk(absolute, relativeName);
561
+ continue;
562
+ }
563
+ if (!details.isFile()) invalid(`bundle tree entry ${JSON.stringify(relativeName)} is not a regular file`);
564
+ if (process.platform !== "win32" && (details.mode & FORBIDDEN_MODE_BITS) !== 0) {
565
+ invalid(`bundle tree entry ${JSON.stringify(relativeName)} has an unsafe file mode`);
566
+ }
567
+ if (!isCanonicalBundlePath(relativeName)) invalid(`bundle tree path ${JSON.stringify(relativeName)} is unsafe`);
568
+ if (details.size > MAX_FILE_BYTES) invalid(`bundle tree entry ${JSON.stringify(relativeName)} exceeds its size limit`);
569
+ if (entries.size >= BUNDLE_ENTRY_COUNT) invalid(`bundle tree has more than ${BUNDLE_ENTRY_COUNT} files`);
570
+ entries.set(relativeName, readFileSync(absolute));
571
+ }
572
+ };
573
+ walk(directory, "");
574
+ return verifyProviderContractBundleEntries(entries);
575
+ }
576
+
577
+ // --- archive reading (bounded, in memory, never extracted to disk) ------------
578
+
579
+ const TAR_BLOCK_SIZE = 512;
580
+
581
+ function parseOctalField(field: Buffer, where: string): number {
582
+ const text = field.toString("ascii").replace(/[\0 ]+$/g, "").replace(/^[\0 ]+/g, "");
583
+ if (text === "") return 0;
584
+ if (!/^[0-7]+$/.test(text)) invalid(`tar ${where} field is not octal`);
585
+ return Number.parseInt(text, 8);
586
+ }
587
+
588
+ function verifyTarHeaderChecksum(header: Buffer): void {
589
+ const recorded = parseOctalField(header.subarray(148, 156), "checksum");
590
+ let computed = 0;
591
+ for (let index = 0; index < TAR_BLOCK_SIZE; index += 1) {
592
+ computed += index >= 148 && index < 156 ? 0x20 : (header[index] as number);
593
+ }
594
+ if (computed !== recorded) invalid("tar header checksum does not match");
595
+ }
596
+
597
+ function readTarEntries(expanded: Buffer): Map<string, Buffer> {
598
+ const entries = new Map<string, Buffer>();
599
+ let offset = 0;
600
+ let totalBytes = 0;
601
+ for (;;) {
602
+ if (expanded.length - offset < TAR_BLOCK_SIZE) invalid("tar stream ends before a complete header");
603
+ const header = expanded.subarray(offset, offset + TAR_BLOCK_SIZE);
604
+ offset += TAR_BLOCK_SIZE;
605
+ if (header.every((byte) => byte === 0)) {
606
+ if (expanded.length - offset < TAR_BLOCK_SIZE) invalid("tar stream has an incomplete terminator");
607
+ const second = expanded.subarray(offset, offset + TAR_BLOCK_SIZE);
608
+ if (!second.every((byte) => byte === 0)) invalid("tar stream has an incomplete terminator");
609
+ offset += TAR_BLOCK_SIZE;
610
+ for (; offset < expanded.length; offset += 1) {
611
+ if (expanded[offset] !== 0) invalid("tar stream has data after its terminator");
612
+ }
613
+ return entries;
614
+ }
615
+ verifyTarHeaderChecksum(header);
616
+ const typeflag = header[156] as number;
617
+ // Only plain regular files: no symlinks, links, directories, devices,
618
+ // and no PAX/GNU metadata entries that could smuggle a second identity.
619
+ if (typeflag !== 0x30 && typeflag !== 0) invalid(`tar entry type ${JSON.stringify(String.fromCharCode(typeflag))} is forbidden`);
620
+ const prefix = header.subarray(345, 500).toString("ascii").replace(/\0+$/g, "");
621
+ if (prefix !== "") invalid("tar entry uses the ustar prefix field; canonical bundle paths never need it");
622
+ const name = header.subarray(0, 100).toString("ascii").replace(/\0+$/g, "");
623
+ if (!isCanonicalBundlePath(name)) invalid(`tar entry path ${JSON.stringify(name)} is unsafe`);
624
+ if (entries.has(name)) invalid(`tar entry path ${JSON.stringify(name)} is duplicated`);
625
+ const mode = parseOctalField(header.subarray(100, 108), "mode");
626
+ if ((mode & FORBIDDEN_MODE_BITS) !== 0) invalid(`tar entry ${JSON.stringify(name)} has an unsafe file mode`);
627
+ const size = parseOctalField(header.subarray(124, 136), "size");
628
+ if (size > MAX_FILE_BYTES) invalid(`tar entry ${JSON.stringify(name)} exceeds its size limit`);
629
+ totalBytes += size;
630
+ if (totalBytes > MAX_BUNDLE_BYTES) invalid("tar entries exceed the bundle size limit");
631
+ if (entries.size >= BUNDLE_ENTRY_COUNT) invalid(`tar has more than ${BUNDLE_ENTRY_COUNT} entries`);
632
+ const padding = (TAR_BLOCK_SIZE - (size % TAR_BLOCK_SIZE)) % TAR_BLOCK_SIZE;
633
+ if (expanded.length - offset < size + padding) invalid("tar stream ends before a complete entry");
634
+ entries.set(name, Buffer.from(expanded.subarray(offset, offset + size)));
635
+ offset += size + padding;
636
+ }
637
+ }
638
+
639
+ export interface VerifiedProviderContractArchive {
640
+ readonly bundle: VerifiedProviderContractBundle;
641
+ readonly archiveSha256: string;
642
+ }
643
+
644
+ /** Verifies a local `gentle-ai-review-provider-contract-<semver>.tar.gz` archive in memory. */
645
+ export function verifyProviderContractBundleArchive(archivePath: string): VerifiedProviderContractArchive {
646
+ const details = lstatSync(archivePath);
647
+ if (details.isSymbolicLink() || !details.isFile()) invalid("archive is not a regular file");
648
+ if (details.size > MAX_ARCHIVE_BYTES) invalid("archive exceeds its size limit");
649
+ const compressed = readFileSync(archivePath);
650
+ let expanded: Buffer;
651
+ try {
652
+ expanded = gunzipSync(compressed, { maxOutputLength: MAX_BUNDLE_BYTES + 1 });
653
+ } catch {
654
+ invalid("archive is not a readable gzip stream within the bundle size limit");
655
+ }
656
+ if (expanded.length > MAX_BUNDLE_BYTES) invalid("expanded tar exceeds the bundle size limit");
657
+ const entries = readTarEntries(expanded);
658
+ return {
659
+ bundle: verifyProviderContractBundleEntries(entries),
660
+ archiveSha256: sha256Hex(compressed),
661
+ };
662
+ }
663
+
664
+ // --- generated consumer contract surface (P2) ---------------------------------
665
+
666
+ export const PROVIDER_ROLES_BASELINE_SCHEMA = "gentle-pi.provider-contract-roles-baseline/v1";
667
+ export const PROVIDER_CAPABILITIES_BASELINE_SCHEMA = "gentle-pi.provider-contract-capabilities-baseline/v1";
668
+ export const PROVIDER_ROLES_BASELINE_FILE = "provider-roles.baseline.json";
669
+ export const PROVIDER_CAPABILITIES_BASELINE_FILE = "provider-capabilities.baseline.json";
670
+
671
+ /**
672
+ * Deterministically derives the provider-owned consumer contract surface from
673
+ * a verified bundle. Pi overlays never live in these files; they are pure
674
+ * projections of the provider bundle and are regenerated, never hand-edited.
675
+ */
676
+ export function generateProviderContractBaselines(bundle: VerifiedProviderContractBundle): ReadonlyMap<string, string> {
677
+ const roles = {
678
+ schema: PROVIDER_ROLES_BASELINE_SCHEMA,
679
+ contract_semver: bundle.contractSemver,
680
+ roles: bundle.roles.map((role) => ({
681
+ id: role.id,
682
+ request_schema_id: role.requestSchemaId,
683
+ result_schema_id: role.resultSchemaId,
684
+ required_capabilities: [...role.requiredCapabilities],
685
+ schema_path: role.schemaPath,
686
+ schema_sha256: role.schemaSha256,
687
+ vector_path: role.vectorPath,
688
+ vector_sha256: role.vectorSha256,
689
+ })),
690
+ };
691
+ const mandatory = [...new Set(bundle.roles.flatMap((role) => [...role.requiredCapabilities]))].sort();
692
+ const capabilities = {
693
+ schema: PROVIDER_CAPABILITIES_BASELINE_SCHEMA,
694
+ contract_semver: bundle.contractSemver,
695
+ transport_capability: bundle.transportCapability,
696
+ mandatory_capabilities: mandatory,
697
+ runtimes: bundle.runtimes === undefined ? null : [...bundle.runtimes],
698
+ pi_registered: piRuntimeRegistration(bundle).registered,
699
+ };
700
+ return new Map([
701
+ [PROVIDER_ROLES_BASELINE_FILE, `${JSON.stringify(roles, null, 2)}\n`],
702
+ [PROVIDER_CAPABILITIES_BASELINE_FILE, `${JSON.stringify(capabilities, null, 2)}\n`],
703
+ ]);
704
+ }