@odla-ai/chapter 0.47.10 → 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/README.md +231 -19
- package/dist/{chunk-VWDEHUNI.js → chunk-KZ2AXEZW.js} +497 -204
- package/dist/chunk-KZ2AXEZW.js.map +1 -0
- package/dist/{chunk-7MEVZEL2.js → chunk-MQ4OPZCS.js} +969 -516
- package/dist/chunk-MQ4OPZCS.js.map +1 -0
- package/dist/{copy-context-Dorxp-WK.d.ts → copy-context-BveBZglM.d.ts} +18 -7
- package/dist/index.cjs +3789 -761
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +753 -70
- package/dist/index.d.ts +753 -70
- package/dist/index.js +3782 -754
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +144 -42
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +92 -2
- package/dist/testing.d.ts +92 -2
- package/dist/testing.js +144 -42
- package/dist/testing.js.map +1 -1
- package/dist/ui/admin/index.d.ts +75 -5
- package/dist/ui/admin/index.js +3 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +4 -2
- package/dist/ui/member/index.d.ts +9 -2
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +9695 -4707
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +509 -9
- package/dist/worker/index.d.ts +509 -9
- package/dist/worker/index.js +8377 -3390
- package/dist/worker/index.js.map +1 -1
- package/package.json +5 -5
- package/runbooks/adopt-existing.md +44 -8
- package/dist/chunk-7MEVZEL2.js.map +0 -1
- package/dist/chunk-VWDEHUNI.js.map +0 -1
package/dist/testing.d.cts
CHANGED
|
@@ -14,6 +14,27 @@ interface ChapterAcceptanceSku {
|
|
|
14
14
|
/** How long the difference from compareAtPriceCents is retained. */
|
|
15
15
|
discountDuration?: "first_term" | "while_active";
|
|
16
16
|
}
|
|
17
|
+
/** Explicit expected terms at a test-clock instant, independent of the SKU price.
|
|
18
|
+
* These are acceptance assertions, never permission to issue an offer or change Stripe.
|
|
19
|
+
*/
|
|
20
|
+
interface ChapterAcceptanceCommercialCase {
|
|
21
|
+
id: string;
|
|
22
|
+
skuId: string;
|
|
23
|
+
purchasedAt: number;
|
|
24
|
+
offeringVersionId: string;
|
|
25
|
+
founding: boolean;
|
|
26
|
+
/** Cohort policy may apply to a Steward without granting a price discount. */
|
|
27
|
+
cohortPolicyVersionId?: string;
|
|
28
|
+
dueTodayCents: number;
|
|
29
|
+
initialTermEndsAt: number;
|
|
30
|
+
renewalCents: number;
|
|
31
|
+
discount?: {
|
|
32
|
+
policyVersionId: string;
|
|
33
|
+
percentOffBps: number;
|
|
34
|
+
duration: "first_term" | "while_active" | "fixed_end";
|
|
35
|
+
endsAt?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
17
38
|
/** Named-seat behavior that must be exercised as a membership add-on. */
|
|
18
39
|
interface ChapterAcceptanceSeatOption {
|
|
19
40
|
skuId: string;
|
|
@@ -45,6 +66,7 @@ interface ChapterAcceptanceTarget {
|
|
|
45
66
|
stripeMode: "test" | "live";
|
|
46
67
|
skus: readonly ChapterAcceptanceSku[];
|
|
47
68
|
options: ChapterAcceptanceOptions;
|
|
69
|
+
commercialCases?: readonly ChapterAcceptanceCommercialCase[];
|
|
48
70
|
}
|
|
49
71
|
/** Complete multi-site contract consumed by hermetic and deployed-dev tests. */
|
|
50
72
|
interface ChapterAcceptanceManifest {
|
|
@@ -60,7 +82,7 @@ type ChapterAcceptanceValidation = {
|
|
|
60
82
|
errors: readonly string[];
|
|
61
83
|
};
|
|
62
84
|
/** Closed set of deterministic and provider-backed acceptance behaviors. */
|
|
63
|
-
type ChapterAcceptanceScenarioKind = "catalog_contract" | "amount_tamper" | "wrong_environment_event" | "sibling_event_isolation" | "submit_replay" | "post_stripe_compensation" | "paid_signup" | "free_signup" | "ordinary_interview" | "invitation_interview_waiver" | "expired_interview_waiver" | "replayed_interview_waiver" | "named_seat" | "duplicate_seat_email" | "expired_seat_window" | "canceled_membership" | "renewal" | "refund_cancel";
|
|
85
|
+
type ChapterAcceptanceScenarioKind = "catalog_contract" | "commercial_quote" | "commercial_renewal" | "commercial_policy_replay" | "amount_tamper" | "wrong_environment_event" | "sibling_event_isolation" | "submit_replay" | "post_stripe_compensation" | "paid_signup" | "free_signup" | "ordinary_interview" | "invitation_interview_waiver" | "expired_interview_waiver" | "replayed_interview_waiver" | "named_seat" | "duplicate_seat_email" | "expired_seat_window" | "seat_declined_refund" | "unaccepted_seat_refund" | "seat_membership_alignment" | "canceled_membership" | "renewal" | "refund_cancel";
|
|
64
86
|
/** One stable generated case. Provider cases are opt-in and test-mode only. */
|
|
65
87
|
interface ChapterAcceptanceScenario {
|
|
66
88
|
id: string;
|
|
@@ -69,6 +91,9 @@ interface ChapterAcceptanceScenario {
|
|
|
69
91
|
targetId: string;
|
|
70
92
|
siteId: string;
|
|
71
93
|
skuId?: string;
|
|
94
|
+
/** Membership whose add-on path is exercised; never assume the first paid SKU. */
|
|
95
|
+
parentSkuId?: string;
|
|
96
|
+
commercialCaseId?: string;
|
|
72
97
|
siblingTargetId?: string;
|
|
73
98
|
}
|
|
74
99
|
/** Public response shape needed by the non-mutating deployed contract probe. */
|
|
@@ -95,6 +120,8 @@ interface ChapterAcceptanceExecutionContext {
|
|
|
95
120
|
scenario: ChapterAcceptanceScenario;
|
|
96
121
|
target: ChapterAcceptanceTarget;
|
|
97
122
|
sku?: ChapterAcceptanceSku;
|
|
123
|
+
parentSku?: ChapterAcceptanceSku;
|
|
124
|
+
commercialCase?: ChapterAcceptanceCommercialCase;
|
|
98
125
|
siblingTarget?: ChapterAcceptanceTarget;
|
|
99
126
|
}
|
|
100
127
|
/** Provider references permitted in durable evidence. Values must be opaque ids or digests, never PII. */
|
|
@@ -137,6 +164,9 @@ interface ChapterAcceptanceRunResult {
|
|
|
137
164
|
cases: ChapterAcceptanceCaseResult[];
|
|
138
165
|
}
|
|
139
166
|
|
|
167
|
+
/** Compare real observed checkout terms against a separately authored acceptance vector. */
|
|
168
|
+
declare function assertCommercialQuote(expected: ChapterAcceptanceCommercialCase, sku: ChapterAcceptanceSku, actual: unknown): boolean;
|
|
169
|
+
|
|
140
170
|
/**
|
|
141
171
|
* Probe public deployed join contracts without creating applications or provider objects.
|
|
142
172
|
* Each request, including its body, is limited to ten seconds and 64 KiB. Redirects
|
|
@@ -157,6 +187,66 @@ declare function runChapterAcceptanceSuite(manifest: ChapterAcceptanceManifest,
|
|
|
157
187
|
/** PII-free line report suitable for CI logs and PM evidence. */
|
|
158
188
|
declare function formatChapterAcceptanceRun(result: ChapterAcceptanceRunResult): string;
|
|
159
189
|
|
|
190
|
+
/** A release proof is tied to a reviewed source commit and one exact dev deployment. */
|
|
191
|
+
interface ChapterReleaseTarget {
|
|
192
|
+
targetId: string;
|
|
193
|
+
appId: string;
|
|
194
|
+
chapterId: string;
|
|
195
|
+
runtime: string;
|
|
196
|
+
environment: "dev";
|
|
197
|
+
dataEnvironment: "dev";
|
|
198
|
+
stripeMode: "test";
|
|
199
|
+
sourceCommit: string;
|
|
200
|
+
deploymentVersion: string;
|
|
201
|
+
}
|
|
202
|
+
/** Required behavior is defined independently of currently enabled feature flags. */
|
|
203
|
+
interface ChapterReleaseRequirement {
|
|
204
|
+
id: string;
|
|
205
|
+
targetId: string;
|
|
206
|
+
phase: "deterministic" | "deployed_contract" | "provider_dev" | "browser_dev";
|
|
207
|
+
assertions: readonly string[];
|
|
208
|
+
/** Each provider kind needs at least one opaque id/digest from the real attempt. */
|
|
209
|
+
providers: readonly ("stripe" | "clerk" | "calendar" | "email")[];
|
|
210
|
+
cleanup: readonly string[];
|
|
211
|
+
}
|
|
212
|
+
/** An independent release contract, not a list inferred from whichever tests happened to run. */
|
|
213
|
+
interface ChapterReleasePlan {
|
|
214
|
+
schemaVersion: 1;
|
|
215
|
+
targets: readonly ChapterReleaseTarget[];
|
|
216
|
+
requirements: readonly ChapterReleaseRequirement[];
|
|
217
|
+
}
|
|
218
|
+
/** Structured result from an actual driver; this completeness check does not execute or authenticate a driver. */
|
|
219
|
+
interface ChapterReleaseProof {
|
|
220
|
+
requirementId: string;
|
|
221
|
+
target: ChapterReleaseTarget;
|
|
222
|
+
phase: ChapterReleaseRequirement["phase"];
|
|
223
|
+
outcome: "passed" | "failed" | "blocked";
|
|
224
|
+
assertions: Readonly<Record<string, boolean>>;
|
|
225
|
+
references: readonly {
|
|
226
|
+
provider: ChapterReleaseRequirement["providers"][number];
|
|
227
|
+
id: string;
|
|
228
|
+
}[];
|
|
229
|
+
cleanup: Readonly<Record<string, boolean>>;
|
|
230
|
+
}
|
|
231
|
+
/** Bounded release blockers. Passing requests a human review, never permission to deploy. */
|
|
232
|
+
interface ChapterReleaseAssessment {
|
|
233
|
+
readyForReview: boolean;
|
|
234
|
+
productionAuthorized: false;
|
|
235
|
+
required: number;
|
|
236
|
+
passed: number;
|
|
237
|
+
blockers: readonly {
|
|
238
|
+
requirementId: string;
|
|
239
|
+
code: string;
|
|
240
|
+
}[];
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Require a complete, exact-candidate release evidence set. Missing, duplicate,
|
|
244
|
+
* failed, stale, wrong-runtime, skipped, or incompletely cleaned attempts block
|
|
245
|
+
* review. This is evidence validation, not provider execution: callers must use
|
|
246
|
+
* real drivers and retain their independently inspectable run artifacts.
|
|
247
|
+
*/
|
|
248
|
+
declare function assessChapterReleaseEvidence(plan: ChapterReleasePlan, proofs: readonly ChapterReleaseProof[]): ChapterReleaseAssessment;
|
|
249
|
+
|
|
160
250
|
/** Validate the exact facts a multi-site acceptance run needs before it writes anywhere. */
|
|
161
251
|
declare function validateChapterAcceptanceManifest(value: unknown): ChapterAcceptanceValidation;
|
|
162
252
|
/**
|
|
@@ -165,4 +255,4 @@ declare function validateChapterAcceptanceManifest(value: unknown): ChapterAccep
|
|
|
165
255
|
*/
|
|
166
256
|
declare function planChapterAcceptanceMatrix(manifest: ChapterAcceptanceManifest): ChapterAcceptanceScenario[];
|
|
167
257
|
|
|
168
|
-
export { type ChapterAcceptanceCaseResult, type ChapterAcceptanceCleanupEvidence, type ChapterAcceptanceEvidence, type ChapterAcceptanceExecutionContext, type ChapterAcceptanceExecutor, type ChapterAcceptanceManifest, type ChapterAcceptanceOptions, type ChapterAcceptanceProviderReference, type ChapterAcceptanceRunResult, type ChapterAcceptanceScenario, type ChapterAcceptanceScenarioKind, type ChapterAcceptanceSeatOption, type ChapterAcceptanceSku, type ChapterAcceptanceTarget, type ChapterAcceptanceValidation, type ChapterJoinConfigProbe, type ChapterJoinContractFinding, type ChapterJoinContractProbeResult, formatChapterAcceptanceRun, planChapterAcceptanceMatrix, probeChapterJoinContracts, runChapterAcceptanceSuite, validateChapterAcceptanceManifest };
|
|
258
|
+
export { type ChapterAcceptanceCaseResult, type ChapterAcceptanceCleanupEvidence, type ChapterAcceptanceCommercialCase, type ChapterAcceptanceEvidence, type ChapterAcceptanceExecutionContext, type ChapterAcceptanceExecutor, type ChapterAcceptanceManifest, type ChapterAcceptanceOptions, type ChapterAcceptanceProviderReference, type ChapterAcceptanceRunResult, type ChapterAcceptanceScenario, type ChapterAcceptanceScenarioKind, type ChapterAcceptanceSeatOption, type ChapterAcceptanceSku, type ChapterAcceptanceTarget, type ChapterAcceptanceValidation, type ChapterJoinConfigProbe, type ChapterJoinContractFinding, type ChapterJoinContractProbeResult, type ChapterReleaseAssessment, type ChapterReleasePlan, type ChapterReleaseProof, type ChapterReleaseRequirement, type ChapterReleaseTarget, assertCommercialQuote, assessChapterReleaseEvidence, formatChapterAcceptanceRun, planChapterAcceptanceMatrix, probeChapterJoinContracts, runChapterAcceptanceSuite, validateChapterAcceptanceManifest };
|
package/dist/testing.d.ts
CHANGED
|
@@ -14,6 +14,27 @@ interface ChapterAcceptanceSku {
|
|
|
14
14
|
/** How long the difference from compareAtPriceCents is retained. */
|
|
15
15
|
discountDuration?: "first_term" | "while_active";
|
|
16
16
|
}
|
|
17
|
+
/** Explicit expected terms at a test-clock instant, independent of the SKU price.
|
|
18
|
+
* These are acceptance assertions, never permission to issue an offer or change Stripe.
|
|
19
|
+
*/
|
|
20
|
+
interface ChapterAcceptanceCommercialCase {
|
|
21
|
+
id: string;
|
|
22
|
+
skuId: string;
|
|
23
|
+
purchasedAt: number;
|
|
24
|
+
offeringVersionId: string;
|
|
25
|
+
founding: boolean;
|
|
26
|
+
/** Cohort policy may apply to a Steward without granting a price discount. */
|
|
27
|
+
cohortPolicyVersionId?: string;
|
|
28
|
+
dueTodayCents: number;
|
|
29
|
+
initialTermEndsAt: number;
|
|
30
|
+
renewalCents: number;
|
|
31
|
+
discount?: {
|
|
32
|
+
policyVersionId: string;
|
|
33
|
+
percentOffBps: number;
|
|
34
|
+
duration: "first_term" | "while_active" | "fixed_end";
|
|
35
|
+
endsAt?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
17
38
|
/** Named-seat behavior that must be exercised as a membership add-on. */
|
|
18
39
|
interface ChapterAcceptanceSeatOption {
|
|
19
40
|
skuId: string;
|
|
@@ -45,6 +66,7 @@ interface ChapterAcceptanceTarget {
|
|
|
45
66
|
stripeMode: "test" | "live";
|
|
46
67
|
skus: readonly ChapterAcceptanceSku[];
|
|
47
68
|
options: ChapterAcceptanceOptions;
|
|
69
|
+
commercialCases?: readonly ChapterAcceptanceCommercialCase[];
|
|
48
70
|
}
|
|
49
71
|
/** Complete multi-site contract consumed by hermetic and deployed-dev tests. */
|
|
50
72
|
interface ChapterAcceptanceManifest {
|
|
@@ -60,7 +82,7 @@ type ChapterAcceptanceValidation = {
|
|
|
60
82
|
errors: readonly string[];
|
|
61
83
|
};
|
|
62
84
|
/** Closed set of deterministic and provider-backed acceptance behaviors. */
|
|
63
|
-
type ChapterAcceptanceScenarioKind = "catalog_contract" | "amount_tamper" | "wrong_environment_event" | "sibling_event_isolation" | "submit_replay" | "post_stripe_compensation" | "paid_signup" | "free_signup" | "ordinary_interview" | "invitation_interview_waiver" | "expired_interview_waiver" | "replayed_interview_waiver" | "named_seat" | "duplicate_seat_email" | "expired_seat_window" | "canceled_membership" | "renewal" | "refund_cancel";
|
|
85
|
+
type ChapterAcceptanceScenarioKind = "catalog_contract" | "commercial_quote" | "commercial_renewal" | "commercial_policy_replay" | "amount_tamper" | "wrong_environment_event" | "sibling_event_isolation" | "submit_replay" | "post_stripe_compensation" | "paid_signup" | "free_signup" | "ordinary_interview" | "invitation_interview_waiver" | "expired_interview_waiver" | "replayed_interview_waiver" | "named_seat" | "duplicate_seat_email" | "expired_seat_window" | "seat_declined_refund" | "unaccepted_seat_refund" | "seat_membership_alignment" | "canceled_membership" | "renewal" | "refund_cancel";
|
|
64
86
|
/** One stable generated case. Provider cases are opt-in and test-mode only. */
|
|
65
87
|
interface ChapterAcceptanceScenario {
|
|
66
88
|
id: string;
|
|
@@ -69,6 +91,9 @@ interface ChapterAcceptanceScenario {
|
|
|
69
91
|
targetId: string;
|
|
70
92
|
siteId: string;
|
|
71
93
|
skuId?: string;
|
|
94
|
+
/** Membership whose add-on path is exercised; never assume the first paid SKU. */
|
|
95
|
+
parentSkuId?: string;
|
|
96
|
+
commercialCaseId?: string;
|
|
72
97
|
siblingTargetId?: string;
|
|
73
98
|
}
|
|
74
99
|
/** Public response shape needed by the non-mutating deployed contract probe. */
|
|
@@ -95,6 +120,8 @@ interface ChapterAcceptanceExecutionContext {
|
|
|
95
120
|
scenario: ChapterAcceptanceScenario;
|
|
96
121
|
target: ChapterAcceptanceTarget;
|
|
97
122
|
sku?: ChapterAcceptanceSku;
|
|
123
|
+
parentSku?: ChapterAcceptanceSku;
|
|
124
|
+
commercialCase?: ChapterAcceptanceCommercialCase;
|
|
98
125
|
siblingTarget?: ChapterAcceptanceTarget;
|
|
99
126
|
}
|
|
100
127
|
/** Provider references permitted in durable evidence. Values must be opaque ids or digests, never PII. */
|
|
@@ -137,6 +164,9 @@ interface ChapterAcceptanceRunResult {
|
|
|
137
164
|
cases: ChapterAcceptanceCaseResult[];
|
|
138
165
|
}
|
|
139
166
|
|
|
167
|
+
/** Compare real observed checkout terms against a separately authored acceptance vector. */
|
|
168
|
+
declare function assertCommercialQuote(expected: ChapterAcceptanceCommercialCase, sku: ChapterAcceptanceSku, actual: unknown): boolean;
|
|
169
|
+
|
|
140
170
|
/**
|
|
141
171
|
* Probe public deployed join contracts without creating applications or provider objects.
|
|
142
172
|
* Each request, including its body, is limited to ten seconds and 64 KiB. Redirects
|
|
@@ -157,6 +187,66 @@ declare function runChapterAcceptanceSuite(manifest: ChapterAcceptanceManifest,
|
|
|
157
187
|
/** PII-free line report suitable for CI logs and PM evidence. */
|
|
158
188
|
declare function formatChapterAcceptanceRun(result: ChapterAcceptanceRunResult): string;
|
|
159
189
|
|
|
190
|
+
/** A release proof is tied to a reviewed source commit and one exact dev deployment. */
|
|
191
|
+
interface ChapterReleaseTarget {
|
|
192
|
+
targetId: string;
|
|
193
|
+
appId: string;
|
|
194
|
+
chapterId: string;
|
|
195
|
+
runtime: string;
|
|
196
|
+
environment: "dev";
|
|
197
|
+
dataEnvironment: "dev";
|
|
198
|
+
stripeMode: "test";
|
|
199
|
+
sourceCommit: string;
|
|
200
|
+
deploymentVersion: string;
|
|
201
|
+
}
|
|
202
|
+
/** Required behavior is defined independently of currently enabled feature flags. */
|
|
203
|
+
interface ChapterReleaseRequirement {
|
|
204
|
+
id: string;
|
|
205
|
+
targetId: string;
|
|
206
|
+
phase: "deterministic" | "deployed_contract" | "provider_dev" | "browser_dev";
|
|
207
|
+
assertions: readonly string[];
|
|
208
|
+
/** Each provider kind needs at least one opaque id/digest from the real attempt. */
|
|
209
|
+
providers: readonly ("stripe" | "clerk" | "calendar" | "email")[];
|
|
210
|
+
cleanup: readonly string[];
|
|
211
|
+
}
|
|
212
|
+
/** An independent release contract, not a list inferred from whichever tests happened to run. */
|
|
213
|
+
interface ChapterReleasePlan {
|
|
214
|
+
schemaVersion: 1;
|
|
215
|
+
targets: readonly ChapterReleaseTarget[];
|
|
216
|
+
requirements: readonly ChapterReleaseRequirement[];
|
|
217
|
+
}
|
|
218
|
+
/** Structured result from an actual driver; this completeness check does not execute or authenticate a driver. */
|
|
219
|
+
interface ChapterReleaseProof {
|
|
220
|
+
requirementId: string;
|
|
221
|
+
target: ChapterReleaseTarget;
|
|
222
|
+
phase: ChapterReleaseRequirement["phase"];
|
|
223
|
+
outcome: "passed" | "failed" | "blocked";
|
|
224
|
+
assertions: Readonly<Record<string, boolean>>;
|
|
225
|
+
references: readonly {
|
|
226
|
+
provider: ChapterReleaseRequirement["providers"][number];
|
|
227
|
+
id: string;
|
|
228
|
+
}[];
|
|
229
|
+
cleanup: Readonly<Record<string, boolean>>;
|
|
230
|
+
}
|
|
231
|
+
/** Bounded release blockers. Passing requests a human review, never permission to deploy. */
|
|
232
|
+
interface ChapterReleaseAssessment {
|
|
233
|
+
readyForReview: boolean;
|
|
234
|
+
productionAuthorized: false;
|
|
235
|
+
required: number;
|
|
236
|
+
passed: number;
|
|
237
|
+
blockers: readonly {
|
|
238
|
+
requirementId: string;
|
|
239
|
+
code: string;
|
|
240
|
+
}[];
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Require a complete, exact-candidate release evidence set. Missing, duplicate,
|
|
244
|
+
* failed, stale, wrong-runtime, skipped, or incompletely cleaned attempts block
|
|
245
|
+
* review. This is evidence validation, not provider execution: callers must use
|
|
246
|
+
* real drivers and retain their independently inspectable run artifacts.
|
|
247
|
+
*/
|
|
248
|
+
declare function assessChapterReleaseEvidence(plan: ChapterReleasePlan, proofs: readonly ChapterReleaseProof[]): ChapterReleaseAssessment;
|
|
249
|
+
|
|
160
250
|
/** Validate the exact facts a multi-site acceptance run needs before it writes anywhere. */
|
|
161
251
|
declare function validateChapterAcceptanceManifest(value: unknown): ChapterAcceptanceValidation;
|
|
162
252
|
/**
|
|
@@ -165,4 +255,4 @@ declare function validateChapterAcceptanceManifest(value: unknown): ChapterAccep
|
|
|
165
255
|
*/
|
|
166
256
|
declare function planChapterAcceptanceMatrix(manifest: ChapterAcceptanceManifest): ChapterAcceptanceScenario[];
|
|
167
257
|
|
|
168
|
-
export { type ChapterAcceptanceCaseResult, type ChapterAcceptanceCleanupEvidence, type ChapterAcceptanceEvidence, type ChapterAcceptanceExecutionContext, type ChapterAcceptanceExecutor, type ChapterAcceptanceManifest, type ChapterAcceptanceOptions, type ChapterAcceptanceProviderReference, type ChapterAcceptanceRunResult, type ChapterAcceptanceScenario, type ChapterAcceptanceScenarioKind, type ChapterAcceptanceSeatOption, type ChapterAcceptanceSku, type ChapterAcceptanceTarget, type ChapterAcceptanceValidation, type ChapterJoinConfigProbe, type ChapterJoinContractFinding, type ChapterJoinContractProbeResult, formatChapterAcceptanceRun, planChapterAcceptanceMatrix, probeChapterJoinContracts, runChapterAcceptanceSuite, validateChapterAcceptanceManifest };
|
|
258
|
+
export { type ChapterAcceptanceCaseResult, type ChapterAcceptanceCleanupEvidence, type ChapterAcceptanceCommercialCase, type ChapterAcceptanceEvidence, type ChapterAcceptanceExecutionContext, type ChapterAcceptanceExecutor, type ChapterAcceptanceManifest, type ChapterAcceptanceOptions, type ChapterAcceptanceProviderReference, type ChapterAcceptanceRunResult, type ChapterAcceptanceScenario, type ChapterAcceptanceScenarioKind, type ChapterAcceptanceSeatOption, type ChapterAcceptanceSku, type ChapterAcceptanceTarget, type ChapterAcceptanceValidation, type ChapterJoinConfigProbe, type ChapterJoinContractFinding, type ChapterJoinContractProbeResult, type ChapterReleaseAssessment, type ChapterReleasePlan, type ChapterReleaseProof, type ChapterReleaseRequirement, type ChapterReleaseTarget, assertCommercialQuote, assessChapterReleaseEvidence, formatChapterAcceptanceRun, planChapterAcceptanceMatrix, probeChapterJoinContracts, runChapterAcceptanceSuite, validateChapterAcceptanceManifest };
|
package/dist/testing.js
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
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
|
|
2
49
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
3
50
|
var TIMEOUT_MS = 1e4;
|
|
@@ -206,14 +253,16 @@ function validateCleanup(cleanup) {
|
|
|
206
253
|
validateLabels(cleanup.checks, "cleanup.checks");
|
|
207
254
|
return { status: cleanup.status, checks: [...cleanup.checks] };
|
|
208
255
|
}
|
|
209
|
-
function contextFor(manifest,
|
|
210
|
-
const scenario2 = planChapterAcceptanceMatrix(manifest).find((item) => item.id ===
|
|
211
|
-
if (!scenario2) throw new TypeError(`unknown acceptance scenario ${
|
|
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}`);
|
|
212
259
|
const target = manifest.targets.find((item) => item.targetId === scenario2.targetId);
|
|
213
260
|
return {
|
|
214
261
|
scenario: scenario2,
|
|
215
262
|
target,
|
|
216
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) } : {},
|
|
217
266
|
...scenario2.siblingTargetId ? {
|
|
218
267
|
siblingTarget: manifest.targets.find((item) => item.targetId === scenario2.siblingTargetId)
|
|
219
268
|
} : {}
|
|
@@ -274,6 +323,66 @@ function formatChapterAcceptanceRun(result) {
|
|
|
274
323
|
return lines.join("\n");
|
|
275
324
|
}
|
|
276
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
|
+
|
|
277
386
|
// src/testing.ts
|
|
278
387
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
279
388
|
var CURRENCY = /^[a-z]{3}$/;
|
|
@@ -281,9 +390,6 @@ var STRIPE_PRICE = /^price_[A-Za-z0-9]{8,}$/;
|
|
|
281
390
|
function activeMemberships2(site) {
|
|
282
391
|
return site.skus.filter((sku) => sku.active && sku.kind === "membership");
|
|
283
392
|
}
|
|
284
|
-
function representativePaidMembership(site) {
|
|
285
|
-
return activeMemberships2(site).find((sku) => sku.priceCents > 0);
|
|
286
|
-
}
|
|
287
393
|
function record2(value) {
|
|
288
394
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
289
395
|
}
|
|
@@ -347,10 +453,10 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
347
453
|
errors.push(`${skuPath} must be an object`);
|
|
348
454
|
continue;
|
|
349
455
|
}
|
|
350
|
-
const
|
|
351
|
-
if (!SLUG.test(
|
|
352
|
-
if (skuIds.has(
|
|
353
|
-
if (
|
|
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);
|
|
354
460
|
if (sku.kind !== "membership" && sku.kind !== "add_on") errors.push(`${skuPath}.kind must be membership or add_on`);
|
|
355
461
|
if (typeof sku.active !== "boolean") errors.push(`${skuPath}.active must be boolean`);
|
|
356
462
|
const priceCents = typeof sku.priceCents === "number" ? sku.priceCents : Number.NaN;
|
|
@@ -367,7 +473,7 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
367
473
|
}
|
|
368
474
|
if (priceCents === 0 && sku.stripePriceId) errors.push(`${skuPath}.stripePriceId must be absent for a free SKU`);
|
|
369
475
|
if (typeof sku.stripePriceId === "string" && sku.stripePriceId) {
|
|
370
|
-
const owner = `${siteId}/${
|
|
476
|
+
const owner = `${siteId}/${id2}`;
|
|
371
477
|
const previous = priceOwners.get(sku.stripePriceId);
|
|
372
478
|
if (previous && previous !== owner) errors.push(`${skuPath}.stripePriceId is already owned by ${previous}`);
|
|
373
479
|
else priceOwners.set(sku.stripePriceId, owner);
|
|
@@ -385,6 +491,7 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
385
491
|
skus.push(sku);
|
|
386
492
|
}
|
|
387
493
|
if (!skus.some((sku) => sku.active && sku.kind === "membership")) errors.push(`${path}.skus must contain an active membership SKU`);
|
|
494
|
+
errors.push(...validateCommercialCases(site.commercialCases, skus, `${path}.commercialCases`));
|
|
388
495
|
const options = record2(site.options);
|
|
389
496
|
if (!options) errors.push(`${path}.options must be an object`);
|
|
390
497
|
if (options?.ordinaryInterview !== true) errors.push(`${path}.options.ordinaryInterview must be true`);
|
|
@@ -413,7 +520,7 @@ function validateChapterAcceptanceManifest(value) {
|
|
|
413
520
|
function scenario(input) {
|
|
414
521
|
return {
|
|
415
522
|
...input,
|
|
416
|
-
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(":")
|
|
417
524
|
};
|
|
418
525
|
}
|
|
419
526
|
function planChapterAcceptanceMatrix(manifest) {
|
|
@@ -424,6 +531,12 @@ function planChapterAcceptanceMatrix(manifest) {
|
|
|
424
531
|
}
|
|
425
532
|
const scenarios = [];
|
|
426
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
|
+
}
|
|
427
540
|
for (const sku of site.skus.filter((candidate) => candidate.active)) {
|
|
428
541
|
scenarios.push(scenario({ suite: "hermetic", kind: "catalog_contract", targetId: site.targetId, siteId: site.siteId, skuId: sku.id }));
|
|
429
542
|
if (sku.priceCents > 0) {
|
|
@@ -452,45 +565,34 @@ function planChapterAcceptanceMatrix(manifest) {
|
|
|
452
565
|
skuId: sku.id
|
|
453
566
|
}));
|
|
454
567
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "ordinary_interview"
|
|
458
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "renewal", targetId: site.targetId, siteId: site.siteId, skuId: paid.id }));
|
|
459
|
-
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" }));
|
|
460
571
|
if (site.options.invitationInterviewWaiver) {
|
|
461
|
-
scenarios.push(scenario({ suite: "provider_dev", kind: "invitation_interview_waiver"
|
|
462
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "expired_interview_waiver"
|
|
463
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "replayed_interview_waiver"
|
|
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" }));
|
|
464
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" }));
|
|
465
579
|
if (site.options.additionalSeat) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
scenarios.push(scenario({
|
|
474
|
-
suite: "hermetic",
|
|
475
|
-
kind: "duplicate_seat_email",
|
|
476
|
-
targetId: site.targetId,
|
|
477
|
-
siteId: site.siteId,
|
|
478
|
-
skuId: site.options.additionalSeat.skuId
|
|
479
|
-
}));
|
|
480
|
-
scenarios.push(scenario({
|
|
481
|
-
suite: "hermetic",
|
|
482
|
-
kind: "expired_seat_window",
|
|
483
|
-
targetId: site.targetId,
|
|
484
|
-
siteId: site.siteId,
|
|
485
|
-
skuId: site.options.additionalSeat.skuId
|
|
486
|
-
}));
|
|
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
|
+
}
|
|
487
587
|
}
|
|
488
|
-
scenarios.push(scenario({ suite: "hermetic", kind: "canceled_membership"
|
|
588
|
+
scenarios.push(scenario({ ...base, suite: "hermetic", kind: "canceled_membership" }));
|
|
489
589
|
}
|
|
490
590
|
}
|
|
491
591
|
return scenarios;
|
|
492
592
|
}
|
|
493
593
|
export {
|
|
594
|
+
assertCommercialQuote,
|
|
595
|
+
assessChapterReleaseEvidence,
|
|
494
596
|
formatChapterAcceptanceRun,
|
|
495
597
|
planChapterAcceptanceMatrix,
|
|
496
598
|
probeChapterJoinContracts,
|