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.
- package/COMPATIBILITY.md +26 -0
- package/LICENSE +201 -0
- package/PROOF_SDK_CONTRACT.md +1255 -0
- package/README.md +60 -0
- package/cli/config.mjs +116 -0
- package/cli/engines/generate_proof_action_registry.mjs +271 -0
- package/cli/engines/parse_proof_schema.mjs +457 -0
- package/cli/engines/proof_coverage.mjs +871 -0
- package/cli/engines/proof_drift.mjs +980 -0
- package/cli/engines/proof_module_check.mjs +498 -0
- package/cli/engines/proof_mutation_check.mjs +1084 -0
- package/cli/engines/proof_mutation_inventory.mjs +508 -0
- package/cli/engines/proof_source_mutation.mjs +35 -0
- package/cli/engines/proof_verify.mjs +620 -0
- package/cli/engines/scan_module_meta.mjs +352 -0
- package/cli/engines/scan_proof_capabilities.mjs +359 -0
- package/cli/proof-harness.mjs +107 -0
- package/dist/action-types-C3OmHRyH.d.ts +26 -0
- package/dist/fixture-B-dmnG5G.d.ts +105 -0
- package/dist/mission-types-Dr5rBHYO.d.ts +299 -0
- package/dist/node.d.ts +79 -0
- package/dist/node.js +553 -0
- package/dist/node.js.map +1 -0
- package/dist/playwright.d.ts +433 -0
- package/dist/playwright.js +1824 -0
- package/dist/playwright.js.map +1 -0
- package/dist/portable-vocabulary.d.ts +86 -0
- package/dist/portable-vocabulary.js +43 -0
- package/dist/portable-vocabulary.js.map +1 -0
- package/dist/server.d.ts +55 -0
- package/dist/server.js +376 -0
- package/dist/server.js.map +1 -0
- package/dist/shared.d.ts +169 -0
- package/dist/shared.js +121 -0
- package/dist/shared.js.map +1 -0
- package/dist/trace-types-DkIzq-l9.d.ts +333 -0
- package/package.json +109 -0
- package/proof-consumer-fixtures/README.md +60 -0
- package/proof-consumer-fixtures/aggregates/M-CONFORMANCE-FAIL.json +112 -0
- package/proof-consumer-fixtures/aggregates/M-CONFORMANCE-PASS.json +101 -0
- package/proof-consumer-fixtures/baseline/failing/widgets-tenant-isolation.json +46 -0
- package/proof-consumer-fixtures/baseline/passing/widgets-tenant-isolation.json +45 -0
- package/proof-consumer-fixtures/corpus.json +102 -0
- package/proof-consumer-fixtures/duplicates/first/widgets-tenant-isolation.json +12 -0
- package/proof-consumer-fixtures/duplicates/second/widgets-tenant-isolation.json +12 -0
- package/proof-consumer-fixtures/inputs/capabilities.json +4 -0
- package/proof-consumer-fixtures/inputs/mission.json +20 -0
- package/proof-consumer-fixtures/inputs/schema.json +4 -0
- package/proof-consumer-fixtures/malformed/invalid-trace.json.invalid +4 -0
- package/proof-consumer-fixtures/malformed/wrong-manifest-version.json +11 -0
- package/proof-consumer-fixtures/malformed/wrong-trace-shape.json +8 -0
- package/proof-consumer-fixtures/mutations/MUT-WIDGETS-LEAK/mutation.json +17 -0
- 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 };
|