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
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# proof-harness
|
|
2
|
+
|
|
3
|
+
Prerelease verification and conformance toolkit for proof traces, missions,
|
|
4
|
+
fixtures, and Playwright probes.
|
|
5
|
+
|
|
6
|
+
This repository is the standalone source of the package. It requires Node 24
|
|
7
|
+
and pnpm 11.9.0:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm install --frozen-lockfile
|
|
11
|
+
pnpm check
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Public prereleases are published under the npm `next` tag. Pin an exact
|
|
17
|
+
prerelease version while the API is stabilizing:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm add proof-harness@0.1.0-next.1
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The package exposes environment-specific entry points:
|
|
24
|
+
|
|
25
|
+
- `proof-harness/shared` — portable types and vocabularies
|
|
26
|
+
- `proof-harness/node` — Node mission validation helpers
|
|
27
|
+
- `proof-harness/playwright` — Playwright proof assertions and traces
|
|
28
|
+
- `proof-harness/server` — server-side fixtures, guards, and service clients
|
|
29
|
+
- `proof-harness/portable-vocabulary` — dependency-free runtime constants
|
|
30
|
+
|
|
31
|
+
See [PROOF_SDK_CONTRACT.md](./PROOF_SDK_CONTRACT.md) for the consumer contract
|
|
32
|
+
and [COMPATIBILITY.md](./COMPATIBILITY.md) for protocol/schema compatibility
|
|
33
|
+
and consumer pinning requirements.
|
|
34
|
+
|
|
35
|
+
The package also provides one `proof-harness` executable with `scan`, `parse`,
|
|
36
|
+
`registry`, `build`, `verify`, `coverage`, `inventory`, `drift`, `modules`,
|
|
37
|
+
`modules-check`, and `mutate` subcommands. Commands read repository paths from
|
|
38
|
+
`proof.config.mjs`; without one they use a conventional application layout.
|
|
39
|
+
Product migration aggregation and explicit mutation definitions remain
|
|
40
|
+
repository-owned trust inputs rather than package defaults.
|
|
41
|
+
|
|
42
|
+
Named seed actors (`"admin"` and `"member"`) read
|
|
43
|
+
`SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD` and
|
|
44
|
+
`SEED_MEMBER_EMAIL`/`SEED_MEMBER_PASSWORD` from the consumer environment. The
|
|
45
|
+
package does not ship product seed credentials.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
Copyright 2026 The Nexus Collective GmbH.
|
|
50
|
+
|
|
51
|
+
Created and maintained by Till Kahlen at
|
|
52
|
+
[The Nexus Collective GmbH](https://nexuscollective.io).
|
|
53
|
+
|
|
54
|
+
Licensed under the [Apache License 2.0](./LICENSE).
|
|
55
|
+
|
|
56
|
+
## Release status
|
|
57
|
+
|
|
58
|
+
The package is released from this public repository with npm provenance.
|
|
59
|
+
Prerelease consumers must pin an exact version rather than using `next`, `^`, or
|
|
60
|
+
`~`; see [COMPATIBILITY.md](./COMPATIBILITY.md) for the compatibility policy.
|
package/cli/config.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
6
|
+
artifacts: {
|
|
7
|
+
capabilities: ".proof/capabilities.json",
|
|
8
|
+
schema: ".proof/schema.json",
|
|
9
|
+
modules: ".proof/modules.json",
|
|
10
|
+
traces: ".proof/traces",
|
|
11
|
+
drift: ".proof/drift.json",
|
|
12
|
+
mutations: ".proof/mutations",
|
|
13
|
+
},
|
|
14
|
+
mission: {
|
|
15
|
+
current: ".proof/current-mission.json",
|
|
16
|
+
directory: ".proof/missions",
|
|
17
|
+
},
|
|
18
|
+
policies: {
|
|
19
|
+
coverage: ".proof/coverage-policy.json",
|
|
20
|
+
mutation: ".proof/mutation-policy.json",
|
|
21
|
+
migration: ".proof/migration-policy.json",
|
|
22
|
+
},
|
|
23
|
+
roots: {
|
|
24
|
+
source: "app",
|
|
25
|
+
actions: ["app/__core", "app/__business-logic", "app/__extensions"],
|
|
26
|
+
modules: ["app/__core", "app/__extensions", "app/__business-logic"],
|
|
27
|
+
sharedModules: "app/__shared",
|
|
28
|
+
proofs: "e2e/proofs",
|
|
29
|
+
fixtures: "e2e/fixtures",
|
|
30
|
+
migrations: "supabase/migrations",
|
|
31
|
+
},
|
|
32
|
+
registryOutput: "app/__shared/proof-runtime/action-registry.generated.ts",
|
|
33
|
+
registryAliases: {
|
|
34
|
+
"app/__core/": "@core/",
|
|
35
|
+
"app/__business-logic/": "@business/",
|
|
36
|
+
"app/__extensions/": "@extensions/",
|
|
37
|
+
},
|
|
38
|
+
repository: {
|
|
39
|
+
packageJson: "package.json",
|
|
40
|
+
lockfile: "pnpm-lock.yaml",
|
|
41
|
+
envExample: ".env.local.example",
|
|
42
|
+
supabaseConfig: "supabase/config.toml",
|
|
43
|
+
},
|
|
44
|
+
moduleKinds: {
|
|
45
|
+
required: [
|
|
46
|
+
{ root: "app/__core", kind: "core" },
|
|
47
|
+
{ root: "app/__extensions", kind: "extension" },
|
|
48
|
+
],
|
|
49
|
+
optional: [{ root: "app/__business-logic", kind: "business" }],
|
|
50
|
+
},
|
|
51
|
+
driftSources: ["app", "package.json"],
|
|
52
|
+
mutationCatalog: null,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function mergeConfig(config = {}) {
|
|
56
|
+
return {
|
|
57
|
+
...DEFAULT_CONFIG,
|
|
58
|
+
...config,
|
|
59
|
+
artifacts: { ...DEFAULT_CONFIG.artifacts, ...config.artifacts },
|
|
60
|
+
mission: { ...DEFAULT_CONFIG.mission, ...config.mission },
|
|
61
|
+
policies: { ...DEFAULT_CONFIG.policies, ...config.policies },
|
|
62
|
+
roots: { ...DEFAULT_CONFIG.roots, ...config.roots },
|
|
63
|
+
repository: { ...DEFAULT_CONFIG.repository, ...config.repository },
|
|
64
|
+
registryAliases: {
|
|
65
|
+
...DEFAULT_CONFIG.registryAliases,
|
|
66
|
+
...config.registryAliases,
|
|
67
|
+
},
|
|
68
|
+
moduleKinds: {
|
|
69
|
+
...DEFAULT_CONFIG.moduleKinds,
|
|
70
|
+
...config.moduleKinds,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadProofConfig({
|
|
76
|
+
cwd = process.cwd(),
|
|
77
|
+
configPath = process.env.PROOF_HARNESS_CONFIG,
|
|
78
|
+
} = {}) {
|
|
79
|
+
const requested = configPath
|
|
80
|
+
? path.resolve(cwd, configPath)
|
|
81
|
+
: path.resolve(cwd, "proof.config.mjs");
|
|
82
|
+
let supplied = {};
|
|
83
|
+
if (fs.existsSync(requested)) {
|
|
84
|
+
const loaded = await import(pathToFileURL(requested).href);
|
|
85
|
+
supplied = loaded.default ?? loaded.proofConfig ?? {};
|
|
86
|
+
} else if (configPath) {
|
|
87
|
+
throw new Error(`proof config not found: ${requested}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rootDir = path.resolve(
|
|
91
|
+
path.dirname(
|
|
92
|
+
fs.existsSync(requested)
|
|
93
|
+
? requested
|
|
94
|
+
: path.resolve(cwd, "proof.config.mjs"),
|
|
95
|
+
),
|
|
96
|
+
supplied.rootDir ?? ".",
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
...mergeConfig(supplied),
|
|
100
|
+
rootDir,
|
|
101
|
+
configPath: fs.existsSync(requested) ? requested : null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function loadMutationCatalog(config) {
|
|
106
|
+
if (!config.mutationCatalog) return [];
|
|
107
|
+
const file = path.resolve(config.rootDir, config.mutationCatalog);
|
|
108
|
+
const loaded = await import(pathToFileURL(file).href);
|
|
109
|
+
const mutations = loaded.mutations ?? loaded.default;
|
|
110
|
+
if (!Array.isArray(mutations)) {
|
|
111
|
+
throw new Error(`${config.mutationCatalog} must export a mutations array`);
|
|
112
|
+
}
|
|
113
|
+
return mutations;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { DEFAULT_CONFIG };
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generate the dev/test-only server-action allowlist from current proof intent.
|
|
3
|
+
//
|
|
4
|
+
// Registration sources:
|
|
5
|
+
// 1. withProof invariants declared in capabilities.json
|
|
6
|
+
// 2. derived workspace-scoped service-role mutation requirements
|
|
7
|
+
// 3. literal action probes in e2e/proofs/*.proof.ts
|
|
8
|
+
//
|
|
9
|
+
// The generated TypeScript file is committed. Fresh checkout build/typecheck
|
|
10
|
+
// jobs do not run proof:build, so gitignoring source required by the invoke
|
|
11
|
+
// route would make the repository uncompilable before setup.
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
import { loadProofConfig } from "../config.mjs";
|
|
18
|
+
|
|
19
|
+
const CONFIG = await loadProofConfig();
|
|
20
|
+
process.chdir(CONFIG.rootDir);
|
|
21
|
+
|
|
22
|
+
const CAPABILITIES_PATH = CONFIG.artifacts.capabilities;
|
|
23
|
+
const PROOFS_DIR = CONFIG.roots.proofs;
|
|
24
|
+
const OUTPUT_PATH = CONFIG.registryOutput;
|
|
25
|
+
|
|
26
|
+
function readJson(file) {
|
|
27
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stripComments(source) {
|
|
31
|
+
return source
|
|
32
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
33
|
+
.replace(/^\s*\/\/.*$/gm, "")
|
|
34
|
+
.replace(/[ \t]+\/\/.*$/gm, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function proofFiles(dir) {
|
|
38
|
+
if (!fs.existsSync(dir)) return [];
|
|
39
|
+
const files = [];
|
|
40
|
+
const walk = (current) => {
|
|
41
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
42
|
+
const full = path.join(current, entry.name);
|
|
43
|
+
if (entry.isDirectory()) walk(full);
|
|
44
|
+
else if (entry.isFile() && entry.name.endsWith(".proof.ts"))
|
|
45
|
+
files.push(full);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
walk(dir);
|
|
49
|
+
return files.sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function literal(body, property) {
|
|
53
|
+
return body.match(new RegExp(`\\b${property}\\s*:\\s*["']([^"']+)["']`))?.[1];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function scanProofActionRefs(source) {
|
|
57
|
+
const clean = stripComments(source);
|
|
58
|
+
const refs = new Set();
|
|
59
|
+
|
|
60
|
+
for (const match of clean.matchAll(/\baction\s*:\s*\{([\s\S]*?)\}/g)) {
|
|
61
|
+
const moduleName = literal(match[1], "module");
|
|
62
|
+
const actionName = literal(match[1], "name");
|
|
63
|
+
if (moduleName && actionName) refs.add(`${moduleName}:${actionName}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const match of clean.matchAll(
|
|
67
|
+
/\binvokeAction\s*\([\s\S]*?,\s*\{([\s\S]*?)\}\s*\)/g,
|
|
68
|
+
)) {
|
|
69
|
+
const moduleName = literal(match[1], "module");
|
|
70
|
+
const actionName = literal(match[1], "action");
|
|
71
|
+
if (moduleName && actionName) refs.add(`${moduleName}:${actionName}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [...refs].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function importPath(file) {
|
|
78
|
+
const normalized = file.split(path.sep).join("/").replace(/\.ts$/, "");
|
|
79
|
+
const mappings = Object.entries(CONFIG.registryAliases);
|
|
80
|
+
for (const [prefix, alias] of mappings) {
|
|
81
|
+
if (normalized.startsWith(prefix)) {
|
|
82
|
+
return `${alias}${normalized.slice(prefix.length)}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw new Error(
|
|
86
|
+
`action file "${file}" is outside the supported module roots`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function localName(ref) {
|
|
91
|
+
return `proofAction_${ref.replace(/[^A-Za-z0-9_$]/g, "_")}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {{
|
|
96
|
+
* capabilities: { capabilities?: Array<{
|
|
97
|
+
* name: string;
|
|
98
|
+
* module: string;
|
|
99
|
+
* exportName?: string | null;
|
|
100
|
+
* invariants?: string[];
|
|
101
|
+
* acceptsWorkspaceId?: boolean;
|
|
102
|
+
* internalOnly?: boolean;
|
|
103
|
+
* serviceRoleMutations?: Array<{ table: string; operation: string }>;
|
|
104
|
+
* file: string;
|
|
105
|
+
* }> };
|
|
106
|
+
* proofSources?: Array<{ file: string; source: string }>;
|
|
107
|
+
* }} input
|
|
108
|
+
*/
|
|
109
|
+
export function buildRegistry({ capabilities, proofSources = [] }) {
|
|
110
|
+
const byRef = new Map(
|
|
111
|
+
(capabilities.capabilities ?? []).map((capability) => [
|
|
112
|
+
`${capability.module}:${capability.name}`,
|
|
113
|
+
capability,
|
|
114
|
+
]),
|
|
115
|
+
);
|
|
116
|
+
const registrations = new Map();
|
|
117
|
+
|
|
118
|
+
const add = (ref, source) => {
|
|
119
|
+
if (!registrations.has(ref)) registrations.set(ref, new Set());
|
|
120
|
+
registrations.get(ref).add(source);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const capability of capabilities.capabilities ?? []) {
|
|
124
|
+
const ref = `${capability.module}:${capability.name}`;
|
|
125
|
+
if ((capability.invariants ?? []).length > 0) add(ref, "explicit_claim");
|
|
126
|
+
if (
|
|
127
|
+
capability.acceptsWorkspaceId === true &&
|
|
128
|
+
capability.internalOnly !== true &&
|
|
129
|
+
(capability.serviceRoleMutations ?? []).length > 0
|
|
130
|
+
) {
|
|
131
|
+
add(ref, "derived_service_role");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const { file, source } of proofSources) {
|
|
136
|
+
for (const ref of scanProofActionRefs(source)) add(ref, `proof:${file}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const entries = [];
|
|
140
|
+
const problems = [];
|
|
141
|
+
for (const [ref, sources] of [...registrations.entries()].sort(([a], [b]) =>
|
|
142
|
+
a.localeCompare(b),
|
|
143
|
+
)) {
|
|
144
|
+
const capability = byRef.get(ref);
|
|
145
|
+
if (!capability) {
|
|
146
|
+
problems.push(
|
|
147
|
+
`${ref} is referenced by ${[...sources].sort().join(", ")} but no scanned createAction capability has that ref`,
|
|
148
|
+
);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!capability.exportName) {
|
|
152
|
+
problems.push(
|
|
153
|
+
`${ref} is selected for proof invocation but ${capability.file} has no literal exported function`,
|
|
154
|
+
);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (capability.usesDirectUpdateTag === true) {
|
|
158
|
+
problems.push(
|
|
159
|
+
`${ref} imports updateTag directly from next/cache; registry actions must use updateTagSafe so Route Handler invocation preserves production semantics without throwing in proof mode`,
|
|
160
|
+
);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
entries.push({
|
|
165
|
+
ref,
|
|
166
|
+
exportName: capability.exportName,
|
|
167
|
+
importPath: importPath(capability.file),
|
|
168
|
+
localName: localName(ref),
|
|
169
|
+
sources: [...sources].sort(),
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
problems.push(
|
|
173
|
+
`${ref} cannot be registered: ${error instanceof Error ? error.message : String(error)}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { entries, problems };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function renderRegistry(entries) {
|
|
182
|
+
const imports = entries.map(
|
|
183
|
+
(entry) =>
|
|
184
|
+
`import { ${entry.exportName} as ${entry.localName} } from "${entry.importPath}";`,
|
|
185
|
+
);
|
|
186
|
+
const rows = entries.flatMap((entry) => [
|
|
187
|
+
` "${entry.ref}":`,
|
|
188
|
+
` ${entry.localName} as unknown as ProofActionHandler,`,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
return [
|
|
192
|
+
"// AUTO-GENERATED by scripts/generate_proof_action_registry.mjs.",
|
|
193
|
+
"// Do not edit by hand; run `pnpm proof:registry`.",
|
|
194
|
+
"",
|
|
195
|
+
...imports,
|
|
196
|
+
...(imports.length > 0 ? [""] : []),
|
|
197
|
+
'import type { ProofActionHandler } from "proof-harness/server";',
|
|
198
|
+
"",
|
|
199
|
+
"export const PROOF_ACTION_REGISTRY: Record<string, ProofActionHandler> = {",
|
|
200
|
+
...rows,
|
|
201
|
+
"};",
|
|
202
|
+
"",
|
|
203
|
+
].join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function generateRegistry({
|
|
207
|
+
rootDir = process.cwd(),
|
|
208
|
+
check = false,
|
|
209
|
+
} = {}) {
|
|
210
|
+
const capabilities = readJson(path.join(rootDir, CAPABILITIES_PATH));
|
|
211
|
+
const sources = proofFiles(path.join(rootDir, PROOFS_DIR)).map((file) => ({
|
|
212
|
+
file: path.relative(rootDir, file).split(path.sep).join("/"),
|
|
213
|
+
source: fs.readFileSync(file, "utf8"),
|
|
214
|
+
}));
|
|
215
|
+
const { entries, problems } = buildRegistry({
|
|
216
|
+
capabilities,
|
|
217
|
+
proofSources: sources,
|
|
218
|
+
});
|
|
219
|
+
if (problems.length > 0) {
|
|
220
|
+
for (const problem of problems) {
|
|
221
|
+
console.error(`[proof:registry] ${problem}`);
|
|
222
|
+
}
|
|
223
|
+
throw new Error(
|
|
224
|
+
`[PROOF_FAIL] action_registry: ${problems.length} registration problem(s)`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const output = renderRegistry(entries);
|
|
229
|
+
const outputFile = path.join(rootDir, OUTPUT_PATH);
|
|
230
|
+
const current = fs.existsSync(outputFile)
|
|
231
|
+
? fs.readFileSync(outputFile, "utf8")
|
|
232
|
+
: null;
|
|
233
|
+
|
|
234
|
+
if (check) {
|
|
235
|
+
if (current !== output) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`[PROOF_FAIL] action_registry_stale: ${OUTPUT_PATH} does not match current actions and proofs\n` +
|
|
238
|
+
" suggestion: run `pnpm proof:registry` and commit the generated file",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
243
|
+
fs.writeFileSync(outputFile, output, "utf8");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(
|
|
247
|
+
`[proof:registry] ${check ? "verified" : "wrote"} ${entries.length} action(s) in ${OUTPUT_PATH}`,
|
|
248
|
+
);
|
|
249
|
+
return entries;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseArgs(argv) {
|
|
253
|
+
const unknown = argv.filter((arg) => arg !== "--check");
|
|
254
|
+
if (unknown.length > 0) {
|
|
255
|
+
throw new Error(`unknown flag(s): ${unknown.join(", ")}`);
|
|
256
|
+
}
|
|
257
|
+
return { check: argv.includes("--check") };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const invokedDirectly =
|
|
261
|
+
process.argv[1] &&
|
|
262
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
263
|
+
|
|
264
|
+
if (invokedDirectly) {
|
|
265
|
+
try {
|
|
266
|
+
generateRegistry(parseArgs(process.argv.slice(2)));
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|