@volter/twin-standard 1.0.32 → 1.0.34
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/README.md +9 -0
- package/dist/src/assessment.d.ts +49 -0
- package/dist/src/assessment.js +124 -0
- package/dist/src/browser-assessment.d.ts +6 -0
- package/dist/src/browser-assessment.js +69 -0
- package/dist/src/cli.js +49 -1
- package/dist/src/create-pack.d.ts +38 -0
- package/dist/src/create-pack.js +576 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/pack-facts.d.ts +117 -0
- package/dist/src/pack-facts.js +242 -0
- package/dist/src/walk/behavior-journey.d.ts +1 -0
- package/dist/src/walk/behavior-journey.js +22 -7
- package/dist/src/walk/vendor-fetch.js +1 -7
- package/package.json +7 -3
- package/src/assessment.ts +93 -0
- package/src/browser-assessment.ts +47 -0
- package/src/cli.ts +30 -1
- package/src/create-pack.ts +523 -0
- package/src/index.ts +5 -0
- package/src/pack-facts.ts +240 -0
- package/src/walk/behavior-journey.ts +21 -8
- package/src/walk/vendor-fetch.ts +1 -8
package/README.md
CHANGED
|
@@ -6,11 +6,15 @@ workflow, a release, and the twin catalog's check of a submitted version all run
|
|
|
6
6
|
place a pack is judged judges it by the same standard.
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
+
twin-standard create <dir> --init <vendor> --package <@scope/name> # fixed files, in your repository
|
|
10
|
+
twin-standard create <unit> --index # fixed handler map and fetch after derivation
|
|
9
11
|
twin-standard gate <pack repository> [<pack or lane dir>…] # every unit passes the form section, or exit 1
|
|
10
12
|
twin-standard gate-published <name>@<version>… [--registry <url>] # a published version, as the catalog grades a submission
|
|
11
13
|
twin-standard grade <pack or lane dir>… [--static] [--form] # each criterion's result
|
|
12
14
|
twin-standard derive <pack or lane dir> [--out <dir>] # write src/generated/ from the vendored spec
|
|
13
15
|
twin-standard check-sources <pack or lane dir> [--recheck] # fetch and record the sources the unit cites
|
|
16
|
+
twin-standard facts <pack dir> [--out <file>] # package-local facts from its descriptor
|
|
17
|
+
twin-standard assess <pack dir> [--browser] [--out <file>] # inside a World
|
|
14
18
|
twin-standard --version # this package's version and each standard's
|
|
15
19
|
```
|
|
16
20
|
|
|
@@ -18,3 +22,8 @@ What each criterion asks, and the rules for changing one, are twin-world's
|
|
|
18
22
|
[architecture, "The grade"](https://github.com/volter-ai/twin-world/blob/main/docs/contributing/architecture.md#the-grade).
|
|
19
23
|
A changed criterion is a release of this package: a criterion's id never changes meaning; a changed criterion takes a
|
|
20
24
|
new id and a new standard version, and a tightened check names what newly fails.
|
|
25
|
+
|
|
26
|
+
The public `assessPack(packDir, { browser: true })` API runs inside a World, reports actual operation coverage from
|
|
27
|
+
kernel dispatch and replays journeys through Chromium when their wire is HTTP. Install Playwright and Chromium for
|
|
28
|
+
that target. Its report distinguishes API support, operations exercised, journey failures and replay, and does not
|
|
29
|
+
claim DOM interaction coverage. Catalog controllers own provenance, admission and publication.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type AssessmentOptions = {
|
|
2
|
+
browser?: boolean;
|
|
3
|
+
conformance?: boolean;
|
|
4
|
+
};
|
|
5
|
+
/** Surface coverage counts actual kernel dispatch, and retains every declared gap in the denominator. */
|
|
6
|
+
export declare function assessPack(packDir: string, options?: AssessmentOptions): Promise<{
|
|
7
|
+
schemaVersion: number;
|
|
8
|
+
vendor: string;
|
|
9
|
+
package: any;
|
|
10
|
+
version: any;
|
|
11
|
+
ready: boolean;
|
|
12
|
+
gates: {
|
|
13
|
+
unit: string;
|
|
14
|
+
failures: string[];
|
|
15
|
+
}[];
|
|
16
|
+
quick: {
|
|
17
|
+
scope: string;
|
|
18
|
+
steps: number;
|
|
19
|
+
answered: number;
|
|
20
|
+
failures: {
|
|
21
|
+
step: string;
|
|
22
|
+
beat: string;
|
|
23
|
+
detail: string;
|
|
24
|
+
}[];
|
|
25
|
+
replay: {
|
|
26
|
+
equal: boolean;
|
|
27
|
+
runs: number;
|
|
28
|
+
firstSha256: string;
|
|
29
|
+
secondSha256: string;
|
|
30
|
+
};
|
|
31
|
+
operationCoverage: {
|
|
32
|
+
total: number;
|
|
33
|
+
exercised: number;
|
|
34
|
+
percent: number;
|
|
35
|
+
operations: string[];
|
|
36
|
+
missing: string[];
|
|
37
|
+
} | null;
|
|
38
|
+
servedOperations: string[];
|
|
39
|
+
surface: {
|
|
40
|
+
total: number;
|
|
41
|
+
served: number;
|
|
42
|
+
gap: number;
|
|
43
|
+
} | null;
|
|
44
|
+
codeCoverage: null;
|
|
45
|
+
stateTransitionCoverage: null;
|
|
46
|
+
};
|
|
47
|
+
browser: Record<string, unknown>;
|
|
48
|
+
conformance: import("./conformance.js").ConformanceResult[] | null;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// Published assessment API. The standard owns journeys and checks; catalog controllers own admission authority.
|
|
10
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { observeDerivedDispatch } from '@volter/world-core/runtime';
|
|
14
|
+
import { p3Gate } from "./gate.js";
|
|
15
|
+
import { conformance } from "./conformance.js";
|
|
16
|
+
import { inMemoryWorld, walk, withPinnedWorldDraws, usePacks } from "./walk/behavior-journey.js";
|
|
17
|
+
import { compilePackFacts } from "./pack-facts.js";
|
|
18
|
+
import { expandFills } from "./walk/journey-kit.js";
|
|
19
|
+
const canonical = (value) => value && typeof value === 'object' ? Array.isArray(value) ? value.map(canonical) : Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)])) : value;
|
|
20
|
+
const json = (value) => JSON.stringify(canonical(value));
|
|
21
|
+
const hash = (value) => createHash('sha256').update(json(value)).digest('hex');
|
|
22
|
+
const read = (file) => JSON.parse(readFileSync(file, 'utf8'));
|
|
23
|
+
/** Surface coverage counts actual kernel dispatch, and retains every declared gap in the denominator. */
|
|
24
|
+
export async function assessPack(packDir, options = {}) {
|
|
25
|
+
if (!process.env.VOLTER_WORLD)
|
|
26
|
+
throw new Error('Assessment must run inside a World');
|
|
27
|
+
const facts = read(join(packDir, 'generated/pack-facts.json'));
|
|
28
|
+
const vendor = Object.keys(facts.packs ?? {})[0];
|
|
29
|
+
if (!vendor || Object.keys(facts.packs).length !== 1)
|
|
30
|
+
throw new Error('Assessment needs one published vendor identity');
|
|
31
|
+
const manifest = read(join(packDir, 'package.json'));
|
|
32
|
+
const compiled = await compilePackFacts([{ vendor, dir: packDir }]);
|
|
33
|
+
if (json(compiled.packs) !== json(facts.packs) || compiled.undeclared.length)
|
|
34
|
+
throw new Error('Published facts differ from the pack descriptor');
|
|
35
|
+
const lanes = readdirSync(packDir).filter((name) => existsSync(join(packDir, name, 'src/manifest.ts')) && !existsSync(join(packDir, name, 'package.json')));
|
|
36
|
+
const units = [packDir, ...lanes.map((name) => join(packDir, name))];
|
|
37
|
+
if (packDir.split(/[\\/]/).at(-1) !== vendor)
|
|
38
|
+
throw new Error('Place the artifact in a directory named for its vendor before assessment');
|
|
39
|
+
usePacks(join(packDir, '..'));
|
|
40
|
+
const gates = await p3Gate(units);
|
|
41
|
+
const life = expandFills(read(join(packDir, 'journeys/customer-life.json')));
|
|
42
|
+
if (!Array.isArray(life.steps) || !life.steps.length)
|
|
43
|
+
throw new Error('Assessment needs a nonempty customer journey');
|
|
44
|
+
const surfaceRows = [];
|
|
45
|
+
for (const unit of units) {
|
|
46
|
+
const file = join(unit, 'src/generated/surface.gen.json');
|
|
47
|
+
if (!existsSync(file))
|
|
48
|
+
continue;
|
|
49
|
+
const surface = read(file);
|
|
50
|
+
if (!Array.isArray(surface.operations) || !surface.operations.length)
|
|
51
|
+
continue;
|
|
52
|
+
surfaceRows.push({ unit: unit === packDir ? vendor : `${vendor}/${unit.slice(packDir.length + 1)}`, signature: hash(surface.operations), ids: surface.operations.map((operation) => operation.id) });
|
|
53
|
+
}
|
|
54
|
+
const declaredHttp = [...new Set(surfaceRows.flatMap((row) => row.ids.map((id) => `${row.unit}:${id}`)))].sort();
|
|
55
|
+
const mod = await import(__rewriteRelativeImportExtension(join(packDir, 'src/index.ts')));
|
|
56
|
+
const factory = Object.entries(mod).find(([name, value]) => /^create\w+Fetch$/.test(name) && !/TwinFetch$/.test(name) && typeof value === 'function')?.[1];
|
|
57
|
+
const surfaceWorld = inMemoryWorld('catalog-surface');
|
|
58
|
+
let owners = null;
|
|
59
|
+
try {
|
|
60
|
+
const wire = factory?.({ root: surfaceWorld.root() });
|
|
61
|
+
if (wire?.ownersByUnit)
|
|
62
|
+
owners = Object.fromEntries(Object.entries(wire.ownersByUnit()).flatMap(([unit, operations]) => Object.entries(operations).map(([id, owner]) => [`${vendor}${unit ? `/${unit}` : ''}:${id}`, owner])));
|
|
63
|
+
else if (wire?.owners)
|
|
64
|
+
owners = Object.fromEntries(Object.entries(wire.owners()).map(([id, owner]) => [`${vendor}:${id}`, owner]));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
surfaceWorld.restore();
|
|
68
|
+
}
|
|
69
|
+
const declared = owners ? Object.keys(owners).sort() : [];
|
|
70
|
+
const servedOperations = owners ? declared.filter((id) => owners[id] !== 'gap') : [];
|
|
71
|
+
const instrumented = declared.length > 0 && json(declared) === json(declaredHttp);
|
|
72
|
+
const coverageOf = (result) => !instrumented || result.ambiguous ? null : { total: declared.length, exercised: result.exercised.length, percent: 100 * result.exercised.length / declared.length, operations: result.exercised, missing: declared.filter((id) => !result.exercised.includes(id)) };
|
|
73
|
+
async function replay(served) {
|
|
74
|
+
const world = inMemoryWorld('catalog-replay');
|
|
75
|
+
const failures = [], trace = [];
|
|
76
|
+
const exercised = new Set();
|
|
77
|
+
const signatures = new WeakMap();
|
|
78
|
+
let ambiguous = false;
|
|
79
|
+
const stop = observeDerivedDispatch(({ surface, operation }) => {
|
|
80
|
+
let signature = signatures.get(surface.operations);
|
|
81
|
+
if (!signature) {
|
|
82
|
+
signature = hash(surface.operations);
|
|
83
|
+
signatures.set(surface.operations, signature);
|
|
84
|
+
}
|
|
85
|
+
const candidates = surfaceRows.filter((row) => row.signature === signature);
|
|
86
|
+
if (!candidates.length)
|
|
87
|
+
return; // Companion dispatch is outside this pack's denominator.
|
|
88
|
+
if (candidates.length !== 1) {
|
|
89
|
+
ambiguous = true;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const id = `${candidates[0].unit}:${operation.id}`;
|
|
93
|
+
exercised.add(id);
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await withPinnedWorldDraws(() => walk(structuredClone(life), world.root(), failures, { beats: true, ...(served ? { served } : {}), hooks: { after: (step, path, answer) => trace.push({ name: step.name ?? '', path, status: answer.status, body: answer.body }) } }));
|
|
97
|
+
return { failures, trace, exercised: [...exercised].sort(), ambiguous };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
stop();
|
|
101
|
+
world.restore();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const first = await replay(), second = await replay();
|
|
105
|
+
const replayEqual = first.trace.length > 0 && json(first.trace) === json(second.trace);
|
|
106
|
+
const coverage = coverageOf(first);
|
|
107
|
+
const quick = { scope: 'packaged-customer-journey; in-process', steps: life.steps.length, answered: first.trace.length, failures: [...first.failures, ...second.failures], replay: { equal: replayEqual, runs: 2, firstSha256: hash(first.trace), secondSha256: hash(second.trace) }, operationCoverage: coverage, servedOperations, surface: owners ? { total: declared.length, served: servedOperations.length, gap: declared.length - servedOperations.length } : null, codeCoverage: null, stateTransitionCoverage: null };
|
|
108
|
+
let browser = { measured: false, reason: options.browser ? 'Journey has a wire Chromium fetch cannot exercise' : 'Browser target not requested' };
|
|
109
|
+
const httpOnly = life.steps.every((step) => 'method' in step || 'wait' in step);
|
|
110
|
+
if (options.browser && httpOnly) {
|
|
111
|
+
const { chromiumServed } = await import("./browser-assessment.js");
|
|
112
|
+
const transport = await chromiumServed(packDir, life.host ?? 'twin.local');
|
|
113
|
+
try {
|
|
114
|
+
const a = await replay(transport.served), b = await replay(transport.served);
|
|
115
|
+
browser = { measured: true, target: 'Chromium fetch, cookie jar and HTTP response behavior; no DOM interaction coverage', version: transport.version, answered: a.trace.length, failures: [...a.failures, ...b.failures], replay: { equal: a.trace.length > 0 && json(a.trace) === json(b.trace), firstSha256: hash(a.trace), secondSha256: hash(b.trace), runs: 2 }, operationCoverage: coverageOf(a), domCoverage: null, clock: 'World and page JavaScript clocks frozen; native cookie expiration uses Chromium wall time' };
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await transport.stop();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const conform = options.conformance === false ? null : await conformance(packDir);
|
|
122
|
+
const ready = gates.length > 0 && gates.every((gate) => !gate.failures.length) && !quick.failures.length && replayEqual && conform !== null && conform.length > 0 && conform.every((result) => !result.failures.length) && (!options.browser || !httpOnly || browser.measured === true && browser.failures.length === 0 && browser.replay.equal);
|
|
123
|
+
return { schemaVersion: 1, vendor, package: manifest.name, version: manifest.version, ready, gates: gates.map((gate) => ({ ...gate, unit: gate.unit === packDir ? vendor : `${vendor}/${gate.unit.slice(packDir.length + 1)}` })), quick, browser, conformance: conform };
|
|
124
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// Browser transport for the existing journey engine. A container supplies network isolation in catalog execution.
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
export async function chromiumServed(packDir, host) {
|
|
12
|
+
const { chromium } = await import('playwright');
|
|
13
|
+
const browser = await chromium.launch({ args: ['--no-sandbox'] });
|
|
14
|
+
const mod = await import(__rewriteRelativeImportExtension(join(packDir, 'src/index.ts')));
|
|
15
|
+
const factory = Object.entries(mod).find(([name, value]) => /^create\w+Fetch$/.test(name) && !/TwinFetch$/.test(name) && typeof value === 'function')?.[1];
|
|
16
|
+
if (!factory) {
|
|
17
|
+
await browser.close();
|
|
18
|
+
throw new Error('Browser assessment needs the pack HTTP fetch factory');
|
|
19
|
+
}
|
|
20
|
+
const served = async (root, options) => {
|
|
21
|
+
const fetchPack = factory({ root, ...options });
|
|
22
|
+
const context = await browser.newContext({ serviceWorkers: 'block', locale: 'en-US', timezoneId: 'UTC' });
|
|
23
|
+
const page = await context.newPage();
|
|
24
|
+
const harness = `https://${host}/__catalog_assessment__`;
|
|
25
|
+
await context.route('**/*', async (route) => {
|
|
26
|
+
const request = route.request();
|
|
27
|
+
if (request.url() === harness) {
|
|
28
|
+
await route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><title>Catalog assessment</title>' });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!['xhr', 'fetch'].includes(request.resourceType())) {
|
|
32
|
+
await route.abort('blockedbyclient');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetchPack(new Request(request.url(), { method: request.method(), headers: await request.allHeaders(), ...(request.postDataBuffer() ? { body: new Uint8Array(request.postDataBuffer()).buffer } : {}) }));
|
|
37
|
+
const headers = Object.fromEntries(response.headers);
|
|
38
|
+
const cookies = response.headers.getSetCookie();
|
|
39
|
+
if (cookies.length)
|
|
40
|
+
headers['set-cookie'] = cookies.join('\n');
|
|
41
|
+
await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
await route.abort('failed');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
await page.goto(harness);
|
|
48
|
+
await page.clock.setFixedTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
49
|
+
return {
|
|
50
|
+
fetch: async (request) => {
|
|
51
|
+
const body = request.body ? Buffer.from(await request.arrayBuffer()).toString('base64') : null;
|
|
52
|
+
const result = await page.evaluate(async ({ url, method, headers, body }) => {
|
|
53
|
+
const bytes = body === null ? undefined : Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
|
|
54
|
+
const response = await fetch(url, { method, headers, body: bytes, redirect: 'manual', credentials: 'include' });
|
|
55
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
56
|
+
let text = '';
|
|
57
|
+
for (let i = 0; i < data.length; i += 32768)
|
|
58
|
+
text += String.fromCharCode(...data.subarray(i, i + 32768));
|
|
59
|
+
return { status: response.status, headers: [...response.headers], body: btoa(text) };
|
|
60
|
+
}, { url: request.url, method: request.method, headers: Object.fromEntries(request.headers), body });
|
|
61
|
+
if (!result.status)
|
|
62
|
+
throw new Error('Chromium returned an opaque response; this journey needs browser-specific navigation assertions');
|
|
63
|
+
return new Response([101, 204, 205, 304].includes(result.status) || request.method === 'HEAD' ? null : Buffer.from(result.body, 'base64'), { status: result.status, headers: result.headers });
|
|
64
|
+
},
|
|
65
|
+
stop: () => { void context.close(); },
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
return { served, version: browser.version(), stop: () => browser.close() };
|
|
69
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
3
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
4
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
5
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
return path;
|
|
9
|
+
};
|
|
2
10
|
// twin-standard — the Protocol 3 standard's commands, the same wherever they run (a pack repository's hook and workflow,
|
|
3
11
|
// a release, the catalog's check of a submission, an author's machine):
|
|
4
12
|
//
|
|
@@ -22,7 +30,7 @@ import { withCheckoutFiles } from "./protocol-3.js";
|
|
|
22
30
|
import { gatePublished } from "./published.js";
|
|
23
31
|
const [command, ...args] = process.argv.slice(2);
|
|
24
32
|
const usage = () => {
|
|
25
|
-
console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | read-page <url> [--raw] | check-sources <unit> [--recheck] | conform <pack dir> | <name>@<version> [--registry <url>] | --version');
|
|
33
|
+
console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | read-page <url> [--raw] | check-sources <unit> [--recheck] | conform <pack dir> | <name>@<version> [--registry <url>] | create <dir> --init <vendor> --package <@scope/name> | create <unit> --index | facts <pack dir> [--out <file>] | assess <pack dir> [--browser] [--out <file>] | --version');
|
|
26
34
|
process.exit(2);
|
|
27
35
|
};
|
|
28
36
|
if (command === '--version') {
|
|
@@ -95,6 +103,46 @@ else if (command === 'grade') {
|
|
|
95
103
|
}
|
|
96
104
|
});
|
|
97
105
|
}
|
|
106
|
+
else if (command === 'create') {
|
|
107
|
+
if (args.includes('--init') && !args.includes('--package'))
|
|
108
|
+
throw new Error('Name your scoped package with --package');
|
|
109
|
+
await (await import("./create-pack.js")).createPack(args);
|
|
110
|
+
}
|
|
111
|
+
else if (command === 'assess') {
|
|
112
|
+
if (!args[0])
|
|
113
|
+
usage();
|
|
114
|
+
const { assessPack } = await import("./assessment.js");
|
|
115
|
+
const report = await assessPack(resolve(args[0]), { browser: args.includes('--browser') });
|
|
116
|
+
const at = args.indexOf('--out');
|
|
117
|
+
if (at >= 0) {
|
|
118
|
+
if (!args[at + 1])
|
|
119
|
+
usage();
|
|
120
|
+
const { writeFileSync } = await import('node:fs');
|
|
121
|
+
writeFileSync(resolve(args[at + 1]), `${JSON.stringify(report, null, 2)}\n`);
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
console.log(JSON.stringify(report, null, 2));
|
|
125
|
+
process.exit(report.ready ? 0 : 1);
|
|
126
|
+
}
|
|
127
|
+
else if (command === 'facts') {
|
|
128
|
+
if (!args[0])
|
|
129
|
+
usage();
|
|
130
|
+
const { compilePackFacts, renderPackFacts } = await import("./pack-facts.js");
|
|
131
|
+
const { mkdirSync, writeFileSync } = await import('node:fs');
|
|
132
|
+
const { join, dirname } = await import('node:path');
|
|
133
|
+
const dir = resolve(args[0]);
|
|
134
|
+
const vendor = String((await import(__rewriteRelativeImportExtension(join(dir, 'src/index.ts')))).pack?.vendor ?? '');
|
|
135
|
+
const at = args.indexOf('--out');
|
|
136
|
+
if (at >= 0 && !args[at + 1])
|
|
137
|
+
usage();
|
|
138
|
+
const out = at >= 0 ? resolve(args[at + 1]) : join(dir, 'generated/pack-facts.json');
|
|
139
|
+
const facts = await compilePackFacts([{ vendor, dir }]);
|
|
140
|
+
if (facts.undeclared.length)
|
|
141
|
+
throw new Error('Pack descriptor is missing');
|
|
142
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
143
|
+
writeFileSync(out, renderPackFacts(facts));
|
|
144
|
+
console.log(out);
|
|
145
|
+
}
|
|
98
146
|
else if (command === 'derive') {
|
|
99
147
|
if (!args[0])
|
|
100
148
|
usage();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** src/semantics/index.ts: the handler map by operationId, keeping the name the unit's fetch imports it by. Each
|
|
3
|
+
* operation of the generated surface whose handler (named as `handlerName` names it) a family's file exports is an
|
|
4
|
+
* entry: an id no JavaScript name can hold (`storage.object.get`, `pulls/create`) is keyed as it is. */
|
|
5
|
+
export declare function handlerIndex(dir: string): string;
|
|
6
|
+
/** What a unit serves, checked against its decision table and its demand (the P3 procedure: demand, then the decision
|
|
7
|
+
* table, then the code). `journeys/decisions.json` (the vendor's, beside its lanes) names, for each operation the unit
|
|
8
|
+
* serves, the demand it answers and the mechanism that serves it (`handler` or `core`); an operation it does not serve
|
|
9
|
+
* is `unmodeled` in the manifest. The unit's owners are the vendor's dispatch's for that unit. A problem stops the index:
|
|
10
|
+
* six packs once shipped answering 404 to everything, and scope once grew past demand, both unseen by the tools. */
|
|
11
|
+
export declare function decisionProblems(dir: string): Promise<string[]>;
|
|
12
|
+
/** A unit's fetch.ts, a fixed file: `createPackFetch` over its parts as they stand (the manifest, the surface, the handler
|
|
13
|
+
* map, and its doors, clock and screens where it has them). `--init` writes it, and `--index` again when a door file,
|
|
14
|
+
* the clock or a screen is added. `name` is the fetch's export (`createClerkFetch`, a lane's `createFapiLaneFetch`). */
|
|
15
|
+
/** A unit's fetch export: a pack's `create<Vendor>Fetch`, a lane's (a unit with no package.json of its own) `create<Lane>LaneFetch`. */
|
|
16
|
+
export declare const fetchName: (dir: string) => string;
|
|
17
|
+
export declare function packFetchFile(dir: string, name: string): string;
|
|
18
|
+
/** The author's brief: the operations left to write, by family, each with the spec's words. */
|
|
19
|
+
export declare function brief(unit: string): Promise<string>;
|
|
20
|
+
/** A pack's `src/index.ts` (a fixed file): the descriptor registered with what the kernel derives from the pack's
|
|
21
|
+
* generated surface (the real-system adapters), or, for a vendor whose every API is a lane, each lane's manifest and
|
|
22
|
+
* surface. A lane's own index is not a pack's and is not written here. */
|
|
23
|
+
export declare function packIndexFile(dir: string, vendor: string): string;
|
|
24
|
+
/** A pack's `src/server.ts` (a fixed file), the kernel's serve seam around its fetch, naming only what its unit has so
|
|
25
|
+
* it typechecks and a bundler follows every import: the plain seam (serveHttp, which takes the sockets a fetch carries
|
|
26
|
+
* itself: "Other wires"), or, for a unit whose spec is a proto, the gRPC seam over the schema derive-pack generated
|
|
27
|
+
* (src/generated/proto.gen.json, imported by its literal path). `--init` writes the plain one, `--index` the gRPC one once
|
|
28
|
+
* the unit's proto is derived. */
|
|
29
|
+
export declare function serverFile(vendor: string, grpc: boolean): string;
|
|
30
|
+
/** A pack's tsconfig.json, a fixed file: what the grade's typecheck (soundness) compiles, the pack and its lanes. */
|
|
31
|
+
export declare const TSCONFIG: string;
|
|
32
|
+
/** The fixed files of a new pack, by path, for a vendor: what nobody should write by hand. */
|
|
33
|
+
export declare function initFiles(vendor: string): Record<string, string>;
|
|
34
|
+
/** The fixed files of a pack of lanes (Cloudflare's API and R2, Sigstore's Fulcio and Rekor): each lane a derived
|
|
35
|
+
* surface of its own under `<lane>/src`, keeping the vendor's one store and sharing its `src/semantics/shared.ts`, and the
|
|
36
|
+
* pack's server routing each lane's host to it. */
|
|
37
|
+
export declare function laneInitFiles(vendor: string, lanes: string[]): Record<string, string>;
|
|
38
|
+
export declare function createPack(args: string[]): Promise<void>;
|