proof-harness 0.1.0-next.1

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 (53) hide show
  1. package/COMPATIBILITY.md +26 -0
  2. package/LICENSE +201 -0
  3. package/PROOF_SDK_CONTRACT.md +1255 -0
  4. package/README.md +60 -0
  5. package/cli/config.mjs +116 -0
  6. package/cli/engines/generate_proof_action_registry.mjs +271 -0
  7. package/cli/engines/parse_proof_schema.mjs +457 -0
  8. package/cli/engines/proof_coverage.mjs +871 -0
  9. package/cli/engines/proof_drift.mjs +980 -0
  10. package/cli/engines/proof_module_check.mjs +498 -0
  11. package/cli/engines/proof_mutation_check.mjs +1084 -0
  12. package/cli/engines/proof_mutation_inventory.mjs +508 -0
  13. package/cli/engines/proof_source_mutation.mjs +35 -0
  14. package/cli/engines/proof_verify.mjs +620 -0
  15. package/cli/engines/scan_module_meta.mjs +352 -0
  16. package/cli/engines/scan_proof_capabilities.mjs +359 -0
  17. package/cli/proof-harness.mjs +107 -0
  18. package/dist/action-types-C3OmHRyH.d.ts +26 -0
  19. package/dist/fixture-B-dmnG5G.d.ts +105 -0
  20. package/dist/mission-types-Dr5rBHYO.d.ts +299 -0
  21. package/dist/node.d.ts +79 -0
  22. package/dist/node.js +553 -0
  23. package/dist/node.js.map +1 -0
  24. package/dist/playwright.d.ts +433 -0
  25. package/dist/playwright.js +1824 -0
  26. package/dist/playwright.js.map +1 -0
  27. package/dist/portable-vocabulary.d.ts +86 -0
  28. package/dist/portable-vocabulary.js +43 -0
  29. package/dist/portable-vocabulary.js.map +1 -0
  30. package/dist/server.d.ts +55 -0
  31. package/dist/server.js +376 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/shared.d.ts +169 -0
  34. package/dist/shared.js +121 -0
  35. package/dist/shared.js.map +1 -0
  36. package/dist/trace-types-DkIzq-l9.d.ts +333 -0
  37. package/package.json +109 -0
  38. package/proof-consumer-fixtures/README.md +60 -0
  39. package/proof-consumer-fixtures/aggregates/M-CONFORMANCE-FAIL.json +112 -0
  40. package/proof-consumer-fixtures/aggregates/M-CONFORMANCE-PASS.json +101 -0
  41. package/proof-consumer-fixtures/baseline/failing/widgets-tenant-isolation.json +46 -0
  42. package/proof-consumer-fixtures/baseline/passing/widgets-tenant-isolation.json +45 -0
  43. package/proof-consumer-fixtures/corpus.json +102 -0
  44. package/proof-consumer-fixtures/duplicates/first/widgets-tenant-isolation.json +12 -0
  45. package/proof-consumer-fixtures/duplicates/second/widgets-tenant-isolation.json +12 -0
  46. package/proof-consumer-fixtures/inputs/capabilities.json +4 -0
  47. package/proof-consumer-fixtures/inputs/mission.json +20 -0
  48. package/proof-consumer-fixtures/inputs/schema.json +4 -0
  49. package/proof-consumer-fixtures/malformed/invalid-trace.json.invalid +4 -0
  50. package/proof-consumer-fixtures/malformed/wrong-manifest-version.json +11 -0
  51. package/proof-consumer-fixtures/malformed/wrong-trace-shape.json +8 -0
  52. package/proof-consumer-fixtures/mutations/MUT-WIDGETS-LEAK/mutation.json +17 -0
  53. package/proof-consumer-fixtures/mutations/MUT-WIDGETS-LEAK/widgets-tenant-isolation.json +50 -0
package/dist/server.js ADDED
@@ -0,0 +1,376 @@
1
+ // src/server/runtime-guard.ts
2
+ import { NextResponse } from "next/server.js";
3
+ function proofGuard(request) {
4
+ const envOk = process.env.NODE_ENV === "development" || process.env.PROOF_MODE === "true";
5
+ if (!envOk) {
6
+ return jsonError(403, {
7
+ error: "[PROOF_FAIL] guard_env: proof endpoints are disabled in this environment",
8
+ suggestion: "Run against a dev server (NODE_ENV=development) or set PROOF_MODE=true on the server."
9
+ });
10
+ }
11
+ const expected = process.env.API_SECRET_KEY;
12
+ if (!expected) {
13
+ return jsonError(500, {
14
+ error: "[PROOF_FAIL] guard_secret_missing: API_SECRET_KEY is not set",
15
+ suggestion: "Set API_SECRET_KEY in the server environment. Proof helpers use it as the shared secret for /api/proof/* routes."
16
+ });
17
+ }
18
+ const provided = request.headers.get("x-proof-secret");
19
+ if (provided !== expected) {
20
+ return jsonError(403, {
21
+ error: "[PROOF_FAIL] guard_secret: x-proof-secret header missing or invalid",
22
+ suggestion: "The runner's API_SECRET_KEY does not match the dev server. If .env.local changed after the server started, restart that server; proof helpers already send the current key in x-proof-secret."
23
+ });
24
+ }
25
+ return null;
26
+ }
27
+ function jsonError(status, body) {
28
+ return NextResponse.json(body, { status });
29
+ }
30
+
31
+ // src/server/service-client.ts
32
+ import { createClient } from "@supabase/supabase-js";
33
+
34
+ // src/server/transient.ts
35
+ var TRANSIENT_MESSAGES = [
36
+ /invalid response was received from the upstream server/i,
37
+ /upstream connect error/i,
38
+ /(service|server) (is )?temporarily unavailable/i,
39
+ /bad gateway/i,
40
+ /gateway time-?out/i,
41
+ /fetch failed/i,
42
+ /network( request)? failed/i,
43
+ /socket hang up/i,
44
+ /ECONNRESET|ECONNREFUSED|EPIPE|ETIMEDOUT|EAI_AGAIN|UND_ERR/
45
+ ];
46
+ function messageChain(error, depth = 0) {
47
+ if (!error || depth > 3) return "";
48
+ if (typeof error === "string") return error;
49
+ const record = error;
50
+ const own = typeof record.message === "string" ? record.message : "";
51
+ return `${own} ${messageChain(record.cause, depth + 1)}`;
52
+ }
53
+ var GATEWAY_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
54
+ function isTransient(error, status) {
55
+ if (!error) return false;
56
+ const httpStatus = status ?? error.status ?? null;
57
+ if (typeof httpStatus === "number") {
58
+ if (GATEWAY_STATUSES.has(httpStatus)) return true;
59
+ if (httpStatus >= 400) return false;
60
+ }
61
+ const text = messageChain(error);
62
+ return TRANSIENT_MESSAGES.some((pattern) => pattern.test(text));
63
+ }
64
+ var TRANSIENT_MAX_ATTEMPTS = 4;
65
+ var BASE_DELAY_MS = 150;
66
+ function backoffMs(attempt) {
67
+ return BASE_DELAY_MS * 2 ** (attempt - 1);
68
+ }
69
+ function announce(label, attempt, maxAttempts, reason, delayMs) {
70
+ console.log(
71
+ `[proof:retry] ${label} failed transiently (attempt ${attempt}/${maxAttempts}): ${reason} \u2014 retrying in ${delayMs}ms`
72
+ );
73
+ }
74
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
75
+ function createRetryingFetch(label, baseFetch = fetch, { maxAttempts = TRANSIENT_MAX_ATTEMPTS } = {}) {
76
+ return async (input, init) => {
77
+ const replayable = init?.body == null || typeof init.body === "string" || init.body instanceof URLSearchParams;
78
+ for (let attempt = 1; ; attempt++) {
79
+ const lastAttempt = attempt >= maxAttempts || !replayable;
80
+ try {
81
+ const response = await baseFetch(input, init);
82
+ if (lastAttempt || !GATEWAY_STATUSES.has(response.status)) {
83
+ return response;
84
+ }
85
+ const delayMs = backoffMs(attempt);
86
+ announce(
87
+ label,
88
+ attempt,
89
+ maxAttempts,
90
+ `HTTP ${response.status}`,
91
+ delayMs
92
+ );
93
+ await sleep(delayMs);
94
+ } catch (error) {
95
+ if (lastAttempt || !isTransient(error))
96
+ throw error;
97
+ const delayMs = backoffMs(attempt);
98
+ announce(
99
+ label,
100
+ attempt,
101
+ maxAttempts,
102
+ messageChain(error).trim() || "transport failure",
103
+ delayMs
104
+ );
105
+ await sleep(delayMs);
106
+ }
107
+ }
108
+ };
109
+ }
110
+
111
+ // src/server/service-client.ts
112
+ function requireProofEnv(name) {
113
+ const value = process.env[name];
114
+ if (!value) {
115
+ throw new Error(
116
+ `[PROOF_FAIL] missing_env: expected ${name}, found empty
117
+ file: src/server/service-client.ts
118
+ suggestion: Ensure CI exports the Supabase env vars before running pnpm proof:verify.`
119
+ );
120
+ }
121
+ return value;
122
+ }
123
+ function createProofServiceClient() {
124
+ return createClient(
125
+ requireProofEnv("NEXT_PUBLIC_SUPABASE_URL"),
126
+ requireProofEnv("SUPABASE_SECRET_KEY"),
127
+ {
128
+ auth: {
129
+ persistSession: false,
130
+ autoRefreshToken: false,
131
+ detectSessionInUrl: false
132
+ },
133
+ // Local Supabase sits behind Kong, which intermittently 502s under the
134
+ // load of a proof run. Installing the retry here covers every query made
135
+ // through this client, including ad-hoc ones inside proof specs.
136
+ global: { fetch: createRetryingFetch("service-role request") }
137
+ }
138
+ );
139
+ }
140
+
141
+ // src/server/seed.ts
142
+ function failProof(category, expected, found, suggestion = `Check that Supabase is running (pnpm exec supabase status) and that SUPABASE_SECRET_KEY is set in .env.local.`) {
143
+ throw new Error(
144
+ `[PROOF_FAIL] ${category}: expected ${expected}, found ${found}
145
+ file: src/server/seed.ts
146
+ suggestion: ${suggestion}`
147
+ );
148
+ }
149
+ function failSeed(category, expected, found, error, status) {
150
+ if (isTransient(error, status)) {
151
+ failProof(
152
+ `${category}_transient`,
153
+ expected,
154
+ `${found} \u2014 still failing after ${TRANSIENT_MAX_ATTEMPTS} attempts`,
155
+ `This is a gateway/transport failure, not a schema problem: the request never reached Postgres. Check that the local Supabase stack is healthy (pnpm exec supabase status) and is not restarting mid-run.`
156
+ );
157
+ }
158
+ failProof(category, expected, found);
159
+ }
160
+ async function waitForPublicUser(sb, userId, email) {
161
+ const maxAttempts = 10;
162
+ const delayMs = 50;
163
+ for (let i = 0; i < maxAttempts; i++) {
164
+ const { data } = await sb.from("users").select("id").eq("id", userId).maybeSingle();
165
+ if (data) return;
166
+ await new Promise((r) => setTimeout(r, delayMs));
167
+ }
168
+ failProof(
169
+ "seed_user_trigger",
170
+ `public.users row for ${email} (id ${userId}) within ${maxAttempts * delayMs}ms`,
171
+ "row still missing \u2014 the handle_new_user trigger did not materialize the public.users mirror row"
172
+ );
173
+ }
174
+ var seed = {
175
+ /**
176
+ * Create a workspace with no owner. Use `seed.user({ workspace, role: "owner" })`
177
+ * afterwards to attach an owner. Returns the inserted row.
178
+ *
179
+ * Bypasses RLS via the service-role client, so it works even when no user
180
+ * is authenticated.
181
+ */
182
+ async workspace(name) {
183
+ const sb = createProofServiceClient();
184
+ const { data, error, status } = await sb.from("workspaces").insert({
185
+ name,
186
+ type: "team"
187
+ }).select("id, name, owner_id").single();
188
+ if (error || !data) {
189
+ failSeed(
190
+ "seed_workspace",
191
+ "a workspace row to be created",
192
+ `insert error: ${error?.message ?? "no row returned"}`,
193
+ error,
194
+ status
195
+ );
196
+ }
197
+ return {
198
+ id: data.id,
199
+ name: data.name,
200
+ ownerId: data.owner_id
201
+ };
202
+ },
203
+ /**
204
+ * Create an auth user with the given credentials and attach them to the
205
+ * provided workspace with the given role. Relies on the `handle_new_user`
206
+ * trigger to mirror the row into `public.users` automatically.
207
+ */
208
+ async user(opts) {
209
+ const sb = createProofServiceClient();
210
+ const { data: userData, error: createErr } = await sb.auth.admin.createUser(
211
+ {
212
+ email: opts.email,
213
+ password: opts.password,
214
+ email_confirm: true
215
+ }
216
+ );
217
+ if (createErr || !userData?.user) {
218
+ failSeed(
219
+ "seed_user_auth",
220
+ `auth user to be created for ${opts.email}`,
221
+ `admin.createUser error: ${createErr?.message ?? "no user returned"}`,
222
+ createErr
223
+ );
224
+ }
225
+ const userId = userData.user.id;
226
+ await waitForPublicUser(sb, userId, opts.email);
227
+ const {
228
+ data: memberRow,
229
+ error: memberErr,
230
+ status: memberStatus
231
+ } = await sb.from("workspace_members").insert({
232
+ workspace_id: opts.workspace.id,
233
+ user_id: userId,
234
+ role: opts.role
235
+ }).select("id").single();
236
+ if (memberErr || !memberRow) {
237
+ failSeed(
238
+ "seed_user_membership",
239
+ `workspace_members row for user ${opts.email} in workspace ${opts.workspace.id}`,
240
+ `insert error: ${memberErr?.message ?? "no row returned"}`,
241
+ memberErr,
242
+ memberStatus
243
+ );
244
+ }
245
+ return {
246
+ id: userId,
247
+ email: opts.email,
248
+ password: opts.password,
249
+ workspaceId: opts.workspace.id,
250
+ role: opts.role,
251
+ membershipId: memberRow.id
252
+ };
253
+ },
254
+ /**
255
+ * Delete a workspace and cascade-clean its members. Used by helpers for
256
+ * teardown. Safe to call on a workspace that no longer exists.
257
+ */
258
+ async deleteWorkspace(workspaceId) {
259
+ const sb = createProofServiceClient();
260
+ await sb.from("workspaces").delete().eq("id", workspaceId);
261
+ },
262
+ /**
263
+ * Delete an auth user (cascades to public.users and workspace_members via
264
+ * the schema's ON DELETE CASCADE). Safe to call if the user no longer
265
+ * exists.
266
+ */
267
+ async deleteUser(userId) {
268
+ const sb = createProofServiceClient();
269
+ await sb.auth.admin.deleteUser(userId);
270
+ }
271
+ };
272
+
273
+ // src/server/auth-rate-limit.ts
274
+ var AUTH_RATE_LIMIT_MAX_ATTEMPTS = 3;
275
+ var AUTH_RATE_LIMIT_MAX_DELAY_MS = 5e3;
276
+ var AUTH_RATE_LIMIT_MAX_TOTAL_WAIT_MS = 1e4;
277
+ var AUTH_RATE_LIMIT_FALLBACK_DELAY_MS = 1e3;
278
+ function isAuthRateLimited(error, status, body) {
279
+ const httpStatus = status ?? error?.status ?? null;
280
+ if (httpStatus === 429) return true;
281
+ if (typeof httpStatus === "number" && httpStatus >= 400) return false;
282
+ const text = `${error?.message ?? ""} ${body ?? ""}`;
283
+ return /\[PROOF_FAIL\]\s*(auth_)?rate_limited:/i.test(text) || /\b(rate limit|too many (requests|attempts|sign-?ins?))\b/i.test(text);
284
+ }
285
+ function parseRetryAfterMs(value, now = Date.now()) {
286
+ if (typeof value === "number" && Number.isFinite(value)) {
287
+ return Math.max(0, value * 1e3);
288
+ }
289
+ if (typeof value !== "string" || !value.trim()) return null;
290
+ const seconds = Number(value);
291
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
292
+ const at = Date.parse(value);
293
+ return Number.isFinite(at) ? Math.max(0, at - now) : null;
294
+ }
295
+ function clampAuthRateLimitDelay(retryAfterMs, remainingBudgetMs = AUTH_RATE_LIMIT_MAX_TOTAL_WAIT_MS) {
296
+ const requested = retryAfterMs ?? AUTH_RATE_LIMIT_FALLBACK_DELAY_MS;
297
+ return Math.max(
298
+ 0,
299
+ Math.min(requested, AUTH_RATE_LIMIT_MAX_DELAY_MS, remainingBudgetMs)
300
+ );
301
+ }
302
+ async function retryAuthRateLimit(label, run, classify, {
303
+ maxAttempts = AUTH_RATE_LIMIT_MAX_ATTEMPTS,
304
+ maxTotalWaitMs = AUTH_RATE_LIMIT_MAX_TOTAL_WAIT_MS,
305
+ sleep: sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
306
+ } = {}) {
307
+ let result = await run();
308
+ let waitedMs = 0;
309
+ for (let attempt = 1; attempt < maxAttempts; attempt += 1) {
310
+ const signal = classify(result);
311
+ if (!signal.limited) return result;
312
+ const remaining = maxTotalWaitMs - waitedMs;
313
+ if (remaining <= 0) return result;
314
+ const delayMs = clampAuthRateLimitDelay(
315
+ parseRetryAfterMs(signal.retryAfter),
316
+ remaining
317
+ );
318
+ if (delayMs <= 0) return result;
319
+ console.log(
320
+ `[proof:retry] ${label} auth rate limited (attempt ${attempt}/${maxAttempts}) \u2014 retrying in ${delayMs}ms`
321
+ );
322
+ await sleep2(delayMs);
323
+ waitedMs += delayMs;
324
+ result = await run();
325
+ }
326
+ return result;
327
+ }
328
+
329
+ // src/server/fixture.ts
330
+ function defineProofFixture(options) {
331
+ return Object.freeze({
332
+ table: options.table,
333
+ create: options.create
334
+ });
335
+ }
336
+ var ProofFixturePendingError = class extends Error {
337
+ constructor(table, reason) {
338
+ super(
339
+ `Fixture factory for "${table}" is incomplete: ${reason}. Complete e2e/fixtures/${table}.ts with values valid for the final schema.`
340
+ );
341
+ this.table = table;
342
+ this.reason = reason;
343
+ this.name = "ProofFixturePendingError";
344
+ }
345
+ table;
346
+ reason;
347
+ code = "fixture_factory_required";
348
+ };
349
+ function pendingProofFixture(table, reason = "the table schema and product constraints are not final") {
350
+ return defineProofFixture({
351
+ table,
352
+ async create() {
353
+ throw new ProofFixturePendingError(table, reason);
354
+ }
355
+ });
356
+ }
357
+ function isProofFixturePendingError(error) {
358
+ return error instanceof ProofFixturePendingError;
359
+ }
360
+ export {
361
+ AUTH_RATE_LIMIT_MAX_ATTEMPTS,
362
+ AUTH_RATE_LIMIT_MAX_DELAY_MS,
363
+ AUTH_RATE_LIMIT_MAX_TOTAL_WAIT_MS,
364
+ ProofFixturePendingError,
365
+ clampAuthRateLimitDelay,
366
+ createProofServiceClient,
367
+ defineProofFixture,
368
+ isAuthRateLimited,
369
+ isProofFixturePendingError,
370
+ parseRetryAfterMs,
371
+ pendingProofFixture,
372
+ proofGuard,
373
+ retryAuthRateLimit,
374
+ seed
375
+ };
376
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/runtime-guard.ts","../src/server/service-client.ts","../src/server/transient.ts","../src/server/seed.ts","../src/server/auth-rate-limit.ts","../src/server/fixture.ts"],"sourcesContent":["// Import External Packages\nimport { NextResponse } from \"next/server.js\";\n// Import Local Imports\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n// ---------------------------------------------------------------------------\n// Runtime guard for /api/proof/* routes.\n//\n// The Proof runtime endpoints (seed-login, later capabilities/schema) exist\n// only to support local verification runs and CI. They must never be usable\n// in production builds aimed at end users.\n//\n// Guarded by two gates:\n// 1. Environment: NODE_ENV === \"development\" OR PROOF_MODE === \"true\"\n// 2. Shared secret: x-proof-secret header must match API_SECRET_KEY\n//\n// The secret gate reuses the existing API_SECRET_KEY rather than introducing\n// a new env var — one fewer thing to configure per environment.\n// ---------------------------------------------------------------------------\n\ninterface GuardRejection {\n error: string;\n suggestion: string;\n}\n\n/**\n * Check whether a request is allowed to hit a /api/proof/* route.\n *\n * @returns `null` when the request may proceed, or a `Response` to return\n * directly (403) when the request must be blocked.\n */\nexport function proofGuard(request: Request): Response | null {\n const envOk =\n process.env.NODE_ENV === \"development\" || process.env.PROOF_MODE === \"true\";\n\n if (!envOk) {\n return jsonError(403, {\n error:\n \"[PROOF_FAIL] guard_env: proof endpoints are disabled in this environment\",\n suggestion:\n \"Run against a dev server (NODE_ENV=development) or set PROOF_MODE=true on the server.\",\n });\n }\n\n const expected = process.env.API_SECRET_KEY;\n if (!expected) {\n return jsonError(500, {\n error: \"[PROOF_FAIL] guard_secret_missing: API_SECRET_KEY is not set\",\n suggestion:\n \"Set API_SECRET_KEY in the server environment. Proof helpers use it as the shared secret for /api/proof/* routes.\",\n });\n }\n\n const provided = request.headers.get(\"x-proof-secret\");\n if (provided !== expected) {\n return jsonError(403, {\n error:\n \"[PROOF_FAIL] guard_secret: x-proof-secret header missing or invalid\",\n suggestion:\n \"The runner's API_SECRET_KEY does not match the dev server. If .env.local changed after the server started, restart that server; proof helpers already send the current key in x-proof-secret.\",\n });\n }\n\n return null;\n}\n\nfunction jsonError(status: number, body: GuardRejection): Response {\n return NextResponse.json(body, { status });\n}\n","// Import External Packages\nimport { createClient } from \"@supabase/supabase-js\";\n// Import Local Imports\nimport { createRetryingFetch } from \"./transient\";\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n// ---------------------------------------------------------------------------\n// Thin re-export of the service-role Supabase client, scoped to Proof.\n//\n// Proof helpers use the service-role client for two things:\n// 1. Seed fixture setup (`seed.workspace`, `seed.user`)\n// 2. Teardown of test data after assertions complete\n//\n// Consumers inside `.proof.ts` specs receive the result of this factory\n// through the `setup({ sb })` callback of `assert.tenantIsolation`. They\n// should NEVER construct a service-role client themselves — keeping the\n// construction centralized makes it easy to add logging, usage limits, or\n// a future mock in one place.\n//\n// This package does not depend on an application's generated database types or\n// service-client wrapper. The factory reads the standard environment variables\n// directly and gives proof-specific diagnostics.\n// ---------------------------------------------------------------------------\n\nfunction requireProofEnv(name: string): string {\n const value = process.env[name];\n if (!value) {\n throw new Error(\n `[PROOF_FAIL] missing_env: expected ${name}, found empty\\n` +\n \" file: src/server/service-client.ts\\n\" +\n \" suggestion: Ensure CI exports the Supabase env vars before running pnpm proof:verify.\",\n );\n }\n return value;\n}\n\n/**\n * Factory for a service-role Supabase client used inside Proof helpers and\n * `.proof.ts` spec files.\n *\n * Returns a service-role Supabase client so spec code can safely type the `sb`\n * parameter as the library's `SupabaseClient`.\n */\nexport function createProofServiceClient() {\n return createClient(\n requireProofEnv(\"NEXT_PUBLIC_SUPABASE_URL\"),\n requireProofEnv(\"SUPABASE_SECRET_KEY\"),\n {\n auth: {\n persistSession: false,\n autoRefreshToken: false,\n detectSessionInUrl: false,\n },\n // Local Supabase sits behind Kong, which intermittently 502s under the\n // load of a proof run. Installing the retry here covers every query made\n // through this client, including ad-hoc ones inside proof specs.\n global: { fetch: createRetryingFetch(\"service-role request\") },\n },\n );\n}\n","// Import External Packages\n// Import Local Imports\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n// ---------------------------------------------------------------------------\n// Transient-failure retry\n//\n// Local Supabase sits behind Kong. During a proof run — every spec seeding its\n// own fixtures through the service role, plus the dev server's own queries — the\n// gateway intermittently answers a request with a 502 whose body reads \"An\n// invalid response was received from the upstream server\", then serves the next\n// request normally. Roughly one local `proof:check` in two hit this, always on a\n// different spec.\n//\n// That noise is not evidence about the code under test, and it can hurt in two\n// ways:\n//\n// 1. A spurious red build. Worse than it sounds: a suite that fails half the\n// time for reasons nobody controls teaches people to re-run until green,\n// which is exactly the reflex that lets a real failure through.\n// 2. A false pass. Several probes read \"the actor's write returned an error and\n// no row landed\" as proof of denial. A write that never reached PostgREST\n// looks identical, so an unretried gateway error can certify an invariant\n// that was never exercised. Callers use `isTransient` to refuse that\n// reading.\n//\n// The retry lives at the transport (`createRetryingFetch`, installed on every\n// Supabase client the SDK hands out) rather than at each call site, because the\n// call sites that matter most are the ad-hoc queries inside `.proof.ts` specs —\n// there is no version of \"remember to wrap it\" that survives the next spec\n// somebody writes.\n//\n// It is bounded, announced on stdout, and deliberately narrow: only gateway\n// statuses (502/503/504) and transport-level failures qualify. A 4xx is the\n// database answering, and even a 500 is usually PostgREST or a route handler\n// answering; retrying either would turn a real failure into a slow real failure,\n// or worse, hide it.\n// ---------------------------------------------------------------------------\n\n/** The shape both `PostgrestError` and `AuthError` satisfy. */\nexport interface TransientErrorLike {\n message?: string | null;\n status?: number | null;\n code?: string | null;\n}\n\n/**\n * Messages that mean \"the request did not reach the database\", matched because\n * Supabase surfaces gateway and transport failures as error text without a\n * usable status on every client path.\n */\nconst TRANSIENT_MESSAGES = [\n /invalid response was received from the upstream server/i,\n /upstream connect error/i,\n /(service|server) (is )?temporarily unavailable/i,\n /bad gateway/i,\n /gateway time-?out/i,\n /fetch failed/i,\n /network( request)? failed/i,\n /socket hang up/i,\n /ECONNRESET|ECONNREFUSED|EPIPE|ETIMEDOUT|EAI_AGAIN|UND_ERR/,\n];\n\n/** Node wraps the real cause; the useful text is often one or two levels down. */\nfunction messageChain(error: unknown, depth = 0): string {\n if (!error || depth > 3) return \"\";\n if (typeof error === \"string\") return error;\n const record = error as { message?: unknown; cause?: unknown };\n const own = typeof record.message === \"string\" ? record.message : \"\";\n return `${own} ${messageChain(record.cause, depth + 1)}`;\n}\n\n/**\n * Statuses the gateway returns when it could not get an answer out of the\n * upstream service. A 500 is excluded on purpose: PostgREST and our own route\n * handlers use it to report real failures.\n */\nexport const GATEWAY_STATUSES = new Set([502, 503, 504]);\n\n/**\n * True when a failure is infrastructure, not an answer.\n *\n * `status` is the HTTP status when the caller has it separately from the error\n * (PostgREST responses carry it alongside `error`; auth errors carry it inside).\n */\nexport function isTransient(\n error: TransientErrorLike | null | undefined,\n status?: number | null,\n): boolean {\n if (!error) return false;\n const httpStatus = status ?? error.status ?? null;\n if (typeof httpStatus === \"number\") {\n if (GATEWAY_STATUSES.has(httpStatus)) return true;\n // Any other status is the stack answering — a denial, a validation error, an\n // internal error — and each is a fact the proof is entitled to act on.\n if (httpStatus >= 400) return false;\n }\n const text = messageChain(error);\n return TRANSIENT_MESSAGES.some((pattern) => pattern.test(text));\n}\n\nexport const TRANSIENT_MAX_ATTEMPTS = 4;\nconst BASE_DELAY_MS = 150;\n\nfunction backoffMs(attempt: number): number {\n return BASE_DELAY_MS * 2 ** (attempt - 1);\n}\n\nfunction announce(\n label: string,\n attempt: number,\n maxAttempts: number,\n reason: string,\n delayMs: number,\n): void {\n // Announced rather than silent: a suite that only goes green on its third\n // attempt every time is a real problem, and hidden retries hide it.\n console.log(\n `[proof:retry] ${label} failed transiently (attempt ${attempt}/${maxAttempts}): ` +\n `${reason} — retrying in ${delayMs}ms`,\n );\n}\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * A `fetch` that retries gateway failures, for installing on a Supabase client\n * via `createClient(url, key, { global: { fetch } })`.\n *\n * Covers every request that client makes — PostgREST queries, auth admin calls,\n * RPC — including the ad-hoc ones written inside proof specs, which is the whole\n * reason the retry lives here instead of at the call sites.\n *\n * Retrying a write is safe here only because gateway failures mean the request\n * did not reach PostgREST; a request carrying a stream body is never retried,\n * since it cannot be replayed and we cannot know how far it got.\n */\nexport function createRetryingFetch(\n label: string,\n baseFetch: typeof fetch = fetch,\n { maxAttempts = TRANSIENT_MAX_ATTEMPTS } = {},\n): typeof fetch {\n return async (input, init) => {\n const replayable =\n init?.body == null ||\n typeof init.body === \"string\" ||\n init.body instanceof URLSearchParams;\n\n for (let attempt = 1; ; attempt++) {\n const lastAttempt = attempt >= maxAttempts || !replayable;\n try {\n const response = await baseFetch(input, init);\n if (lastAttempt || !GATEWAY_STATUSES.has(response.status)) {\n return response;\n }\n const delayMs = backoffMs(attempt);\n announce(\n label,\n attempt,\n maxAttempts,\n `HTTP ${response.status}`,\n delayMs,\n );\n await sleep(delayMs);\n } catch (error) {\n if (lastAttempt || !isTransient(error as TransientErrorLike))\n throw error;\n const delayMs = backoffMs(attempt);\n announce(\n label,\n attempt,\n maxAttempts,\n messageChain(error).trim() || \"transport failure\",\n delayMs,\n );\n await sleep(delayMs);\n }\n }\n };\n}\n\n/**\n * Retry a call that reports failure in its result rather than by throwing.\n *\n * For anything going through a Supabase client, prefer `createRetryingFetch` —\n * it is already installed. This is for the surfaces that do not use it, such as\n * Playwright's `APIRequestContext`.\n *\n * `run` must build the request from scratch on each call: a PostgREST builder is\n * a one-shot thenable, so re-awaiting the same object does not re-issue it.\n *\n * The final result is returned as-is — including a still-failing error — because\n * the caller owns the diagnostics. Nothing here throws, and nothing here decides\n * what a failure means.\n */\nexport async function retryTransient<\n T extends { error?: TransientErrorLike | null; status?: number },\n>(\n label: string,\n // `PromiseLike`, not `Promise`: a PostgREST builder is a thenable, and typing\n // this as `Promise<T>` makes inference fall back to the constraint, which\n // erases `data` and `count` from the caller's result.\n run: () => PromiseLike<T>,\n { maxAttempts = TRANSIENT_MAX_ATTEMPTS } = {},\n): Promise<T> {\n let result = await run();\n\n for (let attempt = 1; attempt < maxAttempts; attempt++) {\n if (!isTransient(result.error, result.status)) return result;\n const delayMs = backoffMs(attempt);\n announce(\n label,\n attempt,\n maxAttempts,\n result.error?.message ?? \"unknown error\",\n delayMs,\n );\n await sleep(delayMs);\n result = await run();\n }\n\n return result;\n}\n","// Import External Packages\n// Import Local Imports\nimport { createProofServiceClient } from \"./service-client\";\nimport { isTransient, TRANSIENT_MAX_ATTEMPTS } from \"./transient\";\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n// ---------------------------------------------------------------------------\n// seed.workspace / seed.user\n//\n// Fixture helpers used by `.proof.ts` specs and by `assert.tenantIsolation`\n// to set up isolated test data. They bypass the business-logic pipeline on\n// purpose: creating fixtures should not exercise the very actions we are\n// trying to verify. Instead we insert directly via the service-role client.\n//\n// Both helpers are idempotent on upsert conflicts is NOT implemented; callers\n// are responsible for passing unique names/emails (`assert.tenantIsolation`\n// uses a short random suffix).\n// ---------------------------------------------------------------------------\n\nexport interface SeedWorkspace {\n id: string;\n name: string;\n ownerId: string | null;\n}\n\nexport interface SeedUser {\n id: string;\n email: string;\n password: string;\n workspaceId: string;\n role: \"owner\" | \"admin\" | \"member\" | \"viewer\";\n /**\n * `workspace_members.id` for this user's membership row in `workspaceId`.\n * Needed by proofs that invoke actions keyed on the membership (e.g.\n * `removeMember` takes `memberId = workspace_members.id`, not user_id).\n */\n membershipId: string;\n}\n\nfunction failProof(\n category: string,\n expected: string,\n found: string,\n suggestion = `Check that Supabase is running (pnpm exec supabase status) and that SUPABASE_SECRET_KEY is set in .env.local.`,\n): never {\n throw new Error(\n `[PROOF_FAIL] ${category}: expected ${expected}, found ${found}\\n` +\n ` file: src/server/seed.ts\\n` +\n ` suggestion: ${suggestion}`,\n );\n}\n\n/**\n * Fixture setup that failed because the gateway never answered is reported as\n * infrastructure. Otherwise the operator reads \"expected a workspace row to be\n * created\" and goes looking for a broken migration.\n */\nfunction failSeed(\n category: string,\n expected: string,\n found: string,\n error: { message?: string | null; status?: number | null } | null | undefined,\n status?: number,\n): never {\n if (isTransient(error, status)) {\n failProof(\n `${category}_transient`,\n expected,\n `${found} — still failing after ${TRANSIENT_MAX_ATTEMPTS} attempts`,\n `This is a gateway/transport failure, not a schema problem: the request never reached Postgres. ` +\n `Check that the local Supabase stack is healthy (pnpm exec supabase status) and is not restarting mid-run.`,\n );\n }\n failProof(category, expected, found);\n}\n\n/**\n * Wait until `public.users` has a row for the given `userId`.\n *\n * `supabase.auth.admin.createUser` creates a row in `auth.users`, which fires\n * the `handle_new_user` trigger that inserts into `public.users`. Postgres\n * runs the trigger inside the same transaction as the INSERT, so the row\n * exists the moment the admin API's HTTP call returns — in theory.\n *\n * In practice the admin API occasionally returns before the post-commit\n * visibility window has settled, and the very next `workspace_members`\n * INSERT fails its FK to `public.users(id)`. This poll closes that window\n * deterministically: short, bounded, ~500ms worst case. A real failure\n * still surfaces with a structured [PROOF_FAIL] so the race is not masked.\n *\n * Note: `workspace_members.user_id` FKs to `public.users(id)`, NOT\n * `auth.users`. The value is the same UUID because `handle_new_user`\n * copies it, but the FK target matters if future migrations ever split\n * them.\n */\nasync function waitForPublicUser(\n sb: ReturnType<typeof createProofServiceClient>,\n userId: string,\n email: string,\n): Promise<void> {\n const maxAttempts = 10;\n const delayMs = 50;\n for (let i = 0; i < maxAttempts; i++) {\n const { data } = await sb\n .from(\"users\")\n .select(\"id\")\n .eq(\"id\", userId)\n .maybeSingle();\n if (data) return;\n await new Promise((r) => setTimeout(r, delayMs));\n }\n failProof(\n \"seed_user_trigger\",\n `public.users row for ${email} (id ${userId}) within ${maxAttempts * delayMs}ms`,\n \"row still missing — the handle_new_user trigger did not materialize the public.users mirror row\",\n );\n}\n\nexport const seed = {\n /**\n * Create a workspace with no owner. Use `seed.user({ workspace, role: \"owner\" })`\n * afterwards to attach an owner. Returns the inserted row.\n *\n * Bypasses RLS via the service-role client, so it works even when no user\n * is authenticated.\n */\n async workspace(name: string): Promise<SeedWorkspace> {\n const sb = createProofServiceClient();\n\n const { data, error, status } = await sb\n .from(\"workspaces\")\n .insert({\n name,\n type: \"team\",\n })\n .select(\"id, name, owner_id\")\n .single();\n\n if (error || !data) {\n failSeed(\n \"seed_workspace\",\n \"a workspace row to be created\",\n `insert error: ${error?.message ?? \"no row returned\"}`,\n error,\n status,\n );\n }\n\n return {\n id: data.id,\n name: data.name,\n ownerId: data.owner_id,\n };\n },\n\n /**\n * Create an auth user with the given credentials and attach them to the\n * provided workspace with the given role. Relies on the `handle_new_user`\n * trigger to mirror the row into `public.users` automatically.\n */\n async user(opts: {\n email: string;\n password: string;\n workspace: SeedWorkspace;\n role: \"owner\" | \"admin\" | \"member\" | \"viewer\";\n }): Promise<SeedUser> {\n const sb = createProofServiceClient();\n\n const { data: userData, error: createErr } = await sb.auth.admin.createUser(\n {\n email: opts.email,\n password: opts.password,\n email_confirm: true,\n },\n );\n\n if (createErr || !userData?.user) {\n failSeed(\n \"seed_user_auth\",\n `auth user to be created for ${opts.email}`,\n `admin.createUser error: ${createErr?.message ?? \"no user returned\"}`,\n createErr,\n );\n }\n\n const userId = userData.user.id;\n\n // `workspace_members.user_id` references `public.users(id)`, not\n // `auth.users`. The `handle_new_user` trigger mirrors the row on\n // `auth.users` INSERT; poll briefly to rule out any post-commit\n // visibility race before the FK insert below.\n await waitForPublicUser(sb, userId, opts.email);\n\n const {\n data: memberRow,\n error: memberErr,\n status: memberStatus,\n } = await sb\n .from(\"workspace_members\")\n .insert({\n workspace_id: opts.workspace.id,\n user_id: userId,\n role: opts.role,\n })\n .select(\"id\")\n .single();\n\n if (memberErr || !memberRow) {\n failSeed(\n \"seed_user_membership\",\n `workspace_members row for user ${opts.email} in workspace ${opts.workspace.id}`,\n `insert error: ${memberErr?.message ?? \"no row returned\"}`,\n memberErr,\n memberStatus,\n );\n }\n\n return {\n id: userId,\n email: opts.email,\n password: opts.password,\n workspaceId: opts.workspace.id,\n role: opts.role,\n membershipId: memberRow.id,\n };\n },\n\n /**\n * Delete a workspace and cascade-clean its members. Used by helpers for\n * teardown. Safe to call on a workspace that no longer exists.\n */\n async deleteWorkspace(workspaceId: string): Promise<void> {\n const sb = createProofServiceClient();\n await sb.from(\"workspaces\").delete().eq(\"id\", workspaceId);\n },\n\n /**\n * Delete an auth user (cascades to public.users and workspace_members via\n * the schema's ON DELETE CASCADE). Safe to call if the user no longer\n * exists.\n */\n async deleteUser(userId: string): Promise<void> {\n const sb = createProofServiceClient();\n await sb.auth.admin.deleteUser(userId);\n },\n};\n","// ---------------------------------------------------------------------------\n// Bounded authentication rate-limit retry.\n//\n// HTTP 429 is neither a product verdict nor a transient gateway failure. Proof\n// login may retry it briefly, honoring Retry-After within a strict budget; once\n// exhausted it remains an explicit auth_rate_limited infrastructure diagnosis.\n// ---------------------------------------------------------------------------\n\nexport interface AuthRateLimitErrorLike {\n message?: string | null;\n status?: number | null;\n}\n\nexport const AUTH_RATE_LIMIT_MAX_ATTEMPTS = 3;\nexport const AUTH_RATE_LIMIT_MAX_DELAY_MS = 5_000;\nexport const AUTH_RATE_LIMIT_MAX_TOTAL_WAIT_MS = 10_000;\nconst AUTH_RATE_LIMIT_FALLBACK_DELAY_MS = 1_000;\n\nexport function isAuthRateLimited(\n error: AuthRateLimitErrorLike | null | undefined,\n status?: number | null,\n body?: string | null,\n): boolean {\n const httpStatus = status ?? error?.status ?? null;\n if (httpStatus === 429) return true;\n if (typeof httpStatus === \"number\" && httpStatus >= 400) return false;\n\n const text = `${error?.message ?? \"\"} ${body ?? \"\"}`;\n return (\n /\\[PROOF_FAIL\\]\\s*(auth_)?rate_limited:/i.test(text) ||\n /\\b(rate limit|too many (requests|attempts|sign-?ins?))\\b/i.test(text)\n );\n}\n\nexport function parseRetryAfterMs(\n value: string | number | null | undefined,\n now = Date.now(),\n): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return Math.max(0, value * 1_000);\n }\n if (typeof value !== \"string\" || !value.trim()) return null;\n\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);\n\n const at = Date.parse(value);\n return Number.isFinite(at) ? Math.max(0, at - now) : null;\n}\n\nexport function clampAuthRateLimitDelay(\n retryAfterMs: number | null,\n remainingBudgetMs = AUTH_RATE_LIMIT_MAX_TOTAL_WAIT_MS,\n): number {\n const requested = retryAfterMs ?? AUTH_RATE_LIMIT_FALLBACK_DELAY_MS;\n return Math.max(\n 0,\n Math.min(requested, AUTH_RATE_LIMIT_MAX_DELAY_MS, remainingBudgetMs),\n );\n}\n\nexport async function retryAuthRateLimit<T>(\n label: string,\n run: () => PromiseLike<T>,\n classify: (result: T) => {\n limited: boolean;\n retryAfter?: string | number | null;\n },\n {\n maxAttempts = AUTH_RATE_LIMIT_MAX_ATTEMPTS,\n maxTotalWaitMs = AUTH_RATE_LIMIT_MAX_TOTAL_WAIT_MS,\n sleep = (ms: number) =>\n new Promise<void>((resolve) => setTimeout(resolve, ms)),\n } = {},\n): Promise<T> {\n let result = await run();\n let waitedMs = 0;\n\n for (let attempt = 1; attempt < maxAttempts; attempt += 1) {\n const signal = classify(result);\n if (!signal.limited) return result;\n\n const remaining = maxTotalWaitMs - waitedMs;\n if (remaining <= 0) return result;\n const delayMs = clampAuthRateLimitDelay(\n parseRetryAfterMs(signal.retryAfter),\n remaining,\n );\n if (delayMs <= 0) return result;\n\n console.log(\n `[proof:retry] ${label} auth rate limited (attempt ${attempt}/${maxAttempts}) — retrying in ${delayMs}ms`,\n );\n await sleep(delayMs);\n waitedMs += delayMs;\n result = await run();\n }\n\n return result;\n}\n","// Import External Packages\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n// Import Local Imports\nimport type { SeedUser, SeedWorkspace } from \"./seed\";\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n/**\n * Seed handles available to a tenant-isolation fixture factory.\n *\n * A factory normally inserts one row for each side so the helper can prove both\n * isolation directions. It may use fewer handles when the table is user-scoped\n * or when an existing seeded workspace/user row is itself the fixture.\n */\nexport interface ProofFixtureContext {\n orgA: SeedWorkspace;\n orgB: SeedWorkspace;\n userA: SeedUser;\n userB: SeedUser;\n sb: SupabaseClient;\n}\n\n/**\n * Explicit, table-owned setup contract for a Proof fixture.\n *\n * Keep factories under `e2e/fixtures/<table>.ts`. The table name is repeated at\n * runtime so a copied factory cannot silently seed one table while a proof\n * probes another.\n */\nexport interface ProofFixtureFactory<TTable extends string = string> {\n readonly table: TTable;\n create(context: ProofFixtureContext): Promise<void>;\n}\n\nexport interface DefineProofFixtureOptions<TTable extends string> {\n table: TTable;\n create(context: ProofFixtureContext): Promise<void>;\n}\n\n/** Define a completed, explicit fixture factory. */\nexport function defineProofFixture<const TTable extends string>(\n options: DefineProofFixtureOptions<TTable>,\n): ProofFixtureFactory<TTable> {\n return Object.freeze({\n table: options.table,\n create: options.create,\n });\n}\n\n/**\n * Error used when a proof exists before its table's valid fixture shape is\n * known. `assert.tenantIsolation` recognizes it and records an incomplete\n * assertion rather than allowing the missing setup to look green.\n */\nexport class ProofFixturePendingError extends Error {\n readonly code = \"fixture_factory_required\";\n\n constructor(\n readonly table: string,\n readonly reason: string,\n ) {\n super(\n `Fixture factory for \"${table}\" is incomplete: ${reason}. Complete e2e/fixtures/${table}.ts with values valid for the final schema.`,\n );\n this.name = \"ProofFixturePendingError\";\n }\n}\n\n/**\n * Declare the intentional pre-schema state of a fixture.\n *\n * The builder replaces this with `defineProofFixture(...)` after migrations\n * settle. Until then the proof remains explicitly incomplete.\n */\nexport function pendingProofFixture<const TTable extends string>(\n table: TTable,\n reason = \"the table schema and product constraints are not final\",\n): ProofFixtureFactory<TTable> {\n return defineProofFixture({\n table,\n async create() {\n throw new ProofFixturePendingError(table, reason);\n },\n });\n}\n\nexport function isProofFixturePendingError(\n error: unknown,\n): error is ProofFixturePendingError {\n return error instanceof ProofFixturePendingError;\n}\n"],"mappings":"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","names":["sleep"]}
@@ -0,0 +1,169 @@
1
+ import { A as AppliedBy, M as ModuleKind } from './trace-types-DkIzq-l9.js';
2
+ export { b as APPLIED_BY, c as ASSERTION_ROLE_ALIASES, d as AssertionRole, e as AssertionStatus, f as MODULE_KINDS, P as PROOF_KINDS, g as PROOF_VERBS, h as ProofKind, i as ProofOperation, j as ProofOptions, k as ProofVerb, R as RLS_CLASSIFICATIONS, l as RlsClassification, S as StepOptions, m as StepResult, n as TRACE_ARTIFACT_SCHEMA_VERSION, T as TraceArtifact, a as TraceAssertion, o as TraceMutation, p as TraceRecorder, q as TraceStep, r as isAppliedBy, s as isModuleKind, t as isProofKind, u as isProofVerb, v as isRlsClassification, w as normalizeAssertionRole, x as normalizeAssertionStatus } from './trace-types-DkIzq-l9.js';
3
+ export { A as ActionResult, I as InvokeActionRequest } from './action-types-C3OmHRyH.js';
4
+ export { C as CapabilityRequirement, M as MISSION_MANIFEST_SCHEMA_VERSION, a as MissionManifest, b as MissionRequirements, S as SchemaRequirement, T as TraceRequirement, c as ValidationCategory, d as ValidationIssue, V as ValidationResult } from './mission-types-Dr5rBHYO.js';
5
+ export { TENANT_ISOLATION_ACTION_HELPERS, TENANT_ISOLATION_TABLE_HELPERS } from './portable-vocabulary.js';
6
+
7
+ declare const MODULE_DESCRIPTOR_SCHEMA_VERSION = 1;
8
+ /**
9
+ * Where a buyer's answer has to land, so the code agent that applies it does
10
+ * not have to rediscover the seams — and so a rename breaks CI instead of
11
+ * quietly misleading the next agent.
12
+ *
13
+ * Two forms, both checkable:
14
+ *
15
+ * "app/__core/legal-notices/src/pages/imprint.tsx" — the file must exist
16
+ * "app/__core/user-auth/src/config.ts#MFA_POLICY" — ...and contain that identifier
17
+ *
18
+ * Seams routinely sit **outside** the declaring module. Answering "we do not
19
+ * need an Impressum" touches the page, its route, the shared footer and
20
+ * `llm.txt`; an agent that only knows about `legal-notices` deletes one of the
21
+ * four and leaves three links pointing at a 404. That is the whole reason this
22
+ * field exists.
23
+ */
24
+ type ModuleSeam = string;
25
+ /**
26
+ * Something the buyer chooses between before this module can be used.
27
+ *
28
+ * Declared by the module rather than known by the planner, so a planner's
29
+ * context stays flat as the library grows from one extension to thirty.
30
+ *
31
+ * **A question the buyer cannot answer does not belong here.** Anything needing
32
+ * technical judgement is a `ModuleDefault` and we own it; asking a dentist
33
+ * about webhook retry semantics is not consultation, it is abdication.
34
+ */
35
+ interface ModuleDecision {
36
+ /** snake_case, unique across every module. A planner keys answers by it. */
37
+ id: string;
38
+ /** In the buyer's language. They must be able to answer it unaided. */
39
+ ask: string;
40
+ /** Why it is being asked, in terms of what changes for them. */
41
+ because: string;
42
+ options: {
43
+ value: string;
44
+ /** What choosing this gets them, again in their language. */
45
+ means: string;
46
+ }[];
47
+ /** How the answer reaches the code. The planner's cost signal. */
48
+ applied_by: AppliedBy;
49
+ /** Every place the answer lands. Checked to still exist. */
50
+ applies: ModuleSeam[];
51
+ }
52
+ /**
53
+ * A value the buyer supplies rather than picks — a legal name, a domain, a
54
+ * support address.
55
+ *
56
+ * Separate from `decisions` because these are not choices and forcing them into
57
+ * one would mean options that are not options. Separate from `defaults` because
58
+ * nothing can be built until they exist: this is the planner's answer to "what
59
+ * must I collect before I dispatch anything?".
60
+ */
61
+ interface ModuleInput {
62
+ /** snake_case, unique across every module. */
63
+ id: string;
64
+ /** In the buyer's language. */
65
+ ask: string;
66
+ /** What breaks or looks wrong if it is left as shipped. */
67
+ because: string;
68
+ /** A concrete illustration, so nobody has to guess the format. */
69
+ example: string;
70
+ /** False when the template's placeholder is survivable for a while. */
71
+ required: boolean;
72
+ applies: ModuleSeam[];
73
+ }
74
+ /**
75
+ * Something we decided on the buyer's behalf.
76
+ *
77
+ * Recorded rather than asked, so a planner can answer "what else could change?"
78
+ * without putting a question in front of someone who has no opinion on invite
79
+ * expiry windows. The code agent gets the seam either way.
80
+ */
81
+ interface ModuleDefault {
82
+ /** snake_case, unique across every module. */
83
+ id: string;
84
+ /** What the template does today, in one plain sentence. */
85
+ is: string;
86
+ /** Where it is set. Checked to still exist. */
87
+ where: ModuleSeam;
88
+ }
89
+ /**
90
+ * The declared half — everything a person writes. The derived half is added by
91
+ * `proof-harness modules`.
92
+ */
93
+ interface ModuleMeta {
94
+ /** Directory name. Checked against where the file sits. */
95
+ id: string;
96
+ kind: ModuleKind;
97
+ /**
98
+ * What this is for, in a sentence a buyer would recognise.
99
+ *
100
+ * The field a planner leans on hardest and the only one nothing can verify —
101
+ * so it is short, and reviewed whenever the module changes.
102
+ */
103
+ purpose: string;
104
+ /**
105
+ * Environment variables this module needs.
106
+ *
107
+ * Declared rather than derived because need is transitive: the notification
108
+ * system reaches Resend through `@shared/email`, so no grep of its own source
109
+ * finds `RESEND_API_KEY`. Checked against `.env.local.example`, and anything
110
+ * the module reads directly must appear here.
111
+ */
112
+ env: string[];
113
+ decisions: ModuleDecision[];
114
+ inputs: ModuleInput[];
115
+ defaults: ModuleDefault[];
116
+ }
117
+ /**
118
+ * A descriptor plus everything the scanner read from the source.
119
+ *
120
+ * This is what lands in `.proof/modules.json` and what a planner actually
121
+ * reads. Nothing below `purpose` is hand-written.
122
+ */
123
+ interface ScannedModule extends ModuleMeta {
124
+ /** Repo-relative, `/` separators. */
125
+ path: string;
126
+ /** Tables this module's own migrations create. */
127
+ entities: string[];
128
+ /** Core / extension / business modules it imports. Never declared. */
129
+ requires: string[];
130
+ /** Shared utilities it imports. Not installable, so not a dependency to order. */
131
+ uses_shared: string[];
132
+ /** From `capabilities.json`: what this module can be asked to do. */
133
+ actions: {
134
+ name: string;
135
+ verb: string | null;
136
+ object: string | null;
137
+ }[];
138
+ /**
139
+ * How battle-tested it is. Derived, and eventually the reason an installed
140
+ * module costs a buyer less than a built one: somebody has already shown
141
+ * these checks can fail.
142
+ */
143
+ proven: {
144
+ specs: number;
145
+ mutations: number;
146
+ };
147
+ }
148
+ /**
149
+ * The generated artifact. Deterministic and unordered-by-nothing: every list is
150
+ * sorted and there is no timestamp, so two runs on one tree are byte-identical.
151
+ */
152
+ interface ModulesArtifact {
153
+ schemaVersion: number;
154
+ modules: ScannedModule[];
155
+ /**
156
+ * Tables no descriptor claims — today the ones owned by `__shared` modules,
157
+ * which are utilities nobody installs and so carry no descriptor.
158
+ *
159
+ * Listed rather than omitted. A planner that cannot see `audit_logs` will
160
+ * eventually promise to build it.
161
+ */
162
+ unowned_tables: {
163
+ table: string;
164
+ module: string;
165
+ path: string;
166
+ }[];
167
+ }
168
+
169
+ export { AppliedBy, MODULE_DESCRIPTOR_SCHEMA_VERSION, type ModuleDecision, type ModuleDefault, type ModuleInput, ModuleKind, type ModuleMeta, type ModuleSeam, type ModulesArtifact, type ScannedModule };