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
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ // ---------------------------------------------------------------------------
3
+ // scan_module_meta.mjs
4
+ //
5
+ // v1 module descriptor scanner.
6
+ //
7
+ // Merges what each module DECLARES about itself (`module.meta.ts`) with what
8
+ // its source SAYS, and writes `.proof/modules.json` — the file a planner reads
9
+ // instead of the codebase.
10
+ //
11
+ // Reads:
12
+ // - app/__core/<module>/module.meta.ts
13
+ // - app/__extensions/<module>/module.meta.ts
14
+ // - app/__business-logic/<module>/module.meta.ts
15
+ // - that module's src/db/migrations/*.sql (which tables it owns)
16
+ // - that module's src/**/*.ts{,x} (which modules it imports)
17
+ // - .proof/capabilities.json (what it can be asked to do)
18
+ //
19
+ // Writes `.proof/modules.json`.
20
+ //
21
+ // The declared/derived split is the whole design. Purpose, decisions, inputs
22
+ // and defaults are declared, because no compiler knows them. Tables,
23
+ // dependencies, actions and file locations are derived, because a written copy
24
+ // of a derivable fact is a fact that will be wrong later and say nothing about
25
+ // it. The downstream prototype this replaces declared its dependencies and got
26
+ // three of five wrong on the first draft; every one of them read as obviously
27
+ // true.
28
+ //
29
+ // Descriptors are read with a plain `await import()`. Node 24 strips types
30
+ // natively, and because a descriptor imports its types with `import type` the
31
+ // alias import is erased before resolution — so this needs no TypeScript
32
+ // loader, no `npx`, and no network. Keeping it dependency-free is deliberate:
33
+ // a project planner has to be able to run `pnpm proof:modules` and get an
34
+ // answer in about a second, without Docker, a database, or a migration
35
+ // aggregate.
36
+ //
37
+ // Deliberately does no verification. `proof_module_check.mjs` checks the
38
+ // declared half against the code, and keeping the two apart means a broken
39
+ // claim fails the check rather than silently producing a smaller artifact.
40
+ //
41
+ // Output is deterministic (sorted, no timestamps) so two runs on one tree are
42
+ // byte-identical.
43
+ //
44
+ // Usage: node scripts/scan_module_meta.mjs
45
+ // ---------------------------------------------------------------------------
46
+
47
+ import fs from "node:fs";
48
+ import path from "node:path";
49
+ import { pathToFileURL } from "node:url";
50
+
51
+ import { tableNamesIn } from "./parse_proof_schema.mjs";
52
+ import { loadProofConfig } from "../config.mjs";
53
+
54
+ const CONFIG = await loadProofConfig();
55
+ process.chdir(CONFIG.rootDir);
56
+
57
+ // Module packages have no `"type"` field, so importing a `.ts` file out of one
58
+ // makes Node warn that it had to reparse as ESM. Adding `"type": "module"` to
59
+ // thirteen package.json files to silence it would change how the bundler reads
60
+ // them, which is a real risk traded for a cosmetic one. Filter the warning
61
+ // instead, and let every other warning through untouched.
62
+ process.removeAllListeners("warning");
63
+ process.on("warning", (warning) => {
64
+ if (warning.code === "MODULE_TYPELESS_PACKAGE_JSON") return;
65
+ console.warn(warning.stack ?? String(warning));
66
+ });
67
+
68
+ const DESCRIPTOR = "module.meta.ts";
69
+ const OUTPUT_PATH = CONFIG.artifacts.modules;
70
+ const CAPABILITIES_PATH = CONFIG.artifacts.capabilities;
71
+ const MUTATION_HARNESS = CONFIG.mutationCatalog;
72
+ const PROOFS_DIR = CONFIG.roots.proofs;
73
+
74
+ /** Roots whose children are modules a planner can reason about. */
75
+ const MODULE_ROOTS = CONFIG.roots.modules;
76
+
77
+ /** Utilities. They own tables but are never installed, so they carry no descriptor. */
78
+ const SHARED_ROOT = CONFIG.roots.sharedModules;
79
+
80
+ /**
81
+ * One module importing another, in any of the forms the codebase uses:
82
+ * `from "@core/x/…"`, `export … from "@shared/x/…"`, `import("@extensions/x/…")`.
83
+ * Matching the quoted specifier rather than the keyword catches all three.
84
+ */
85
+ const IMPORT_PATTERN =
86
+ /["']@(core|shared|extensions|business)\/([a-zA-Z0-9._-]+)/g;
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Discovery
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function normalize(value) {
93
+ return value.split(path.sep).join("/");
94
+ }
95
+
96
+ function directoriesIn(root) {
97
+ if (!fs.existsSync(root)) return [];
98
+ return fs
99
+ .readdirSync(root, { withFileTypes: true })
100
+ .filter((e) => e.isDirectory())
101
+ .map((e) => e.name)
102
+ .sort();
103
+ }
104
+
105
+ /** Every module directory, whether or not it has a descriptor yet. */
106
+ function allModuleDirs() {
107
+ const found = [];
108
+ for (const root of MODULE_ROOTS) {
109
+ for (const name of directoriesIn(root)) {
110
+ found.push({ id: name, dir: path.join(root, name), root });
111
+ }
112
+ }
113
+ return found;
114
+ }
115
+
116
+ function describedModules() {
117
+ return allModuleDirs().filter((m) =>
118
+ fs.existsSync(path.join(m.dir, DESCRIPTOR)),
119
+ );
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // The declared half
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /**
127
+ * Read one descriptor.
128
+ *
129
+ * A descriptor is TypeScript so a wrong shape is a compile error under
130
+ * `pnpm typecheck` rather than a runtime surprise. Reading it here needs no
131
+ * loader — see the header.
132
+ */
133
+ async function readDescriptor(file) {
134
+ const url = pathToFileURL(path.resolve(file)).href;
135
+ const mod = await import(url);
136
+ if (!mod.meta) {
137
+ throw new Error(`${normalize(file)} does not export \`meta\``);
138
+ }
139
+ return mod.meta;
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // The derived half
144
+ // ---------------------------------------------------------------------------
145
+
146
+ function sqlFilesIn(dir) {
147
+ const migrations = path.join(dir, "src", "db", "migrations");
148
+ if (!fs.existsSync(migrations)) return [];
149
+ return fs
150
+ .readdirSync(migrations)
151
+ .filter((f) => f.endsWith(".sql"))
152
+ .sort()
153
+ .map((f) => path.join(migrations, f));
154
+ }
155
+
156
+ /** Tables this module's own migrations create. */
157
+ function tablesOwnedBy(dir) {
158
+ const tables = new Set();
159
+ for (const file of sqlFilesIn(dir)) {
160
+ for (const name of tableNamesIn(fs.readFileSync(file, "utf8"))) {
161
+ tables.add(name);
162
+ }
163
+ }
164
+ return [...tables].sort();
165
+ }
166
+
167
+ function sourceFilesIn(dir) {
168
+ const out = [];
169
+ const walk = (d) => {
170
+ if (!fs.existsSync(d)) return;
171
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
172
+ const full = path.join(d, entry.name);
173
+ if (entry.isDirectory()) {
174
+ // Tests import freely to build fixtures. That is a dependency of
175
+ // testing the module, not a dependency of the module.
176
+ if (entry.name === "__tests" || entry.name === "node_modules") continue;
177
+ walk(full);
178
+ } else if (/\.tsx?$/.test(entry.name) && entry.name !== DESCRIPTOR) {
179
+ out.push(full);
180
+ }
181
+ }
182
+ };
183
+ walk(path.join(dir, "src"));
184
+ return out.sort();
185
+ }
186
+
187
+ /**
188
+ * Which modules this one imports, split by whether they are installable.
189
+ *
190
+ * `requires` is what a planner has to order a build around. `uses_shared` is
191
+ * reported separately because a shared utility is not something anyone
192
+ * installs, prices, or can be offered — putting it in `requires` would make
193
+ * every module look like it depends on twelve things.
194
+ */
195
+ function importsOf(dir, id, realModuleIds) {
196
+ const requires = new Set();
197
+ const shared = new Set();
198
+ for (const file of sourceFilesIn(dir)) {
199
+ const source = fs.readFileSync(file, "utf8");
200
+ IMPORT_PATTERN.lastIndex = 0;
201
+ let m;
202
+ while ((m = IMPORT_PATTERN.exec(source)) !== null) {
203
+ const [, alias, name] = m;
204
+ if (alias === "shared") {
205
+ shared.add(name);
206
+ continue;
207
+ }
208
+ if (name === id) continue;
209
+ // Only real directories. A typo'd alias is a build error, not a
210
+ // dependency, and inventing a node for it would corrupt the graph.
211
+ if (realModuleIds.has(name)) requires.add(name);
212
+ }
213
+ }
214
+ return {
215
+ requires: [...requires].sort(),
216
+ uses_shared: [...shared].sort(),
217
+ };
218
+ }
219
+
220
+ /** What this module can be asked to do, from the capability scan. */
221
+ function actionsFor(id) {
222
+ if (!fs.existsSync(CAPABILITIES_PATH)) return [];
223
+ const { capabilities = [] } = JSON.parse(
224
+ fs.readFileSync(CAPABILITIES_PATH, "utf8"),
225
+ );
226
+ return capabilities
227
+ .filter((c) => c.module === id)
228
+ .map((c) => ({ name: c.name, verb: c.verb, object: c.object }))
229
+ .sort((a, b) => a.name.localeCompare(b.name));
230
+ }
231
+
232
+ /**
233
+ * How battle-tested this module is.
234
+ *
235
+ * Derived rather than claimed, because "this module is well tested" is exactly
236
+ * the sentence nobody updates downwards.
237
+ *
238
+ * The mutation count is read out of the harness as text, which is the one piece
239
+ * of scraping left in this file. Importing `proof_mutation_check.mjs` for its
240
+ * `MUTATIONS` array would be more honest and would drag `@next/env` into a
241
+ * script that has to stay dependency-free (see the header). The scrape is
242
+ * anchored to the table map derived above, so it cannot drift silently in the
243
+ * way a hand-written count would — but it is the first thing to revisit if the
244
+ * harness ever grows a machine-readable inventory.
245
+ */
246
+ function provenFor(tables, actions) {
247
+ let specs = 0;
248
+ if (fs.existsSync(PROOFS_DIR)) {
249
+ const targets = [...tables, ...actions.map((a) => a.name)];
250
+ for (const file of fs.readdirSync(PROOFS_DIR).sort()) {
251
+ if (!file.endsWith(".proof.ts")) continue;
252
+ const source = fs.readFileSync(path.join(PROOFS_DIR, file), "utf8");
253
+ if (targets.some((t) => source.includes(t))) specs += 1;
254
+ }
255
+ }
256
+
257
+ let mutations = 0;
258
+ if (MUTATION_HARNESS && fs.existsSync(MUTATION_HARNESS)) {
259
+ const owned = new Set(tables);
260
+ const source = fs.readFileSync(MUTATION_HARNESS, "utf8");
261
+ for (const line of source.split("\n")) {
262
+ const m = line.match(/table:\s*"public\.([a-z_]+)"/);
263
+ if (m && owned.has(m[1])) mutations += 1;
264
+ }
265
+ }
266
+
267
+ return { specs, mutations };
268
+ }
269
+
270
+ /**
271
+ * Tables nobody's descriptor claims.
272
+ *
273
+ * Today these are the `__shared` modules' own tables — `audit_logs` and
274
+ * `rate_limit_counters`. Listed rather than omitted: a planner that cannot see
275
+ * `audit_logs` will eventually promise to build it.
276
+ */
277
+ function unownedTables(claimed) {
278
+ const rows = [];
279
+ for (const name of directoriesIn(SHARED_ROOT)) {
280
+ const dir = path.join(SHARED_ROOT, name);
281
+ for (const table of tablesOwnedBy(dir)) {
282
+ if (claimed.has(table)) continue;
283
+ rows.push({ table, module: name, path: normalize(dir) });
284
+ }
285
+ }
286
+ return rows.sort((a, b) => a.table.localeCompare(b.table));
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Main
291
+ // ---------------------------------------------------------------------------
292
+
293
+ async function main() {
294
+ const realModuleIds = new Set(allModuleDirs().map((m) => m.id));
295
+ const described = describedModules();
296
+ const scanned = [];
297
+
298
+ for (const { id, dir } of described) {
299
+ const declared = await readDescriptor(path.join(dir, DESCRIPTOR));
300
+ const entities = tablesOwnedBy(dir);
301
+ const actions = actionsFor(id);
302
+ const { requires, uses_shared } = importsOf(dir, id, realModuleIds);
303
+
304
+ scanned.push({
305
+ ...declared,
306
+ path: normalize(dir),
307
+ entities,
308
+ requires,
309
+ uses_shared,
310
+ actions,
311
+ proven: provenFor(entities, actions),
312
+ });
313
+ }
314
+
315
+ scanned.sort((a, b) => a.id.localeCompare(b.id));
316
+
317
+ const claimed = new Set(scanned.flatMap((m) => m.entities));
318
+ const payload = {
319
+ schemaVersion: 1,
320
+ modules: scanned,
321
+ unowned_tables: unownedTables(claimed),
322
+ };
323
+
324
+ fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });
325
+ fs.writeFileSync(
326
+ OUTPUT_PATH,
327
+ JSON.stringify(payload, null, 2) + "\n",
328
+ "utf8",
329
+ );
330
+
331
+ const undescribed = realModuleIds.size - described.length;
332
+ console.log(
333
+ `[proof:modules] scanned ${described.length} module descriptor(s), ` +
334
+ `${undescribed} module(s) without one, ` +
335
+ `${payload.unowned_tables.length} unowned table(s); wrote ${OUTPUT_PATH}`,
336
+ );
337
+ }
338
+
339
+ // Only run the CLI when executed directly, so importing this module for tests
340
+ // has no side effects.
341
+ const invokedDirectly =
342
+ process.argv[1] !== undefined &&
343
+ import.meta.url === pathToFileURL(process.argv[1]).href;
344
+
345
+ if (invokedDirectly) {
346
+ main().catch((err) => {
347
+ console.error(`[proof:modules] unexpected error: ${err?.stack ?? err}`);
348
+ process.exit(2);
349
+ });
350
+ }
351
+
352
+ export { main };