@odla-ai/chapter 0.47.9 → 0.48.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/dist/testing.js CHANGED
@@ -1,4 +1,143 @@
1
+ // src/testing-commercial.ts
2
+ var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3
+ var id = (value) => typeof value === "string" && /^[a-z0-9][a-z0-9_.:-]{1,159}$/.test(value);
4
+ var time = (value) => Number.isSafeInteger(value) && Number(value) > 0;
5
+ function validateCommercialCases(value, skus, path) {
6
+ if (value === void 0) return [];
7
+ if (!Array.isArray(value) || !value.length || value.length > 32) return [`${path} must be a non-empty array of at most 32 cases`];
8
+ const errors = [];
9
+ const ids = /* @__PURE__ */ new Set();
10
+ for (const [index, raw] of value.entries()) {
11
+ const item = object(raw);
12
+ const at = `${path}[${index}]`;
13
+ if (!item) {
14
+ errors.push(`${at} must be an object`);
15
+ continue;
16
+ }
17
+ if (!id(item.id) || ids.has(String(item.id))) errors.push(`${at}.id must be a unique stable id`);
18
+ ids.add(String(item.id));
19
+ const sku = skus.find((candidate) => candidate.id === item.skuId && candidate.active && candidate.kind === "membership" && candidate.priceCents > 0);
20
+ if (!sku) errors.push(`${at}.skuId must name an active paid base membership SKU`);
21
+ if (sku?.compareAtPriceCents !== void 0 || sku?.discountDuration !== void 0) {
22
+ errors.push(`${at} must not combine a discounted SKU with a separate commercial policy`);
23
+ }
24
+ if (!id(item.offeringVersionId)) errors.push(`${at}.offeringVersionId is required`);
25
+ if (typeof item.founding !== "boolean" || item.founding && !id(item.cohortPolicyVersionId) || item.cohortPolicyVersionId !== void 0 && !id(item.cohortPolicyVersionId)) errors.push(`${at}.founding requires a stable cohort policy identity`);
26
+ if (!time(item.purchasedAt) || !time(item.initialTermEndsAt) || item.initialTermEndsAt <= item.purchasedAt) {
27
+ errors.push(`${at} must specify a purchase instant and a later initial term end`);
28
+ }
29
+ const discount = object(item.discount);
30
+ if (item.discount !== void 0 && (!discount || !id(discount.policyVersionId) || !Number.isInteger(discount.percentOffBps) || Number(discount.percentOffBps) <= 0 || Number(discount.percentOffBps) >= 1e4 || !["first_term", "while_active", "fixed_end"].includes(String(discount.duration)) || (discount.duration === "fixed_end" ? !time(discount.endsAt) || Number(discount.endsAt) <= Number(item.purchasedAt) : discount.endsAt !== void 0))) {
31
+ errors.push(`${at}.discount must specify a version, percentage, and explicit expiry policy`);
32
+ }
33
+ if (sku) {
34
+ const due = Math.round(sku.priceCents * (1e4 - Number(discount?.percentOffBps ?? 0)) / 1e4);
35
+ const ongoing = discount?.duration === "while_active" || discount?.duration === "fixed_end" && Number(discount.endsAt) > Number(item.initialTermEndsAt);
36
+ if (item.dueTodayCents !== due || item.renewalCents !== (ongoing ? due : sku.priceCents)) {
37
+ errors.push(`${at} expected initial and renewal amounts must match the separate policy on the base price`);
38
+ }
39
+ }
40
+ }
41
+ return errors;
42
+ }
43
+ function assertCommercialQuote(expected, sku, actual) {
44
+ const quote = object(actual), lines = object(quote?.lineItems), terms = object(quote?.commercial);
45
+ return Boolean(quote && terms && sku.id === expected.skuId && quote.tierId === expected.skuId && quote.currency === sku.currency && quote.interval === sku.billing && lines?.standardCents === sku.priceCents && lines.discountCents === sku.priceCents - expected.dueTodayCents && lines.dueTodayCents === expected.dueTodayCents && terms.offeringVersionId === expected.offeringVersionId && terms.initialTermEndsAt === expected.initialTermEndsAt && terms.renewalCents === expected.renewalCents && terms.autoRenew === true && terms.discountPolicyVersionId === expected.discount?.policyVersionId && terms.discountDuration === expected.discount?.duration);
46
+ }
47
+
1
48
  // src/testing-probe.ts
49
+ var MAX_BODY_BYTES = 64 * 1024;
50
+ var TIMEOUT_MS = 1e4;
51
+ function record(value) {
52
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
53
+ }
54
+ async function readBoundedJson(response, signal) {
55
+ const reader = response.body?.getReader();
56
+ if (!reader) throw new Error("invalid_json");
57
+ const chunks = [];
58
+ let size = 0;
59
+ const abort = () => {
60
+ void reader.cancel().catch(() => {
61
+ });
62
+ };
63
+ signal.addEventListener("abort", abort, { once: true });
64
+ try {
65
+ if (signal.aborted) throw new Error("response_failed");
66
+ for (; ; ) {
67
+ const { done, value } = await reader.read();
68
+ if (done) break;
69
+ size += value.byteLength;
70
+ if (size > MAX_BODY_BYTES) throw new Error("response_too_large");
71
+ chunks.push(value);
72
+ }
73
+ const bytes = new Uint8Array(size);
74
+ let offset = 0;
75
+ for (const chunk of chunks) {
76
+ bytes.set(chunk, offset);
77
+ offset += chunk.byteLength;
78
+ }
79
+ try {
80
+ return JSON.parse(new TextDecoder().decode(bytes));
81
+ } catch {
82
+ throw new Error("invalid_json");
83
+ }
84
+ } finally {
85
+ signal.removeEventListener("abort", abort);
86
+ await reader.cancel().catch(() => {
87
+ });
88
+ reader.releaseLock();
89
+ }
90
+ }
91
+ async function probe(url, fetcher, json) {
92
+ const controller = new AbortController();
93
+ let timer;
94
+ const timeout = new Promise((resolve) => {
95
+ timer = setTimeout(() => {
96
+ controller.abort();
97
+ resolve({ status: 0, error: "request_timeout" });
98
+ }, TIMEOUT_MS);
99
+ });
100
+ try {
101
+ return await Promise.race([timeout, (async () => {
102
+ let response;
103
+ try {
104
+ response = await fetcher(url, {
105
+ headers: { "cache-control": "no-cache" },
106
+ redirect: "manual",
107
+ signal: controller.signal
108
+ });
109
+ } catch {
110
+ return { status: 0, error: "request_failed" };
111
+ }
112
+ if (!json || !response.ok) {
113
+ await response.body?.cancel().catch(() => {
114
+ });
115
+ return { status: response.status };
116
+ }
117
+ try {
118
+ return {
119
+ status: response.status,
120
+ json: await readBoundedJson(response, controller.signal),
121
+ identity: Object.fromEntries(["app-id", "chapter-id", "data-environment", "runtime"].map((name) => [name, slug(response.headers.get(`x-odla-${name}`) ?? void 0)]))
122
+ };
123
+ } catch (error) {
124
+ const code = error instanceof Error && ["invalid_json", "response_too_large"].includes(error.message) ? error.message : "response_failed";
125
+ return { status: response.status, error: code };
126
+ }
127
+ })()]);
128
+ } finally {
129
+ clearTimeout(timer);
130
+ }
131
+ }
132
+ function slug(value) {
133
+ return value === void 0 ? void 0 : typeof value === "string" && /^[a-z0-9][a-z0-9-]{1,62}$/.test(value) ? value : "invalid_value";
134
+ }
135
+ function amount(value) {
136
+ return value === void 0 ? void 0 : Number.isSafeInteger(value) && Number(value) >= 0 ? value : "invalid_value";
137
+ }
138
+ function boolean(value) {
139
+ return value === void 0 ? void 0 : typeof value === "boolean" ? value : "invalid_value";
140
+ }
2
141
  function activeMemberships(site) {
3
142
  return site.skus.filter((sku) => sku.active && sku.kind === "membership");
4
143
  }
@@ -7,31 +146,35 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
7
146
  if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
8
147
  const findings = [];
9
148
  for (const site of manifest.targets) {
10
- let health;
11
- let configResponse;
12
- try {
13
- [health, configResponse] = await Promise.all([
14
- fetcher(`${site.origin}/api/health`, { headers: { "cache-control": "no-cache" } }),
15
- fetcher(`${site.origin}/api/join-config`, { headers: { "cache-control": "no-cache" } })
16
- ]);
17
- } catch (error) {
18
- findings.push({ targetId: site.targetId, siteId: site.siteId, path: "fetch", expected: "reachable", actual: error instanceof Error ? error.message : String(error) });
19
- continue;
20
- }
149
+ const [health, configResponse] = await Promise.all([
150
+ probe(`${site.origin}/api/health`, fetcher, true),
151
+ probe(`${site.origin}/api/join-config`, fetcher, true)
152
+ ]);
21
153
  const targetNote = (path, expected, actual) => {
22
154
  if (JSON.stringify(expected) !== JSON.stringify(actual)) findings.push({ targetId: site.targetId, siteId: site.siteId, path, expected, actual });
23
155
  };
24
- targetNote("/api/health status", 200, health.status);
25
- targetNote("/api/join-config status", 200, configResponse.status);
26
- if (!configResponse.ok) continue;
27
- let config;
28
- try {
29
- config = await configResponse.json();
30
- } catch {
31
- targetNote("/api/join-config JSON", "object", "invalid JSON");
156
+ targetNote("/api/health status", 200, health.error ?? health.status);
157
+ if (health.status === 200 && !health.error) targetNote("/api/health ok", true, boolean(record(health.json)?.ok));
158
+ if (health.status === 200 && !health.error) {
159
+ for (const [name, expected] of Object.entries({
160
+ "app-id": site.target.appId,
161
+ "chapter-id": site.target.chapterId,
162
+ "data-environment": site.target.dataEnvironment,
163
+ "runtime": site.target.runtime
164
+ })) targetNote(`/api/health x-odla-${name}`, expected, health.identity?.[name]);
165
+ }
166
+ targetNote("/api/join-config status", 200, configResponse.status || configResponse.error);
167
+ if (configResponse.status !== 200) continue;
168
+ if (configResponse.error) {
169
+ targetNote("/api/join-config JSON", "object", configResponse.error === "invalid_json" ? "invalid JSON" : configResponse.error);
170
+ continue;
171
+ }
172
+ const config = record(configResponse.json);
173
+ if (!config) {
174
+ targetNote("/api/join-config JSON", "object", "invalid shape");
32
175
  continue;
33
176
  }
34
- targetNote("id", site.target.chapterId, config.id);
177
+ targetNote("id", site.target.chapterId, slug(config.id));
35
178
  const actualTiers = Array.isArray(config.tiers) ? config.tiers : [];
36
179
  const expectedTiers = activeMemberships(site).map((sku) => ({
37
180
  id: sku.id,
@@ -41,17 +184,18 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
41
184
  free: sku.priceCents === 0
42
185
  }));
43
186
  const projected = actualTiers.map((tier) => {
44
- const value = tier;
187
+ const value = record(tier);
188
+ if (!value) return "invalid_tier";
45
189
  return {
46
- id: value.id,
47
- priceCents: value.priceCents,
48
- currency: value.currency,
49
- interval: value.interval,
50
- free: value.free
190
+ id: slug(value.id),
191
+ priceCents: amount(value.priceCents),
192
+ currency: value.currency === void 0 ? void 0 : typeof value.currency === "string" && /^[a-z]{3}$/.test(value.currency) ? value.currency : "invalid_value",
193
+ interval: value.interval === void 0 ? void 0 : ["month", "year", "once"].includes(String(value.interval)) ? value.interval : "invalid_value",
194
+ free: boolean(value.free)
51
195
  };
52
196
  });
53
197
  targetNote("tiers", expectedTiers, projected);
54
- targetNote("paymentsReady", expectedTiers.some((tier) => !tier.free), config.paymentsReady);
198
+ targetNote("paymentsReady", expectedTiers.some((tier) => !tier.free), boolean(config.paymentsReady));
55
199
  }
56
200
  return { ok: findings.length === 0, findings };
57
201
  }
@@ -59,16 +203,21 @@ async function probeChapterJoinContracts(manifest, fetcher = fetch) {
59
203
  // src/testing-runner.ts
60
204
  var SAFE_LABEL = /^[a-z0-9][a-z0-9._:-]{0,159}$/;
61
205
  var SAFE_REFERENCE = /^[A-Za-z0-9_:.=-]{3,240}$/;
206
+ var CREDENTIAL = /(?:^|[_.:-])(?:sk|rk|pk)_(?:test|live)_|whsec_|_secret_|eyJ[A-Za-z0-9_-]+\./i;
207
+ var ERROR_CODES = /* @__PURE__ */ new Set(["invalid_evidence", "assertion_failed", "preflight_failed", "unsupported_scenario", "execution_failed"]);
208
+ function safeValue(value, pattern) {
209
+ return pattern.test(value) && !CREDENTIAL.test(value);
210
+ }
62
211
  function safeErrorCode(error) {
63
212
  if (error && typeof error === "object") {
64
213
  const code = error.code;
65
- if (typeof code === "string" && SAFE_LABEL.test(code)) return code;
214
+ if (typeof code === "string" && ERROR_CODES.has(code)) return code;
66
215
  }
67
216
  return "execution_failed";
68
217
  }
69
218
  function validateLabels(values, path) {
70
219
  if (!Array.isArray(values) || values.length === 0) throw Object.assign(new TypeError(`${path} must be non-empty`), { code: "invalid_evidence" });
71
- if (values.length > 64 || values.some((value) => typeof value !== "string" || !SAFE_LABEL.test(value))) {
220
+ if (values.length > 64 || values.some((value) => typeof value !== "string" || !safeValue(value, SAFE_LABEL))) {
72
221
  throw Object.assign(new TypeError(`${path} contains an unsafe or unbounded label`), { code: "invalid_evidence" });
73
222
  }
74
223
  }
@@ -77,7 +226,7 @@ function validateReferences(values) {
77
226
  if (!Array.isArray(values) || values.length > 64) throw Object.assign(new TypeError("references must be bounded"), { code: "invalid_evidence" });
78
227
  const kinds = /* @__PURE__ */ new Set(["stripe_event", "stripe_object", "clerk_user", "calendar_event", "email_message", "event_digest"]);
79
228
  for (const reference of values) {
80
- if (!reference || !kinds.has(reference.kind) || typeof reference.value !== "string" || !SAFE_REFERENCE.test(reference.value) || reference.value.includes("@")) {
229
+ if (!reference || !kinds.has(reference.kind) || typeof reference.value !== "string" || !safeValue(reference.value, SAFE_REFERENCE) || reference.value.includes("@")) {
81
230
  throw Object.assign(new TypeError("references may contain only opaque provider ids or digests"), { code: "invalid_evidence" });
82
231
  }
83
232
  }
@@ -86,52 +235,70 @@ function validateEvidence(evidence, provider) {
86
235
  if (!evidence || typeof evidence !== "object") throw Object.assign(new TypeError("executor returned no evidence"), { code: "invalid_evidence" });
87
236
  validateLabels(evidence.checks, "evidence.checks");
88
237
  validateReferences(evidence.references);
89
- if (provider && (typeof evidence.deploymentVersion !== "string" || !SAFE_REFERENCE.test(evidence.deploymentVersion))) {
238
+ if ((provider || evidence.deploymentVersion !== void 0) && (typeof evidence.deploymentVersion !== "string" || !safeValue(evidence.deploymentVersion, SAFE_REFERENCE))) {
90
239
  throw Object.assign(new TypeError("provider evidence requires a safe deploymentVersion"), { code: "invalid_evidence" });
91
240
  }
241
+ return {
242
+ checks: [...evidence.checks],
243
+ ...evidence.deploymentVersion !== void 0 ? { deploymentVersion: evidence.deploymentVersion } : {},
244
+ ...evidence.references !== void 0 ? {
245
+ references: evidence.references.map(({ kind, value }) => ({ kind, value }))
246
+ } : {}
247
+ };
92
248
  }
93
249
  function validateCleanup(cleanup) {
94
250
  if (!cleanup || cleanup.status !== "complete" && cleanup.status !== "incomplete") {
95
251
  throw Object.assign(new TypeError("executor returned invalid cleanup evidence"), { code: "cleanup_failed" });
96
252
  }
97
253
  validateLabels(cleanup.checks, "cleanup.checks");
254
+ return { status: cleanup.status, checks: [...cleanup.checks] };
98
255
  }
99
- function contextFor(manifest, id) {
100
- const scenario2 = planChapterAcceptanceMatrix(manifest).find((item) => item.id === id);
101
- if (!scenario2) throw new TypeError(`unknown acceptance scenario ${id}`);
256
+ function contextFor(manifest, id2) {
257
+ const scenario2 = planChapterAcceptanceMatrix(manifest).find((item) => item.id === id2);
258
+ if (!scenario2) throw new TypeError(`unknown acceptance scenario ${id2}`);
102
259
  const target = manifest.targets.find((item) => item.targetId === scenario2.targetId);
103
260
  return {
104
261
  scenario: scenario2,
105
262
  target,
106
263
  ...scenario2.skuId ? { sku: target.skus.find((item) => item.id === scenario2.skuId) } : {},
264
+ ...scenario2.parentSkuId ? { parentSku: target.skus.find((item) => item.id === scenario2.parentSkuId) } : {},
265
+ ...scenario2.commercialCaseId ? { commercialCase: target.commercialCases?.find((item) => item.id === scenario2.commercialCaseId) } : {},
107
266
  ...scenario2.siblingTargetId ? {
108
267
  siblingTarget: manifest.targets.find((item) => item.targetId === scenario2.siblingTargetId)
109
268
  } : {}
110
269
  };
111
270
  }
112
271
  async function runChapterAcceptanceSuite(manifest, suite, executor) {
272
+ if (suite !== "hermetic" && suite !== "provider_dev") throw new TypeError("unknown acceptance suite");
113
273
  const scenarios = planChapterAcceptanceMatrix(manifest).filter((item) => item.suite === suite);
114
274
  if (suite === "provider_dev" && typeof executor.cleanup !== "function") {
115
275
  throw new TypeError("provider_dev acceptance requires a cleanup executor before any case can run");
116
276
  }
117
277
  const cases = [];
278
+ let cleanupFailed = false;
118
279
  for (const scenario2 of scenarios) {
280
+ if (cleanupFailed) {
281
+ cases.push({ id: scenario2.id, suite, outcome: "failed", errorCode: "not_run_cleanup_failed" });
282
+ continue;
283
+ }
119
284
  const context = contextFor(manifest, scenario2.id);
120
285
  let evidence;
121
286
  let errorCode;
122
287
  let cleanup;
123
288
  try {
124
- evidence = await executor.execute(context);
125
- validateEvidence(evidence, suite === "provider_dev");
289
+ evidence = validateEvidence(await executor.execute(context), suite === "provider_dev");
126
290
  } catch (error) {
127
291
  errorCode = safeErrorCode(error);
128
292
  } finally {
129
293
  if (suite === "provider_dev") {
130
294
  try {
131
- cleanup = await executor.cleanup(context);
132
- validateCleanup(cleanup);
133
- if (cleanup.status !== "complete") errorCode = "cleanup_incomplete";
295
+ cleanup = validateCleanup(await executor.cleanup(context));
296
+ if (cleanup.status !== "complete") {
297
+ cleanupFailed = true;
298
+ errorCode = "cleanup_incomplete";
299
+ }
134
300
  } catch {
301
+ cleanupFailed = true;
135
302
  errorCode = "cleanup_failed";
136
303
  }
137
304
  }
@@ -156,6 +323,66 @@ function formatChapterAcceptanceRun(result) {
156
323
  return lines.join("\n");
157
324
  }
158
325
 
326
+ // src/testing-release.ts
327
+ var ID = /^[a-z0-9][a-z0-9_.:-]{1,199}$/;
328
+ var validId = (value) => typeof value === "string" && ID.test(value);
329
+ var opaque = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_:-]{2,159}$/.test(value) && !/(?:sk|rk|pk|whsec)_(?:test_|live_)?|_secret_|bearer|password|token/i.test(value);
330
+ var keys = ["targetId", "appId", "chapterId", "runtime", "environment", "dataEnvironment", "stripeMode", "sourceCommit", "deploymentVersion"];
331
+ function assessChapterReleaseEvidence(plan, proofs) {
332
+ const blockers = [];
333
+ const block = (requirementId, code) => blockers.push({ requirementId, code });
334
+ const targets = /* @__PURE__ */ new Map();
335
+ if (!plan || plan.schemaVersion !== 1 || !Array.isArray(plan.targets) || !plan.targets.length || plan.targets.length > 64 || !Array.isArray(plan.requirements) || !plan.requirements.length || plan.requirements.length > 4096 || !Array.isArray(proofs) || proofs.length > 8192) {
336
+ return {
337
+ readyForReview: false,
338
+ productionAuthorized: false,
339
+ required: 0,
340
+ passed: 0,
341
+ blockers: [{ requirementId: "plan", code: "invalid_release_plan" }]
342
+ };
343
+ }
344
+ for (const target of plan.targets) {
345
+ if (!target || !validId(target.targetId) || targets.has(target.targetId) || ![target.appId, target.chapterId, target.runtime].every(validId) || target.environment !== "dev" || target.dataEnvironment !== "dev" || target.stripeMode !== "test" || typeof target.sourceCommit !== "string" || !/^[0-9a-f]{40}$/.test(target.sourceCommit) || !opaque(target.deploymentVersion)) {
346
+ block("plan", "invalid_or_undeployed_target");
347
+ continue;
348
+ }
349
+ targets.set(target.targetId, target);
350
+ }
351
+ const requirementIds = /* @__PURE__ */ new Set();
352
+ let passed = 0;
353
+ for (const requirement of plan.requirements) {
354
+ if (!requirement || !validId(requirement.id) || requirementIds.has(requirement.id) || !["deterministic", "deployed_contract", "provider_dev", "browser_dev"].includes(requirement.phase) || !Array.isArray(requirement.assertions) || !requirement.assertions.length || !requirement.assertions.every(validId) || new Set(requirement.assertions).size !== requirement.assertions.length || !Array.isArray(requirement.providers) || requirement.providers.some((kind) => !["stripe", "clerk", "calendar", "email"].includes(kind)) || !Array.isArray(requirement.cleanup) || !requirement.cleanup.every(validId) || requirement.phase === "provider_dev" && (!requirement.providers.length || !requirement.cleanup.length)) {
355
+ block("plan", "invalid_requirement");
356
+ continue;
357
+ }
358
+ requirementIds.add(requirement.id);
359
+ const target = targets.get(requirement.targetId);
360
+ if (!target) {
361
+ block(requirement.id, "missing_candidate_target");
362
+ continue;
363
+ }
364
+ const matching = proofs.filter((proof2) => proof2?.requirementId === requirement.id);
365
+ if (matching.length !== 1) {
366
+ block(requirement.id, matching.length ? "duplicate_proof" : "missing_proof");
367
+ continue;
368
+ }
369
+ const proof = matching[0];
370
+ const before = blockers.length;
371
+ if (!proof.target || keys.some((key) => proof.target[key] !== target[key])) block(requirement.id, "candidate_mismatch");
372
+ if (proof.phase !== requirement.phase) block(requirement.id, "wrong_execution_phase");
373
+ if (proof.outcome !== "passed") block(requirement.id, "attempt_not_passed");
374
+ if (!proof.assertions || requirement.assertions.some((id2) => proof.assertions[id2] !== true)) block(requirement.id, "assertions_incomplete");
375
+ if (!Array.isArray(proof.references) || proof.references.some((reference) => !reference || !opaque(reference.id) || !["stripe", "clerk", "calendar", "email"].includes(reference.provider)) || requirement.providers.some((provider) => !proof.references.some((reference) => reference.provider === provider))) block(requirement.id, "provider_evidence_incomplete");
376
+ if (!proof.cleanup || requirement.cleanup.some((id2) => proof.cleanup[id2] !== true)) block(requirement.id, "cleanup_incomplete");
377
+ if (blockers.length === before) passed++;
378
+ }
379
+ if (proofs.some((proof) => !proof || !requirementIds.has(proof.requirementId))) block("plan", "unexpected_proof");
380
+ for (const target of targets.values()) {
381
+ if (!plan.requirements.some((requirement) => requirement?.targetId === target.targetId)) block("plan", "uncovered_target");
382
+ }
383
+ return { readyForReview: blockers.length === 0, productionAuthorized: false, required: plan.requirements.length, passed, blockers };
384
+ }
385
+
159
386
  // src/testing.ts
160
387
  var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
161
388
  var CURRENCY = /^[a-z]{3}$/;
@@ -163,15 +390,12 @@ var STRIPE_PRICE = /^price_[A-Za-z0-9]{8,}$/;
163
390
  function activeMemberships2(site) {
164
391
  return site.skus.filter((sku) => sku.active && sku.kind === "membership");
165
392
  }
166
- function representativePaidMembership(site) {
167
- return activeMemberships2(site).find((sku) => sku.priceCents > 0);
168
- }
169
- function record(value) {
393
+ function record2(value) {
170
394
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
171
395
  }
172
396
  function validateChapterAcceptanceManifest(value) {
173
397
  const errors = [];
174
- const input = record(value);
398
+ const input = record2(value);
175
399
  if (!input) return { ok: false, errors: ["manifest must be an object"] };
176
400
  if (input.schemaVersion !== 1) errors.push("schemaVersion must be 1");
177
401
  const inputTargets = Array.isArray(input.targets) ? input.targets : [];
@@ -182,7 +406,7 @@ function validateChapterAcceptanceManifest(value) {
182
406
  const priceOwners = /* @__PURE__ */ new Map();
183
407
  for (const [siteIndex, rawSite] of inputTargets.entries()) {
184
408
  const path = `targets[${siteIndex}]`;
185
- const site = record(rawSite);
409
+ const site = record2(rawSite);
186
410
  if (!site) {
187
411
  errors.push(`${path} must be an object`);
188
412
  continue;
@@ -197,13 +421,13 @@ function validateChapterAcceptanceManifest(value) {
197
421
  try {
198
422
  if (typeof site.origin !== "string") throw new TypeError("origin");
199
423
  const origin = new URL(site.origin);
200
- if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash) {
424
+ if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash || origin.username || origin.password) {
201
425
  errors.push(`${path}.origin must be an HTTPS origin without a path, query, or fragment`);
202
426
  }
203
427
  } catch {
204
428
  errors.push(`${path}.origin must be a valid HTTPS origin`);
205
429
  }
206
- const target = record(site.target);
430
+ const target = record2(site.target);
207
431
  const targetValues = {};
208
432
  for (const key of ["appId", "environment", "dataEnvironment", "runtime", "chapterId"]) {
209
433
  targetValues[key] = typeof target?.[key] === "string" ? String(target[key]) : "";
@@ -224,15 +448,15 @@ function validateChapterAcceptanceManifest(value) {
224
448
  const skus = [];
225
449
  for (const [skuIndex, rawSku] of inputSkus.entries()) {
226
450
  const skuPath = `${path}.skus[${skuIndex}]`;
227
- const sku = record(rawSku);
451
+ const sku = record2(rawSku);
228
452
  if (!sku) {
229
453
  errors.push(`${skuPath} must be an object`);
230
454
  continue;
231
455
  }
232
- const id = typeof sku.id === "string" ? sku.id : "";
233
- if (!SLUG.test(id)) errors.push(`${skuPath}.id must be a stable lowercase slug`);
234
- if (skuIds.has(id)) errors.push(`${skuPath}.id duplicates ${JSON.stringify(id)}`);
235
- if (id) skuIds.add(id);
456
+ const id2 = typeof sku.id === "string" ? sku.id : "";
457
+ if (!SLUG.test(id2)) errors.push(`${skuPath}.id must be a stable lowercase slug`);
458
+ if (skuIds.has(id2)) errors.push(`${skuPath}.id duplicates ${JSON.stringify(id2)}`);
459
+ if (id2) skuIds.add(id2);
236
460
  if (sku.kind !== "membership" && sku.kind !== "add_on") errors.push(`${skuPath}.kind must be membership or add_on`);
237
461
  if (typeof sku.active !== "boolean") errors.push(`${skuPath}.active must be boolean`);
238
462
  const priceCents = typeof sku.priceCents === "number" ? sku.priceCents : Number.NaN;
@@ -249,7 +473,7 @@ function validateChapterAcceptanceManifest(value) {
249
473
  }
250
474
  if (priceCents === 0 && sku.stripePriceId) errors.push(`${skuPath}.stripePriceId must be absent for a free SKU`);
251
475
  if (typeof sku.stripePriceId === "string" && sku.stripePriceId) {
252
- const owner = `${siteId}/${id}`;
476
+ const owner = `${siteId}/${id2}`;
253
477
  const previous = priceOwners.get(sku.stripePriceId);
254
478
  if (previous && previous !== owner) errors.push(`${skuPath}.stripePriceId is already owned by ${previous}`);
255
479
  else priceOwners.set(sku.stripePriceId, owner);
@@ -267,12 +491,13 @@ function validateChapterAcceptanceManifest(value) {
267
491
  skus.push(sku);
268
492
  }
269
493
  if (!skus.some((sku) => sku.active && sku.kind === "membership")) errors.push(`${path}.skus must contain an active membership SKU`);
270
- const options = record(site.options);
494
+ errors.push(...validateCommercialCases(site.commercialCases, skus, `${path}.commercialCases`));
495
+ const options = record2(site.options);
271
496
  if (!options) errors.push(`${path}.options must be an object`);
272
497
  if (options?.ordinaryInterview !== true) errors.push(`${path}.options.ordinaryInterview must be true`);
273
498
  if (typeof options?.invitationInterviewWaiver !== "boolean") errors.push(`${path}.options.invitationInterviewWaiver must be boolean`);
274
499
  if (options?.autoRenew !== true) errors.push(`${path}.options.autoRenew must be true`);
275
- const seat = record(options?.additionalSeat);
500
+ const seat = record2(options?.additionalSeat);
276
501
  if (options?.additionalSeat !== void 0 && !seat) errors.push(`${path}.options.additionalSeat must be an object`);
277
502
  if (seat) {
278
503
  const seatSku = skus.find((sku) => sku.id === seat.skuId);
@@ -295,17 +520,23 @@ function validateChapterAcceptanceManifest(value) {
295
520
  function scenario(input) {
296
521
  return {
297
522
  ...input,
298
- id: [input.suite, input.targetId, input.skuId, input.siblingTargetId, input.kind].filter(Boolean).join(":")
523
+ id: [input.suite, input.targetId, input.skuId, input.parentSkuId, input.commercialCaseId, input.siblingTargetId, input.kind].filter(Boolean).join(":")
299
524
  };
300
525
  }
301
526
  function planChapterAcceptanceMatrix(manifest) {
302
527
  const validation = validateChapterAcceptanceManifest(manifest);
303
528
  if (!validation.ok) throw new TypeError(`invalid Chapter acceptance manifest: ${validation.errors.join("; ")}`);
304
- if (manifest.targets.some((site) => site.stripeMode !== "test")) {
305
- throw new TypeError("provider acceptance runs are development-only and require Stripe test mode on every target");
529
+ if (manifest.targets.some((site) => site.stripeMode !== "test" || site.target.environment !== "dev" || site.target.dataEnvironment !== "dev")) {
530
+ throw new TypeError("provider acceptance runs are development-only and require Stripe test mode and dev runtime/data environments on every target");
306
531
  }
307
532
  const scenarios = [];
308
533
  for (const site of manifest.targets) {
534
+ for (const vector of site.commercialCases ?? []) {
535
+ const base = { targetId: site.targetId, siteId: site.siteId, skuId: vector.skuId, commercialCaseId: vector.id };
536
+ scenarios.push(scenario({ ...base, suite: "hermetic", kind: "commercial_policy_replay" }));
537
+ scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "commercial_quote" }));
538
+ scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "commercial_renewal" }));
539
+ }
309
540
  for (const sku of site.skus.filter((candidate) => candidate.active)) {
310
541
  scenarios.push(scenario({ suite: "hermetic", kind: "catalog_contract", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
311
542
  if (sku.priceCents > 0) {
@@ -334,45 +565,34 @@ function planChapterAcceptanceMatrix(manifest) {
334
565
  skuId: sku.id
335
566
  }));
336
567
  }
337
- const paid = representativePaidMembership(site);
338
- if (paid) {
339
- scenarios.push(scenario({ suite: "provider_dev", kind: "ordinary_interview", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
340
- scenarios.push(scenario({ suite: "provider_dev", kind: "renewal", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
341
- scenarios.push(scenario({ suite: "provider_dev", kind: "refund_cancel", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
568
+ for (const membership of activeMemberships2(site)) {
569
+ const base = { targetId: site.targetId, siteId: site.siteId, skuId: membership.id };
570
+ scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "ordinary_interview" }));
342
571
  if (site.options.invitationInterviewWaiver) {
343
- scenarios.push(scenario({ suite: "provider_dev", kind: "invitation_interview_waiver", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
344
- scenarios.push(scenario({ suite: "hermetic", kind: "expired_interview_waiver", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
345
- scenarios.push(scenario({ suite: "hermetic", kind: "replayed_interview_waiver", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
572
+ scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "invitation_interview_waiver" }));
573
+ scenarios.push(scenario({ ...base, suite: "hermetic", kind: "expired_interview_waiver" }));
574
+ scenarios.push(scenario({ ...base, suite: "hermetic", kind: "replayed_interview_waiver" }));
346
575
  }
576
+ if (membership.priceCents === 0) continue;
577
+ scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "renewal" }));
578
+ scenarios.push(scenario({ ...base, suite: "provider_dev", kind: "refund_cancel" }));
347
579
  if (site.options.additionalSeat) {
348
- scenarios.push(scenario({
349
- suite: "provider_dev",
350
- kind: "named_seat",
351
- targetId: site.targetId,
352
- siteId: site.siteId,
353
- skuId: site.options.additionalSeat.skuId
354
- }));
355
- scenarios.push(scenario({
356
- suite: "hermetic",
357
- kind: "duplicate_seat_email",
358
- targetId: site.targetId,
359
- siteId: site.siteId,
360
- skuId: site.options.additionalSeat.skuId
361
- }));
362
- scenarios.push(scenario({
363
- suite: "hermetic",
364
- kind: "expired_seat_window",
365
- targetId: site.targetId,
366
- siteId: site.siteId,
367
- skuId: site.options.additionalSeat.skuId
368
- }));
580
+ const seat = { ...base, skuId: site.options.additionalSeat.skuId, parentSkuId: membership.id };
581
+ for (const kind of ["named_seat", "seat_declined_refund", "unaccepted_seat_refund", "seat_membership_alignment"]) {
582
+ scenarios.push(scenario({ ...seat, suite: "provider_dev", kind }));
583
+ }
584
+ for (const kind of ["duplicate_seat_email", "expired_seat_window"]) {
585
+ scenarios.push(scenario({ ...seat, suite: "hermetic", kind }));
586
+ }
369
587
  }
370
- scenarios.push(scenario({ suite: "hermetic", kind: "canceled_membership", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
588
+ scenarios.push(scenario({ ...base, suite: "hermetic", kind: "canceled_membership" }));
371
589
  }
372
590
  }
373
591
  return scenarios;
374
592
  }
375
593
  export {
594
+ assertCommercialQuote,
595
+ assessChapterReleaseEvidence,
376
596
  formatChapterAcceptanceRun,
377
597
  planChapterAcceptanceMatrix,
378
598
  probeChapterJoinContracts,