@volter/twin-standard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +20 -0
- package/dist/src/check-sources.d.ts +33 -0
- package/dist/src/check-sources.js +128 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +105 -0
- package/dist/src/derive.d.ts +2 -0
- package/dist/src/derive.js +349 -0
- package/dist/src/gate.d.ts +9 -0
- package/dist/src/gate.js +109 -0
- package/dist/src/grade.d.ts +30 -0
- package/dist/src/grade.js +36 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +11 -0
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +33 -0
- package/dist/src/p3-rules.d.ts +8 -0
- package/dist/src/p3-rules.js +43 -0
- package/dist/src/protocol-3.d.ts +5 -0
- package/dist/src/protocol-3.js +721 -0
- package/dist/src/published.d.ts +13 -0
- package/dist/src/published.js +36 -0
- package/dist/src/spec-documents.d.ts +12 -0
- package/dist/src/spec-documents.js +61 -0
- package/dist/src/spec-ir-client.d.ts +20 -0
- package/dist/src/spec-ir-client.js +77 -0
- package/dist/src/spec-ir-commands.d.ts +38 -0
- package/dist/src/spec-ir-commands.js +38 -0
- package/dist/src/spec-ir-discovery.d.ts +11 -0
- package/dist/src/spec-ir-discovery.js +106 -0
- package/dist/src/spec-ir-graphql.d.ts +25 -0
- package/dist/src/spec-ir-graphql.js +46 -0
- package/dist/src/spec-ir-lines.d.ts +19 -0
- package/dist/src/spec-ir-lines.js +76 -0
- package/dist/src/spec-ir-proto.d.ts +80 -0
- package/dist/src/spec-ir-proto.js +339 -0
- package/dist/src/spec-ir.d.ts +104 -0
- package/dist/src/spec-ir.js +691 -0
- package/dist/src/spec-patches.d.ts +8 -0
- package/dist/src/spec-patches.js +24 -0
- package/dist/src/spec.d.ts +5 -0
- package/dist/src/spec.js +7 -0
- package/dist/src/types.d.ts +75 -0
- package/dist/src/types.js +4 -0
- package/dist/src/unit.d.ts +5 -0
- package/dist/src/unit.js +14 -0
- package/package.json +71 -0
- package/src/check-sources.ts +109 -0
- package/src/cli.ts +75 -0
- package/src/derive.ts +316 -0
- package/src/gate.ts +95 -0
- package/src/grade.ts +47 -0
- package/src/index.ts +12 -0
- package/src/lanes.ts +29 -0
- package/src/p3-rules.ts +44 -0
- package/src/protocol-3.ts +617 -0
- package/src/published.ts +37 -0
- package/src/spec-documents.ts +58 -0
- package/src/spec-ir-client.ts +86 -0
- package/src/spec-ir-commands.ts +50 -0
- package/src/spec-ir-discovery.ts +104 -0
- package/src/spec-ir-graphql.ts +64 -0
- package/src/spec-ir-lines.ts +76 -0
- package/src/spec-ir-proto.ts +289 -0
- package/src/spec-ir.ts +689 -0
- package/src/spec-patches.ts +23 -0
- package/src/spec.ts +8 -0
- package/src/types.ts +53 -0
- package/src/unit.ts +15 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
// Protocol 3, the derived pack (docs/contributing/architecture.md, "Protocol 3: the derived pack", "Pack layout",
|
|
2
|
+
// "The report", "The grade"): its criteria, each recomputed from the tree with no network and no clock.
|
|
3
|
+
//
|
|
4
|
+
// Version 1 checks what the repository's tools can decide today. Criteria the grade section names that no tool
|
|
5
|
+
// decides yet (a credential stored as issued; hollow capabilities; the invariants row) are listed in PLANNED and
|
|
6
|
+
// counted in no grade until a version of this standard checks them.
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
12
|
+
import { gunzipSync } from 'node:zlib';
|
|
13
|
+
import ts from 'typescript';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { sharingLanesIn } from './lanes.ts';
|
|
16
|
+
import type { Criterion, GradeUnit, Standard } from './types.ts';
|
|
17
|
+
|
|
18
|
+
/** The lanes over the unit's vendor's state, when the unit is one of them ("Pack layout"), else none: a lane over its
|
|
19
|
+
* vendor's state takes the vendor's states, shared helpers, manifest and engine from the vendor's src/, and nothing of
|
|
20
|
+
* another lane. */
|
|
21
|
+
function vendorLanes(unit: GradeUnit): string[] {
|
|
22
|
+
if (!unit.name.includes('/')) return [];
|
|
23
|
+
const pkg = dirname(unit.dir);
|
|
24
|
+
const lanes = sharingLanesIn(pkg, basename(pkg));
|
|
25
|
+
return lanes.includes(basename(unit.dir)) ? lanes : [];
|
|
26
|
+
}
|
|
27
|
+
/** Whether `from` (relative to `depth` directories under the lane) names the vendor's `rest` of its src/. */
|
|
28
|
+
function vendorPath(unit: GradeUnit, from: string, depth: number, rest: RegExp): boolean {
|
|
29
|
+
if (!vendorLanes(unit).length) return false;
|
|
30
|
+
const m = new RegExp(`^${'\\.\\./'.repeat(depth)}src/(.+)$`).exec(from);
|
|
31
|
+
return m !== null && rest.test(m[1]!);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const pass = (reason: string) => ({ pass: true, reason });
|
|
35
|
+
const fail = (reason: string) => ({ pass: false, reason });
|
|
36
|
+
const sha256 = (bytes: Uint8Array | string) => createHash('sha256').update(bytes).digest('hex');
|
|
37
|
+
const readJson = (path: string): unknown => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return undefined; } };
|
|
38
|
+
|
|
39
|
+
/** What spec/ holds beside the vendored documents: provenance, corrections, the grouping, the examples index, recordings. */
|
|
40
|
+
const SPEC_METADATA = /^(SOURCE\.md|sources\.json|patches\.json|grouping\.json|doc-examples\.json|README\.md|[^/]*\.sha256|recordings\/.*)$/;
|
|
41
|
+
/** A tool in spec/ is not a vendored document (it belongs in scripts/): named by the layout criterion, never hashed. */
|
|
42
|
+
const SPEC_TOOL = /\.(py|sh|ts|js|mjs|cjs|rb)$/;
|
|
43
|
+
/** The vendored documents: every file under spec/ that is not its metadata. */
|
|
44
|
+
function specFiles(unit: GradeUnit): string[] {
|
|
45
|
+
const dir = join(unit.dir, 'spec');
|
|
46
|
+
return filesUnder(dir).filter((f) => !SPEC_METADATA.test(f) && !SPEC_TOOL.test(f)).map((f) => join(dir, f));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Every file under a directory git does not ignore, relative to it (the checkout's tree, never runtime state). */
|
|
50
|
+
function filesUnder(dir: string, base = dir): string[] {
|
|
51
|
+
if (!existsSync(dir)) return [];
|
|
52
|
+
// tracked and not-yet-added files alike (T0 runs before the commit); ignored runtime state (.volter) never
|
|
53
|
+
const run = spawnSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
54
|
+
const tracked = run.status === 0 ? [...new Set(String(run.stdout).split('\0').filter(Boolean))] : [];
|
|
55
|
+
return tracked.map((f) => relative(base, join(dir, f))).filter((f) => existsSync(join(base, f)));
|
|
56
|
+
}
|
|
57
|
+
/** Every file under a directory, tracked or not (a directory the grader itself wrote). */
|
|
58
|
+
function allFilesUnder(dir: string, base = dir): string[] {
|
|
59
|
+
if (!existsSync(dir)) return [];
|
|
60
|
+
const out: string[] = [];
|
|
61
|
+
for (const name of readdirSync(dir)) {
|
|
62
|
+
const path = join(dir, name);
|
|
63
|
+
if (statSync(path).isDirectory()) out.push(...allFilesUnder(path, base));
|
|
64
|
+
else out.push(relative(base, path));
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** The tracked entries directly inside a directory. */
|
|
69
|
+
function entriesOf(dir: string): string[] {
|
|
70
|
+
return [...new Set(filesUnder(dir).map((f) => f.split('/')[0]!))].sort();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** "Pack layout"'s closed list (architecture.md). A lane's package files and serve factory are its vendor's. */
|
|
74
|
+
const LAYOUT_DIRS = new Set(['generated', 'semantics', 'engine', 'screens']);
|
|
75
|
+
const PACKAGE_FILES = new Set(['README.md', 'package.json', 'LICENSE', 'CHANGELOG.md', 'tsconfig.json', 'tsconfig.build.json', 'gate.ts', 'census.json', 'vendor']);
|
|
76
|
+
const TEST_FILE = /\.(test|uitest)\.tsx?$|\.d\.ts$/;
|
|
77
|
+
const own = (unit: GradeUnit) => unit.name.split('/').at(-1)!;
|
|
78
|
+
/** The standard's own derivation (./derive.ts): every grader derives the same way, whatever repository it grades. */
|
|
79
|
+
const DERIVE = fileURLToPath(new URL(`./derive${import.meta.url.endsWith('.ts') ? '.ts' : '.js'}`, import.meta.url));
|
|
80
|
+
/** The npm package a unit is in: the pack, or a lane's vendor. */
|
|
81
|
+
const packageOf = (unit: GradeUnit): string => (unit.name.includes('/') ? dirname(unit.dir) : unit.dir);
|
|
82
|
+
const isLane = (unit: GradeUnit) => unit.name.includes('/');
|
|
83
|
+
/** The fixed names at src/'s root ("Pack layout"); a lane's serve factory and bin are its vendor's. */
|
|
84
|
+
function srcFileInForm(unit: GradeUnit, name: string): boolean {
|
|
85
|
+
if (name.endsWith('.d.ts')) return true; // the types of a vendor SDK the pack's tests drive
|
|
86
|
+
return (isLane(unit) ? ['manifest.ts', 'index.ts', 'fetch.ts', 'budget.ts'] : ['manifest.ts', 'index.ts', 'cli.ts', 'fetch.ts', 'server.ts', 'budget.ts']).includes(name);
|
|
87
|
+
}
|
|
88
|
+
/** The fixed files of semantics/ besides the families' handler files and their tests. */
|
|
89
|
+
const SEMANTICS_FIXED = new Set(['states.ts', 'index.ts', 'shared.ts', 'doors.ts', 'seed.ts', 'around.ts', 'events.ts', 'clock.ts', 'tenant.ts', 'scenario.ts', 'gap.ts', 'graphql.ts', 'sockets.ts']);
|
|
90
|
+
/** journeys/'s closed set. */
|
|
91
|
+
const JOURNEY_FILES = new Set(['customer-life.outline.md', 'customer-life.outline.verdict.json', 'customer-life.json', 'customer-life.verdict.json', 'decisions.json',
|
|
92
|
+
'vendor-examples.json', 'vendor-examples.verdict.json', 'demand.json', 'unreachable.json', 'score.json', 'coverage.json',
|
|
93
|
+
// a generative pack's life names the World's scenario it walks under (architecture, "Writing a life")
|
|
94
|
+
'scenario.json']);
|
|
95
|
+
/** Entries at the unit's root the form does not name (a vendor's lanes are units of their own). */
|
|
96
|
+
function rootOutsideForm(unit: GradeUnit): string[] {
|
|
97
|
+
return entriesOf(unit.dir).filter((name) => {
|
|
98
|
+
if (['spec', 'journeys', 'src'].includes(name)) return false;
|
|
99
|
+
if (existsSync(join(unit.dir, name, 'src', 'manifest.ts'))) return false;
|
|
100
|
+
return isLane(unit) || !(PACKAGE_FILES.has(name) || name === `${own(unit)}-spec-census.json`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Source with its comments removed (a comment naming process.env reads nothing). */
|
|
105
|
+
function stripComments(text: string): string {
|
|
106
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/.*$/gm, '$1');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Why there is no report: no life to walk, or a walk that failed. */
|
|
110
|
+
function noReport(unit: GradeUnit): string {
|
|
111
|
+
if (unit.walkError) return `the walk failed: ${unit.walkError}`;
|
|
112
|
+
return existsSync(join(unit.dir, 'journeys', 'customer-life.json')) ? 'the customer life was not walked (no report given)' : 'no journeys/customer-life.json';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** An operation's family: the first segment of its path after a version (`/v1/organizations/{id}` is `organizations`), a
|
|
116
|
+
* dotted RPC method's first part (`chat.postMessage` is `chat`), a line command's name (architecture, "Pack layout"). */
|
|
117
|
+
function familyOf(path: string): string {
|
|
118
|
+
// the interim rule ("What an author writes"), until derive-pack writes the spec's own grouping on each operation
|
|
119
|
+
const segs = path.split('/').filter((x) => x && !/^v?\d+(\.\d+)?$|^\d{4}-\d{2}-\d{2}$/.test(x) && !x.startsWith('{'));
|
|
120
|
+
if (!segs.length) return 'operations'; // named in a header: every path is `/`
|
|
121
|
+
const first = segs[0]!;
|
|
122
|
+
const bare = first.replace(/^\.+/, ''); // `/.well-known/...` is the family `well-known`
|
|
123
|
+
return (bare.includes('.') ? bare.split('.')[0]! : bare).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** What no handler, helper or awaited manifest function may reach ("What an author writes, and how"). */
|
|
127
|
+
const FORBIDDEN_REACH: ReadonlyArray<readonly [RegExp, string]> = [
|
|
128
|
+
[/\bMath\.random\b|\bcrypto\.(randomUUID|getRandomValues|subtle)\b|\brandomBytes\b|\brandomUUID\b/, 'randomness or an unmarked cipher'],
|
|
129
|
+
[/\bDate\.now\b|\bnew Date\(\s*\)|(^|[^.\w])Date\(\s*\)|\bperformance\.now\b/m, 'the wall clock'],
|
|
130
|
+
[/\bprocess\.env\b|\bBun\.env\b|\bDeno\.env\b/, 'the environment'],
|
|
131
|
+
[/(^|[^\w])fetch\s*\(|\bXMLHttpRequest\b|\bnode:(net|http|https|tls|dgram)\b/m, 'the network'],
|
|
132
|
+
[/\bimport\s*\(|\brequire\s*\(/, 'a module loaded at run time'],
|
|
133
|
+
[/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
|
|
134
|
+
[/\bSemanticsContext\b|\bSemantics\b|\bsemanticsContext\b/, "the kernel's wide context (a handler is typed Handler, over HandlerContext; a lane reaches its vendor's through ctx.over)"],
|
|
135
|
+
];
|
|
136
|
+
/** A handler's name for an operationId: the id with every character a JavaScript name cannot hold made `_`, and a
|
|
137
|
+
* leading digit prefixed with `_` (`pulls/create` is `pulls_create`) ("What an author writes, and how"). */
|
|
138
|
+
const RESERVED = new Set(['break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'await']);
|
|
139
|
+
export const handlerName = (operationId: string): string => {
|
|
140
|
+
const name = operationId.replace(/[^A-Za-z0-9_$]/g, '_').replace(/^(\d)/, '_$1');
|
|
141
|
+
// a name JavaScript reserves (an operation named `delete`) takes a trailing `_`
|
|
142
|
+
return RESERVED.has(name) ? `${name}_` : name;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** The unit's operations by id, with their family, from its generated surface. */
|
|
146
|
+
function operationsOf(unit: GradeUnit): Map<string, string> {
|
|
147
|
+
const surface = readJson(join(unit.dir, 'src', 'generated', 'surface.gen.json')) as { operations?: Array<{ id: string; path: string; family?: string }> } | undefined;
|
|
148
|
+
return new Map((surface?.operations ?? []).map((o) => [o.id, o.family ?? familyOf(o.path)]));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function report(unit: GradeUnit) {
|
|
152
|
+
return unit.report;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const criteria: Criterion[] = [
|
|
156
|
+
// ── form ─────────────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
{
|
|
158
|
+
id: 'spec-vendored', section: 'form',
|
|
159
|
+
asks: "the vendor's spec vendored under spec/ with SOURCE.md",
|
|
160
|
+
check(unit) {
|
|
161
|
+
const files = specFiles(unit);
|
|
162
|
+
if (!files.length) return fail('no vendored spec under spec/ (openapi, smithy, discovery, asyncapi or an RFC)');
|
|
163
|
+
if (!existsSync(join(unit.dir, 'spec', 'SOURCE.md'))) return fail('spec/SOURCE.md is missing');
|
|
164
|
+
return pass(files.length > 4 ? `${files.length} documents` : files.map((f) => relative(join(unit.dir, 'spec'), f)).join(', '));
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'spec-hash', section: 'form',
|
|
169
|
+
asks: "SOURCE.md's sha256 matches the vendored bytes",
|
|
170
|
+
check(unit) {
|
|
171
|
+
const files = specFiles(unit);
|
|
172
|
+
const source = existsSync(join(unit.dir, 'spec', 'SOURCE.md')) ? readFileSync(join(unit.dir, 'spec', 'SOURCE.md'), 'utf8') : '';
|
|
173
|
+
const sidecars = filesUnder(join(unit.dir, 'spec')).filter((f) => f.endsWith('.sha256')).map((f) => readFileSync(join(unit.dir, 'spec', f), 'utf8')).join('\n');
|
|
174
|
+
const stated = new Set(((source + sidecars).match(/\b[0-9a-f]{64}\b/g) ?? []));
|
|
175
|
+
if (!files.length || !stated.size) return fail(!files.length ? 'no vendored spec' : 'neither SOURCE.md nor a .sha256 beside the spec states a sha256');
|
|
176
|
+
const unmatched = files.filter((f) => {
|
|
177
|
+
const bytes = readFileSync(f);
|
|
178
|
+
const hashes = [sha256(bytes)];
|
|
179
|
+
if (f.endsWith('.gz')) { try { hashes.push(sha256(gunzipSync(bytes))); } catch { /* not gzip */ } }
|
|
180
|
+
return !hashes.some((h) => stated.has(h));
|
|
181
|
+
});
|
|
182
|
+
return unmatched.length ? fail(`no stated sha256 matches ${unmatched.map((f) => basename(f)).join(', ')}`) : pass('matches');
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'spec-patches', section: 'form',
|
|
187
|
+
asks: 'every correction to the spec carries its reason and its source',
|
|
188
|
+
check(unit) {
|
|
189
|
+
const path = join(unit.dir, 'spec', 'patches.json');
|
|
190
|
+
// a criterion whose premise is absent is not met: no spec, no corrections to judge
|
|
191
|
+
if (!specFiles(unit).length) return fail('no vendored spec');
|
|
192
|
+
if (!existsSync(path)) return pass('no corrections');
|
|
193
|
+
const doc = readJson(path) as { patches?: Array<{ why?: string; source?: { url?: string; spec?: string } }> } | undefined;
|
|
194
|
+
const patches = doc?.patches;
|
|
195
|
+
if (!Array.isArray(patches)) return fail('spec/patches.json is not { patches: [...] }');
|
|
196
|
+
const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || /https?:\/\//.test(p.why))).length;
|
|
197
|
+
return bare ? fail(`${bare} of ${patches.length} corrections lack a reason or a source`) : pass(`${patches.length} corrections, each sourced`);
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'manifest', section: 'form',
|
|
202
|
+
asks: 'src/manifest.ts: the facts no spec carries, its resources and state machines',
|
|
203
|
+
check(unit) {
|
|
204
|
+
return existsSync(join(unit.dir, 'src', 'manifest.ts')) ? pass('present') : fail('src/manifest.ts is missing');
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'derived-reproducible', section: 'form',
|
|
209
|
+
asks: 'derive-pack regenerates src/generated/ byte for byte',
|
|
210
|
+
check(unit) {
|
|
211
|
+
const gen = join(unit.dir, 'src', 'generated');
|
|
212
|
+
if (!existsSync(gen)) return fail('src/generated/ is missing');
|
|
213
|
+
// derive into a directory of its own and compare: the pack's tree is never written
|
|
214
|
+
const tmp = mkdtempSync(join(tmpdir(), 'grade-derive-'));
|
|
215
|
+
let run, changed: string[];
|
|
216
|
+
try {
|
|
217
|
+
run = spawnSync(process.execPath, [DERIVE, unit.dir, '--out', tmp], { cwd: unit.dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 300_000 });
|
|
218
|
+
const before = filesUnder(gen), after = allFilesUnder(tmp);
|
|
219
|
+
changed = [...new Set([...before, ...after])].sort().filter((f) => !before.includes(f) || !after.includes(f) || !readFileSync(join(gen, f)).equals(readFileSync(join(tmp, f))));
|
|
220
|
+
} finally {
|
|
221
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
// the pack's own path and the scratch directory are this checkout's, not the pack's: the verdict names neither, so
|
|
224
|
+
// a standing written from one checkout reads the same from any other
|
|
225
|
+
if (run.status !== 0) return fail(`derive-pack refused: ${String(run.stderr || run.stdout).split(unit.dir).join('.').split(tmp).join('<out>').trim().split('\n').slice(-2).join(' ').slice(0, 200)}`);
|
|
226
|
+
return changed.length ? fail(`derive-pack writes differently: ${changed.slice(0, 5).join(', ')}${changed.length > 5 ? ` and ${changed.length - 5} more` : ''}`) : pass(`${filesUnder(gen).length} files reproduced`);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'layout', section: 'form',
|
|
231
|
+
asks: "the unit's files exactly Pack layout's tree: fixed names at src/'s root, semantics/ its fixed files and one per spec family, screens/ one per declared screen, journeys/ its closed set",
|
|
232
|
+
async check(unit) {
|
|
233
|
+
const src = join(unit.dir, 'src');
|
|
234
|
+
if (!existsSync(src)) return fail('src/ is missing');
|
|
235
|
+
const families = new Set(operationsOf(unit).values());
|
|
236
|
+
const outside = [
|
|
237
|
+
...rootOutsideForm(unit),
|
|
238
|
+
...filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f)).map((f) => `spec/${f}`),
|
|
239
|
+
...entriesOf(src).filter((name) => statSync(join(src, name)).isDirectory() ? !LAYOUT_DIRS.has(name) : !srcFileInForm(unit, name)).map((n) => `src/${n}`),
|
|
240
|
+
...filesUnder(join(src, 'semantics')).filter((f) => {
|
|
241
|
+
if (f.includes('/')) return true;
|
|
242
|
+
const stem = f.replace(/\.test\.tsx?$/, '').replace(/\.tsx?$/, '');
|
|
243
|
+
return !(SEMANTICS_FIXED.has(f) || SEMANTICS_FIXED.has(`${stem}.ts`) || (families.has(stem.replace(/-ops$/, '')) && /\.(test\.)?ts$/.test(f)));
|
|
244
|
+
}).map((f) => `src/semantics/${f}`),
|
|
245
|
+
// a lane over its vendor's state has no journeys of its own: the vendor's life walks it ("Pack layout")
|
|
246
|
+
...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !JOURNEY_FILES.has(f)).map((f) => `journeys/${f}`),
|
|
247
|
+
];
|
|
248
|
+
// screens/: one file per screen the manifest declares, named by its id
|
|
249
|
+
if (existsSync(join(src, 'screens'))) {
|
|
250
|
+
let ids = new Set<string>();
|
|
251
|
+
try {
|
|
252
|
+
const m = ((await import(join(src, 'manifest.ts'))) as { manifest?: { screens?: Array<{ id: string }> } }).manifest;
|
|
253
|
+
ids = new Set((m?.screens ?? []).map((x) => x.id));
|
|
254
|
+
} catch { /* a manifest that does not load fails manifest-data and states-module */ }
|
|
255
|
+
// screens/shared.tsx: what two or more of the pack's screens share (its page shell, its skin), itself no screen
|
|
256
|
+
outside.push(...filesUnder(join(src, 'screens')).filter((f) => !/^shared\.tsx?$/.test(f) && (!ids.has(f.replace(/\.(ui)?test\.tsx?$/, '').replace(/\.tsx?$/, '')) || f.includes('/'))).map((f) => `src/screens/${f}`));
|
|
257
|
+
}
|
|
258
|
+
const fetchFile = join(src, 'fetch.ts');
|
|
259
|
+
if (!existsSync(fetchFile)) outside.push('(no src/fetch.ts)');
|
|
260
|
+
else if (!/\bcreatePackFetch\s*\(/.test(readFileSync(fetchFile, 'utf8'))) outside.push('(src/fetch.ts builds no createPackFetch)');
|
|
261
|
+
// a screen's one answer is `screen(ctx)`, which the kernel calls for its host and path
|
|
262
|
+
for (const f of existsSync(join(src, 'screens')) ? filesUnder(join(src, 'screens')).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x) && !/^shared\.tsx?$/.test(x)) : []) {
|
|
263
|
+
const names = [...stripComments(readFileSync(join(src, 'screens', f), 'utf8')).matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)|^export\s+(?:const|let|var|class)\s+([A-Za-z0-9_$]+)|^export\s*\{/gm)].map((m) => m[1] ?? m[2] ?? '{…}');
|
|
264
|
+
if (names.join() !== 'screen') outside.push(`(src/screens/${f} exports ${names.join(', ') || 'nothing'}, not only screen)`);
|
|
265
|
+
}
|
|
266
|
+
return outside.length ? fail(`${outside.length} outside the form: ${outside.slice(0, 8).join(', ')}${outside.length > 8 ? ` and ${outside.length - 8} more` : ''}`) : pass('the layout exactly');
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'handler-contract', section: 'form',
|
|
271
|
+
asks: 'every file in semantics/ reaches no randomness, clock, environment, network or module but the kernel, the states and the shared helpers; each handler file is its family\'s and exports only its operations, named by the fixed rule',
|
|
272
|
+
check(unit) {
|
|
273
|
+
const dir = join(unit.dir, 'src', 'semantics');
|
|
274
|
+
if (!existsSync(dir)) return pass('no handlers');
|
|
275
|
+
const ops = operationsOf(unit);
|
|
276
|
+
const families = new Set(ops.values());
|
|
277
|
+
const found: string[] = [];
|
|
278
|
+
for (const f of filesUnder(dir).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x))) {
|
|
279
|
+
const text = stripComments(readFileSync(join(dir, f), 'utf8'));
|
|
280
|
+
const raw = readFileSync(join(dir, f), 'utf8');
|
|
281
|
+
// a stopgap awaiting a kernel mechanism is not the contract: the mechanism is built, then the handler written
|
|
282
|
+
if (/\/\/\s*awaiting:/.test(raw)) found.push(`${f} awaits a kernel mechanism (an awaiting: marker)`);
|
|
283
|
+
// every file of semantics/, the fixed ones included: what no handler may reach, no helper may either
|
|
284
|
+
for (const [pattern, what] of FORBIDDEN_REACH) {
|
|
285
|
+
if (pattern.test(text)) found.push(`${f} reaches ${what}`);
|
|
286
|
+
}
|
|
287
|
+
for (const m of text.matchAll(/^\s*import\s+(?:[^'"]*\sfrom\s+)?['"]([^'"]+)['"]/gm)) {
|
|
288
|
+
const from = m[1]!;
|
|
289
|
+
const mapImport = f === 'index.ts' && /^\.\/[A-Za-z0-9_-]+\.ts$/.test(from); // the handler map names its families' handlers
|
|
290
|
+
// the pack's engine slot (src/engine/), itself held to the pack contract (kernel-contract): a handler computes with it
|
|
291
|
+
const engineModule = /^\.\.\/engine\/[A-Za-z0-9_-]+\.ts$/.test(from) || vendorPath(unit, from, 3, /^engine\/[A-Za-z0-9_-]+\.ts$/);
|
|
292
|
+
// a lane over its vendor's state takes the vendor's states and shared helpers ("Pack layout")
|
|
293
|
+
const vendorShared = vendorPath(unit, from, 3, /^(semantics\/(states|shared)|manifest)\.ts$/);
|
|
294
|
+
if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared) found.push(`${f} imports ${from}`);
|
|
295
|
+
}
|
|
296
|
+
// the handler map is typed over the narrow context, so the compiler holds every handler it names to the contract
|
|
297
|
+
if (f === 'index.ts' && !/:\s*Record<\s*string\s*,\s*Handler\s*>/.test(text)) found.push('index.ts does not type its handler map Record<string, Handler>');
|
|
298
|
+
if (['states.ts', 'index.ts', 'shared.ts', 'seed.ts'].includes(f)) continue;
|
|
299
|
+
const isDoors = f === 'doors.ts';
|
|
300
|
+
if (f === 'events.ts') {
|
|
301
|
+
const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
302
|
+
// a vendor whose API has a family named `events` (Polar's usage events) keeps that family's handlers here too,
|
|
303
|
+
// beside the webhook renderer; `values` is an event's further envelope values (world-core's EventValues)
|
|
304
|
+
const extra = names.filter((n) => n !== 'data' && n !== 'values');
|
|
305
|
+
const own = families.has('events') ? extra.filter((n) => ops.get([...ops.keys()].find((id) => handlerName(id) === n) ?? '') === 'events') : [];
|
|
306
|
+
if (!names.includes('data') && !own.length) found.push(`events.ts exports ${names.join(', ') || 'nothing'}, not data`);
|
|
307
|
+
const stray = extra.filter((n) => !own.includes(n));
|
|
308
|
+
if (stray.length) found.push(`events.ts exports ${stray.join(', ')}, neither data, values nor the events family's operations`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (f === 'clock.ts') {
|
|
312
|
+
const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
313
|
+
if (names.join() !== 'clock' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text)) found.push(`clock.ts exports ${names.join(', ') || 'nothing'}, not only clock`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (f === 'around.ts') {
|
|
317
|
+
const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
318
|
+
if (names.join() !== 'around') found.push(`around.ts exports ${names.join(', ') || 'nothing'}, not only around`);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (f === 'scenario.ts') {
|
|
322
|
+
const names = [...text.matchAll(/^export\s+(?:const|let|var|function|async\s+function)\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
323
|
+
if (names.join() !== 'scenario' || /^export\s*\{/m.test(text)) found.push(`scenario.ts exports ${names.join(', ') || 'nothing'}, not only scenario`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (f === 'graphql.ts') {
|
|
327
|
+
// the GraphQL wire's resolvers (world-core graphql-wire.ts): `graphql`, and nothing else
|
|
328
|
+
const names = [...text.matchAll(/^export\s+(?:const|let|var|function|async\s+function)\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
329
|
+
if (names.join() !== 'graphql' || /^export\s*\{/m.test(text)) found.push(`graphql.ts exports ${names.join(', ') || 'nothing'}, not only graphql`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (f === 'sockets.ts') {
|
|
333
|
+
// the manifest's sockets (world-core sockets.ts): one exported engine per socket id, and nothing else
|
|
334
|
+
if (/^export\s+(?:async\s+)?function\s|^export\s+(let|var|class)\s|^export\s*\{/m.test(text)) found.push('sockets.ts exports otherwise than one const engine per socket');
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (f === 'gap.ts') {
|
|
338
|
+
const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
339
|
+
if (names.join() !== 'gap' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text)) found.push(`gap.ts exports ${names.join(', ') || 'nothing'}, not only gap`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (f === 'tenant.ts') {
|
|
343
|
+
const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
344
|
+
if (names.join() !== 'tenant' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text)) found.push(`tenant.ts exports ${names.join(', ') || 'nothing'}, not only tenant`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const family = f.replace(/\.tsx?$/, '').replace(/-ops$/, '');
|
|
348
|
+
if (!isDoors && (f.includes('/') || !families.has(family))) { found.push(`${f} is no family of the spec`); continue; }
|
|
349
|
+
if (/^export\s*\{/m.test(text) || /^export\s+(const|let|var|class)\s/m.test(text)) found.push(`${f} exports otherwise than one function per operation`);
|
|
350
|
+
if (isDoors) continue; // a door is named by its door, declared in the manifest
|
|
351
|
+
for (const m of text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)) {
|
|
352
|
+
const name = m[1]!;
|
|
353
|
+
const op = [...ops.keys()].find((id) => handlerName(id) === name);
|
|
354
|
+
if (!op) found.push(`${f} exports ${name}, which names no operation of the spec`);
|
|
355
|
+
else if (ops.get(op) !== family) found.push(`${f} exports ${name}, an operation of ${ops.get(op)}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const note = '';
|
|
359
|
+
return found.length ? fail(`${found.length}: ${found.slice(0, 6).join('; ')}${found.length > 6 ? ` and ${found.length - 6} more` : ''}${note}`) : pass(`every file of semantics/ holds to the contract${note}`);
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: 'engine-pure', section: 'form',
|
|
364
|
+
asks: "src/engine/ is the vendor's computation and nothing else: no context, no state read or written, no answer made, nothing of semantics/, screens/ or the manifest reached, and none of what no handler may reach",
|
|
365
|
+
check(unit) {
|
|
366
|
+
const dir = join(unit.dir, 'src', 'engine');
|
|
367
|
+
if (!existsSync(dir)) return pass('the pack has no engine');
|
|
368
|
+
const found: string[] = [];
|
|
369
|
+
for (const f of filesUnder(dir).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x))) {
|
|
370
|
+
const text = stripComments(readFileSync(join(dir, f), 'utf8'));
|
|
371
|
+
// what a handler does belongs to a handler (semantics/<family>.ts, shared.ts), where the handler contract holds it
|
|
372
|
+
if (/\b(HandlerContext|WriteHookContext|SemanticsContext)\b/.test(text)) found.push(`${f} takes a context`);
|
|
373
|
+
if (/\bnew\s+Response\s*\(|\bResponse\.(json|redirect|error)\s*\(/.test(text)) found.push(`${f} makes an answer`);
|
|
374
|
+
for (const from of ts.preProcessFile(readFileSync(join(dir, f), 'utf8'), true, true).importedFiles.map((i) => i.fileName)) {
|
|
375
|
+
if (/^\.\.\/(semantics|screens)\/|^\.\.\/manifest\.ts$|^\.\.\/fetch\.ts$/.test(from)) found.push(`${f} imports ${from}`);
|
|
376
|
+
}
|
|
377
|
+
for (const [re, what] of FORBIDDEN_REACH) if (re.test(text)) found.push(`${f} reaches ${what}`);
|
|
378
|
+
}
|
|
379
|
+
return found.length ? fail(`${found.length}: ${found.slice(0, 6).join('; ')}${found.length > 6 ? ` and ${found.length - 6} more` : ''}`) : pass('the engine is computation only');
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: 'kernel-contract', section: 'form',
|
|
384
|
+
asks: "every file of src/ imports the kernel through its root, the pack contract, and nothing past it: no `@volter/world-core/runtime` or other entry (cli.ts alone takes args and lifecycle), no node builtin, no other package but @volter/world-ui",
|
|
385
|
+
check(unit) {
|
|
386
|
+
const src = join(unit.dir, 'src');
|
|
387
|
+
if (!existsSync(src)) return fail('src/ is missing');
|
|
388
|
+
const found: string[] = [];
|
|
389
|
+
for (const f of filesUnder(src).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x) && !x.startsWith('generated/'))) {
|
|
390
|
+
// the file's imports, re-exports and dynamic imports as TypeScript reads them (a string that only reads like one,
|
|
391
|
+
// an error message's "from '<name>'", is none)
|
|
392
|
+
const specifiers = ts.preProcessFile(readFileSync(join(src, f), 'utf8'), true, true).importedFiles.map((i) => i.fileName);
|
|
393
|
+
for (const from of specifiers) {
|
|
394
|
+
if (from.startsWith('.')) continue;
|
|
395
|
+
if (from === '@volter/world-core' || from === '@volter/world-ui') continue;
|
|
396
|
+
if (f === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle')) continue;
|
|
397
|
+
found.push(`${f} imports ${from}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return found.length ? fail(`${found.length}: ${found.slice(0, 6).join('; ')}${found.length > 6 ? ` and ${found.length - 6} more` : ''}`) : pass('every file imports only the pack contract');
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: 'manifest-data', section: 'form',
|
|
405
|
+
asks: 'the manifest is data: its imports and one exported manifest object literal, with no function in it',
|
|
406
|
+
check(unit) {
|
|
407
|
+
const path = join(unit.dir, 'src', 'manifest.ts');
|
|
408
|
+
if (!existsSync(path)) return fail('src/manifest.ts is missing');
|
|
409
|
+
// parsed, not matched: the file's top-level statements and every function inside its object
|
|
410
|
+
const file = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
411
|
+
const found: string[] = [];
|
|
412
|
+
let exported = 0;
|
|
413
|
+
for (const st of file.statements) {
|
|
414
|
+
if (ts.isImportDeclaration(st)) {
|
|
415
|
+
const from = (st.moduleSpecifier as ts.StringLiteral).text;
|
|
416
|
+
const vendorStates = vendorPath(unit, from, 2, /^semantics\/states\.ts$/);
|
|
417
|
+
if (from !== '@volter/world-core' && from !== './semantics/states.ts' && from !== './semantics/shared.ts' && from !== './semantics/events.ts' && !vendorStates) found.push(`imports ${from}`);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (ts.isTypeAliasDeclaration(st) || ts.isInterfaceDeclaration(st)) continue;
|
|
421
|
+
const isManifest = ts.isVariableStatement(st) && st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
422
|
+
&& st.declarationList.declarations.length === 1 && (st.declarationList.declarations[0]!.name as ts.Identifier).text === 'manifest';
|
|
423
|
+
if (isManifest) { exported += 1; continue; }
|
|
424
|
+
// a constant that is data (a citation, a list reused across resources) is the manifest's data too
|
|
425
|
+
const isCode = (n: ts.Node): boolean => ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isTaggedTemplateExpression(n) || ts.isGetAccessor(n) || ts.isSetAccessor(n) || ts.forEachChild(n, isCode) === true;
|
|
426
|
+
if (ts.isVariableStatement(st) && !st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
427
|
+
&& st.declarationList.declarations.every((d) => d.initializer && !isCode(d.initializer))) continue;
|
|
428
|
+
found.push(`a top-level ${ts.SyntaxKind[st.kind]} beside the manifest (${st.getText().slice(0, 40).replace(/\s+/g, ' ')}…)`);
|
|
429
|
+
}
|
|
430
|
+
if (exported !== 1) found.push('no single exported manifest');
|
|
431
|
+
const visit = (node: ts.Node): void => {
|
|
432
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node)) {
|
|
433
|
+
// no function in the data: what a manifest cannot state as data (a write hook, who is calling) is a kernel
|
|
434
|
+
// mechanism not yet built, never a function beside the facts
|
|
435
|
+
const prop = ts.isPropertyAssignment(node.parent) ? node.parent : ts.isMethodDeclaration(node) ? node : undefined;
|
|
436
|
+
const key = prop && ts.isIdentifier(prop.name) ? prop.name.text : undefined;
|
|
437
|
+
found.push(`a function at ${key ?? 'an unnamed place'}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isTaggedTemplateExpression(node)) {
|
|
441
|
+
found.push('an accessor or a tagged template, code in the data');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
445
|
+
found.push(`a call in the data (${node.getText().slice(0, 30).replace(/\s+/g, ' ')}…)`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
ts.forEachChild(node, visit);
|
|
449
|
+
};
|
|
450
|
+
visit(file);
|
|
451
|
+
return found.length ? fail([...new Set(found)].slice(0, 6).join('; ')) : pass('data');
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: 'states-module', section: 'form',
|
|
456
|
+
asks: 'every state machine and state ruling in src/semantics/states.ts (`export const states`), each resource\'s taken whole by the manifest',
|
|
457
|
+
async check(unit) {
|
|
458
|
+
// read by loading the modules, not their text: a machine built by a helper or named otherwise is still found
|
|
459
|
+
const manifestPath = join(unit.dir, 'src', 'manifest.ts');
|
|
460
|
+
const statesPath = join(unit.dir, 'src', 'semantics', 'states.ts');
|
|
461
|
+
let manifest: { resources?: Record<string, { state?: unknown; notState?: unknown }> } | undefined;
|
|
462
|
+
try { manifest = ((await import(manifestPath)) as { manifest?: typeof manifest }).manifest; } catch (e) { return fail(`manifest.ts does not load: ${(e as Error).message.slice(0, 120)}`); }
|
|
463
|
+
const resources = Object.entries(manifest?.resources ?? {});
|
|
464
|
+
const ruled = resources.filter(([, r]) => (r.state && Object.keys(r.state as object).length) || (Array.isArray(r.notState) && r.notState.length));
|
|
465
|
+
if (!ruled.length) return pass('the pack declares no state machine and rules no candidate');
|
|
466
|
+
if (!existsSync(statesPath)) return fail(`${ruled.length} resources carry states or rulings, and there is no semantics/states.ts`);
|
|
467
|
+
let states: Record<string, { state?: unknown; notState?: unknown }> | undefined;
|
|
468
|
+
try { states = ((await import(statesPath)) as { states?: typeof states }).states; } catch (e) { return fail(`semantics/states.ts does not load: ${(e as Error).message.slice(0, 120)}`); }
|
|
469
|
+
if (!states) return fail('semantics/states.ts exports no `states`');
|
|
470
|
+
const off = ruled.filter(([name, r]) => (r.state !== undefined && r.state !== states![name]?.state) || (r.notState !== undefined && r.notState !== states![name]?.notState)).map(([name]) => name);
|
|
471
|
+
return off.length ? fail(`resources whose states or rulings are not states.ts's own: ${off.join(', ')}`) : pass(`${ruled.length} resources, their states and rulings from states.ts`);
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
id: 'no-env-on-serve-path', section: 'form',
|
|
476
|
+
asks: "the form's source reads no environment variable (cli, budget and tests aside)",
|
|
477
|
+
check(unit) {
|
|
478
|
+
const src = join(unit.dir, 'src');
|
|
479
|
+
const readers = filesUnder(src).filter((f) => /\.tsx?$/.test(f) && !TEST_FILE.test(f) && !/(^|\/)(cli|[a-z0-9-]+-cli|[a-z0-9-]+-budget)\.ts$/.test(f))
|
|
480
|
+
.filter((f) => /\bprocess\.env\b|\bBun\.env\b|\bDeno\.env\b/.test(stripComments(readFileSync(join(src, f), 'utf8'))));
|
|
481
|
+
return readers.length ? fail(`reads the environment: ${readers.slice(0, 6).join(', ')}`) : pass('none');
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
// it reads the walked life's report, so it is proof, never form (the form is what a reading of the tree decides)
|
|
486
|
+
id: 'no-dead-code', section: 'proof',
|
|
487
|
+
asks: 'no file kept whose every declaration is recorded unreachable',
|
|
488
|
+
check(unit) {
|
|
489
|
+
if (!unit.report) return fail(noReport(unit));
|
|
490
|
+
const u = readJson(join(unit.dir, 'journeys', 'unreachable.json')) as { code?: Array<{ file: string; declarations?: string[] }> } | undefined;
|
|
491
|
+
const src = join(unit.dir, 'src');
|
|
492
|
+
const dead = (u?.code ?? []).filter((entry) => {
|
|
493
|
+
const path = join(src, entry.file);
|
|
494
|
+
if (!existsSync(path) || !entry.declarations?.length) return false;
|
|
495
|
+
const declared = [...readFileSync(path, 'utf8').matchAll(/^export\s+(?:async\s+)?(?:function|const|class|let)\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]!);
|
|
496
|
+
if (!declared.length || !declared.every((d) => entry.declarations!.includes(d))) return false;
|
|
497
|
+
// a declaration recorded is a part of it no World runs; the file is dead only when nothing else imports it
|
|
498
|
+
const stem = entry.file.replace(/\.tsx?$/, '').split('/').at(-1)!;
|
|
499
|
+
return !filesUnder(src).some((f) => f !== entry.file && /\.tsx?$/.test(f) && !TEST_FILE.test(f)
|
|
500
|
+
&& new RegExp(`from ['"][^'"]*\\b${stem}(\\.tsx?)?['"]`).test(readFileSync(join(src, f), 'utf8')));
|
|
501
|
+
}).map((e) => e.file);
|
|
502
|
+
return dead.length ? fail(`recorded wholly unreachable yet kept: ${dead.join(', ')}`) : pass('none');
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
// ── proof ────────────────────────────────────────────────────────────────────────────────────
|
|
506
|
+
{
|
|
507
|
+
id: 'outline-judged', section: 'proof',
|
|
508
|
+
asks: "the outline judged plausible by another session, the verdict naming the outline's current content",
|
|
509
|
+
check(unit) {
|
|
510
|
+
const journeys = vendorLanes(unit).length ? join(dirname(unit.dir), 'journeys') : join(unit.dir, 'journeys');
|
|
511
|
+
const outline = join(journeys, 'customer-life.outline.md');
|
|
512
|
+
const verdict = readJson(join(journeys, 'customer-life.outline.verdict.json')) as { sha256?: string; plausible?: boolean; judge?: { agent?: string; session?: string }; authors?: string[] } | undefined;
|
|
513
|
+
if (!existsSync(outline)) return fail('no journeys/customer-life.outline.md');
|
|
514
|
+
if (!verdict) return fail('no outline verdict');
|
|
515
|
+
if (verdict.sha256 !== sha256(readFileSync(outline))) return fail("the verdict names an older outline (its sha256 is not the outline's)");
|
|
516
|
+
if (!verdict.plausible) return fail('judged not plausible');
|
|
517
|
+
if (/self-judged/i.test(verdict.judge?.session ?? '')) return fail('self-judged (not a verdict)');
|
|
518
|
+
if (!verdict.judge?.agent) return fail('the verdict does not name its judge (judge.agent)');
|
|
519
|
+
if (!verdict.authors?.length) return fail('the verdict does not name the authors, so the judge is not shown to be another session');
|
|
520
|
+
if (verdict.authors.includes(verdict.judge.agent)) return fail('the judge is among the authors');
|
|
521
|
+
return pass(`plausible, judge ${verdict.judge.agent}`);
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: 'life-judged', section: 'proof',
|
|
526
|
+
asks: "the customer life judged plausible by another session, the verdict naming its current content",
|
|
527
|
+
check(unit) {
|
|
528
|
+
const r = report(unit);
|
|
529
|
+
if (!r) return fail(noReport(unit));
|
|
530
|
+
return r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: 'life-checks', section: 'proof',
|
|
535
|
+
asks: 'every check on every step of the life holds, with no judge flag',
|
|
536
|
+
check(unit) {
|
|
537
|
+
const r = report(unit);
|
|
538
|
+
if (!r) return fail(noReport(unit));
|
|
539
|
+
if (r.failures.length || r.flags.length) return fail(`${r.failures.length} checks failing, ${r.flags.length} judge flags${r.failures[0] ? ` — first: ${r.failures[0].beat} ${r.failures[0].step.slice(0, 60)}` : ''}`);
|
|
540
|
+
return pass(`${r.life.steps} steps hold`);
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: 'examples', section: 'proof',
|
|
545
|
+
asks: "every example the vendor published for a served operation replayed and judged",
|
|
546
|
+
check(unit) {
|
|
547
|
+
const r = report(unit);
|
|
548
|
+
if (!r) return fail(noReport(unit));
|
|
549
|
+
return r.examples.done ? pass(r.examples.line) : fail(r.examples.line || 'not replayed');
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: 'coverage', section: 'proof',
|
|
554
|
+
asks: 'the life and the examples reach every hand-written line and declared move, or the pack records why no World can',
|
|
555
|
+
check(unit) {
|
|
556
|
+
const r = report(unit);
|
|
557
|
+
if (!r?.coverage) return fail(r ? 'coverage not measured' : noReport(unit));
|
|
558
|
+
const c = r.coverage.code;
|
|
559
|
+
const note = `lines ${c.run}/${c.lines} run, ${c.recorded} recorded unreachable`;
|
|
560
|
+
return r.coverage.missing ? fail(`${r.coverage.missing} missing — ${note}`) : pass(note);
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: 'exemptions-reasoned', section: 'proof',
|
|
565
|
+
asks: 'every line or move recorded unreachable carries its reason',
|
|
566
|
+
check(unit) {
|
|
567
|
+
const u = readJson(join(unit.dir, 'journeys', 'unreachable.json')) as { code?: Array<{ why?: string }>; state?: Array<{ why?: string }> } | undefined;
|
|
568
|
+
if (!unit.report) return fail(noReport(unit));
|
|
569
|
+
if (!u) return pass('no exemptions');
|
|
570
|
+
const all = [...(u.code ?? []), ...(u.state ?? [])];
|
|
571
|
+
const bare = all.filter((e) => !e.why?.trim()).length;
|
|
572
|
+
return bare ? fail(`${bare} of ${all.length} exemptions without a reason`) : pass(`${all.length} exemptions, each with its reason`);
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
// ── soundness ────────────────────────────────────────────────────────────────────────────────
|
|
576
|
+
{
|
|
577
|
+
id: 'typecheck', section: 'soundness', executed: true,
|
|
578
|
+
asks: "the pack typechecks",
|
|
579
|
+
check(unit) {
|
|
580
|
+
const pkg = packageOf(unit);
|
|
581
|
+
if (!existsSync(join(pkg, 'tsconfig.json'))) return fail(`${pkg} has no tsconfig.json`);
|
|
582
|
+
const tsc = () => spawnSync('bunx', ['tsc', '--noEmit', '-p', '.'], { cwd: pkg, stdio: ['ignore', 'pipe', 'pipe'], timeout: 600_000 });
|
|
583
|
+
let run = tsc();
|
|
584
|
+
if (run.status !== 0) run = tsc(); // a red is rerun once before it counts (gates, "Reading a red")
|
|
585
|
+
const errors = String(run.stdout).split('\n').filter((l) => /error TS\d+/.test(l));
|
|
586
|
+
return run.status === 0 ? pass('clean') : fail(`${errors.length} errors${errors[0] ? ` — ${errors[0].slice(0, 160)}` : ''}`);
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
id: 'suites', section: 'soundness', executed: true,
|
|
591
|
+
asks: "the pack's own tests pass",
|
|
592
|
+
check(unit) {
|
|
593
|
+
const pkg = packageOf(unit);
|
|
594
|
+
const scope = `./${relative(pkg, join(unit.dir, 'src'))}/`;
|
|
595
|
+
// T0's per-test timeout: the 5 s default goes red on a random pack under load (verify-pack.ts)
|
|
596
|
+
const suites = () => spawnSync('bun', ['test', '--timeout', '60000', scope], { cwd: pkg, stdio: ['ignore', 'pipe', 'pipe'], timeout: 900_000 });
|
|
597
|
+
let run = suites();
|
|
598
|
+
if (run.status !== 0) run = suites(); // a red is rerun once before it counts (gates, "Reading a red")
|
|
599
|
+
const out = String(run.stdout) + String(run.stderr);
|
|
600
|
+
if (/Tests need "\.test"|0 test files matching/.test(out) || /\b0 tests? across/.test(out)) return pass('no tests of its own (the life is its proof)');
|
|
601
|
+
const passed = out.match(/^\s*(\d+) pass/m)?.[1] ?? '?';
|
|
602
|
+
const failed = out.match(/^\s*(\d+) fail/m)?.[1] ?? '?';
|
|
603
|
+
return run.status === 0 ? pass(`${passed} pass`) : fail(`${failed} fail, ${passed} pass`);
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
];
|
|
607
|
+
|
|
608
|
+
/** Named by "The grade" and decided by no tool yet: counted in no grade until a version of this standard checks them. */
|
|
609
|
+
export const PLANNED = [
|
|
610
|
+
'no handler that only does what the derived core does: its operation run through the core answering and writing the same',
|
|
611
|
+
'no credential stored as it was issued',
|
|
612
|
+
"no hollow capability beyond the gate's allowlist",
|
|
613
|
+
"the pack's row of the invariants matrix with every cell passing or not applicable",
|
|
614
|
+
"a patch's quote re-found in its stored source",
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
export const protocol3: Standard = { id: 'protocol-3', version: 3, title: 'Protocol 3: the derived pack', criteria, planned: PLANNED };
|