@volter/twin-standard 1.0.148 → 1.0.150
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 +12 -3
- package/client-sdks/bun.lock +0 -32
- package/client-sdks/package.json +0 -2
- package/dist/client-sdks/bun.lock +0 -32
- package/dist/client-sdks/package.json +0 -2
- package/dist/src/assessment.d.ts +6 -4
- package/dist/src/assessment.js +2 -1
- package/dist/src/cli.js +21 -6
- package/dist/src/clients/harness.d.ts +3 -1
- package/dist/src/clients/harness.js +4 -2
- package/dist/src/clients/jira.d.ts +2 -0
- package/dist/src/clients/jira.js +71 -0
- package/dist/src/clients/polar.d.ts +2 -0
- package/dist/src/clients/polar.js +73 -0
- package/dist/src/clients/resend.js +38 -0
- package/dist/src/clients/run.js +1 -1
- package/dist/src/clients/sentry.js +1 -26
- package/dist/src/clients/smtp.d.ts +2 -0
- package/dist/src/clients/smtp.js +54 -0
- package/dist/src/clients/stripe.js +1 -1
- package/dist/src/clients/volteridentity.js +4 -3
- package/dist/src/clients/x.d.ts +2 -0
- package/dist/src/clients/x.js +64 -0
- package/dist/src/conformance.d.ts +4 -3
- package/dist/src/conformance.js +293 -8
- package/dist/src/consumer.js +5 -1
- package/dist/src/create-pack.d.ts +5 -0
- package/dist/src/create-pack.js +38 -4
- package/dist/src/derive.js +21 -13
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +47 -4
- package/dist/src/grade.d.ts +1 -0
- package/dist/src/grade.js +1 -1
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pack-facts.js +11 -2
- package/dist/src/pack-files.js +2 -1
- package/dist/src/protocol-3.js +40 -6
- package/dist/src/spec-ir-client.js +3 -2
- package/dist/src/spec-ir-commands.d.ts +73 -0
- package/dist/src/spec-ir-commands.js +63 -0
- package/dist/src/spec-ir.d.ts +2 -0
- package/dist/src/spec-ir.js +43 -0
- package/dist/src/tools/judge-brief.d.ts +1 -0
- package/dist/src/tools/judge-brief.js +67 -0
- package/dist/src/tools/life-coverage-walk.d.ts +2 -0
- package/dist/src/tools/life-coverage-walk.js +220 -0
- package/dist/src/tools/life-coverage.d.ts +2 -0
- package/dist/src/tools/life-coverage.js +271 -0
- package/dist/src/tools/packs-root.d.ts +9 -0
- package/dist/src/tools/packs-root.js +17 -0
- package/dist/src/tools/run-tool.d.ts +1 -0
- package/dist/src/tools/run-tool.js +18 -0
- package/dist/src/tools/score-pack.d.ts +2 -0
- package/dist/src/tools/score-pack.js +264 -0
- package/dist/src/tools/vendor-examples.d.ts +2 -0
- package/dist/src/tools/vendor-examples.js +1493 -0
- package/dist/src/types.d.ts +2 -0
- package/dist/src/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +6 -0
- package/dist/src/walk/behavior-journey.js +108 -8
- package/dist/src/walk/journey-kit.d.ts +23 -1
- package/dist/src/walk/journey-kit.js +7 -2
- package/dist/src/walk/rendered-scalar.d.ts +3 -0
- package/dist/src/walk/rendered-scalar.js +23 -0
- package/dist/src/walk/sse-example.js +45 -0
- package/package.json +2 -2
- package/src/assessment.ts +2 -1
- package/src/cli.ts +15 -2
- package/src/clients/harness.ts +4 -2
- package/src/clients/jira.ts +73 -0
- package/src/clients/polar.ts +73 -0
- package/src/clients/resend.ts +32 -0
- package/src/clients/run.ts +2 -2
- package/src/clients/sentry.ts +1 -25
- package/src/clients/smtp.ts +49 -0
- package/src/clients/stripe.ts +1 -1
- package/src/clients/volteridentity.ts +4 -3
- package/src/clients/x.ts +66 -0
- package/src/conformance.ts +226 -10
- package/src/consumer.ts +5 -1
- package/src/create-pack.ts +35 -5
- package/src/derive.ts +21 -14
- package/src/gate.ts +40 -6
- package/src/grade.ts +2 -2
- package/src/pack-facts.ts +10 -3
- package/src/pack-files.ts +2 -1
- package/src/protocol-3.ts +32 -6
- package/src/spec-ir-client.ts +3 -2
- package/src/spec-ir-commands.ts +81 -0
- package/src/spec-ir.ts +31 -0
- package/src/tools/judge-brief.ts +63 -0
- package/src/tools/life-coverage-walk.ts +173 -0
- package/src/tools/life-coverage.ts +237 -0
- package/src/tools/packs-root.ts +21 -0
- package/src/tools/run-tool.ts +10 -0
- package/src/tools/score-pack.ts +244 -0
- package/src/tools/vendor-examples.ts +1250 -0
- package/src/types.ts +2 -0
- package/src/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +65 -4
- package/src/walk/journey-kit.ts +21 -3
- package/src/walk/rendered-scalar.ts +17 -0
- package/src/walk/sse-example.ts +37 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// A JUDGE'S BRIEF (pack-done's (f); architecture, "What a judge rejects"): what a judge reads and writes for one vendor's
|
|
2
|
+
// outline, life and published examples, printed for a judge in another session (`codex exec`, `claude -p`, any agent
|
|
3
|
+
// runtime). The verdict names its judge and the authors; the grade refuses one whose judge is an author
|
|
4
|
+
// (packages/twin-standard/src/verdict.ts). One judging per pack: the verdict stands through later changes.
|
|
5
|
+
//
|
|
6
|
+
// twin-standard judge-brief <vendor> --authors <author name>[,<agent>...] (from the pack repository's root; twin-world's
|
|
7
|
+
// bun scripts/judge-brief.ts runs the same)
|
|
8
|
+
import { attributionName } from '../verdict.ts';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { packsRoot } from './packs-root.ts';
|
|
13
|
+
const PACKS = packsRoot();
|
|
14
|
+
/** The architecture a judge's list is read from: the twin-world checkout this standard is in, else its published copy. */
|
|
15
|
+
const ARCHITECTURE = join(import.meta.dir, '..', '..', '..', '..', 'docs', 'contributing', 'architecture.md');
|
|
16
|
+
const PUBLISHED_ARCHITECTURE = 'https://world-docs.volter.ai/docs/contributing/architecture.md';
|
|
17
|
+
|
|
18
|
+
const vendor = process.argv[2];
|
|
19
|
+
const authorsAt = process.argv.indexOf('--authors');
|
|
20
|
+
const authors = authorsAt > 0 ? (process.argv[authorsAt + 1] ?? '').split(',').map((a) => a.trim()).filter(Boolean).map((name) => attributionName(name)!) : [];
|
|
21
|
+
if (!vendor || vendor.startsWith('-') || !authors.length) {
|
|
22
|
+
console.error('usage: twin-standard judge-brief <vendor> --authors <author name>[,<agent>...] (from the pack repository\'s root)');
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
const journeys = join(PACKS, vendor, 'journeys');
|
|
26
|
+
if (!existsSync(journeys)) { console.error(`judge-brief: ${vendor} has no journeys/ in ${PACKS}`); process.exit(2); }
|
|
27
|
+
// the list of what a judge rejects, as architecture holds it
|
|
28
|
+
const architecture = existsSync(ARCHITECTURE) ? readFileSync(ARCHITECTURE, 'utf8') : await fetch(PUBLISHED_ARCHITECTURE).then((r) => (r.ok ? r.text() : ''), () => '');
|
|
29
|
+
const listAt = architecture.indexOf('#### What a judge rejects\n');
|
|
30
|
+
if (listAt < 0) { console.error(`judge-brief: the architecture (${existsSync(ARCHITECTURE) ? ARCHITECTURE : PUBLISHED_ARCHITECTURE}) has no "#### What a judge rejects" section`); process.exit(2); }
|
|
31
|
+
const listFrom = architecture.indexOf('\n- ', listAt);
|
|
32
|
+
const listTo = architecture.indexOf('\n\n', listFrom + 1);
|
|
33
|
+
const rejects = architecture.slice(listFrom + 1, listTo < 0 ? undefined : listTo).trim();
|
|
34
|
+
const sha = (file: string): string => createHash('sha256').update(readFileSync(file)).digest('hex');
|
|
35
|
+
const judged = [
|
|
36
|
+
{ file: 'customer-life.outline.md', verdict: 'customer-life.outline.verdict.json', what: "the life's outline: the customer's story in acts" },
|
|
37
|
+
{ file: 'customer-life.json', verdict: 'customer-life.verdict.json', what: 'the life: every step an application or person takes, its expectations' },
|
|
38
|
+
{ file: 'vendor-examples.json', verdict: 'vendor-examples.verdict.json', what: "the published examples: the vendor's own documented requests and answers, replayed" },
|
|
39
|
+
].filter((j) => existsSync(join(journeys, j.file)));
|
|
40
|
+
|
|
41
|
+
console.log(`You are the judge of the ${vendor} twin's journeys. You did not write them, and you judge them in a session other than
|
|
42
|
+
the run that wrote them. Judge them against the vendor and against the list below; do not edit any judged file, and run no
|
|
43
|
+
test. Read what you need: the pack's spec (${vendor}/spec/, SOURCE.md naming every page read), its demand
|
|
44
|
+
(${vendor}/journeys/demand.json: what the applications call), its decision table (journeys/decisions.json), and the
|
|
45
|
+
vendor's own documentation the spec cites.
|
|
46
|
+
|
|
47
|
+
What you judge (in ${vendor}/journeys/), each with the sha256 your verdict names:
|
|
48
|
+
${judged.map((j) => `- ${j.file} (${j.what}): sha256 ${sha(join(journeys, j.file))}`).join('\n')}
|
|
49
|
+
|
|
50
|
+
What a judge rejects (docs/contributing/architecture.md, "What a judge rejects"):
|
|
51
|
+
${rejects}
|
|
52
|
+
|
|
53
|
+
For each file write its verdict beside it, as JSON:
|
|
54
|
+
${judged.map((j) => `- ${j.verdict}`).join('\n')}
|
|
55
|
+
{
|
|
56
|
+
"judge": { "agent": "<what you are: your runtime and model>", "name": "<your author name or reviewer role>" },
|
|
57
|
+
"authors": ${JSON.stringify(authors)},
|
|
58
|
+
"sha256": "<the judged file's sha256 above>",
|
|
59
|
+
"judgedAt": "<today, YYYY-MM-DD>",
|
|
60
|
+
"plausible": <true only when no ruling stands>,
|
|
61
|
+
"rulings": [ { "id": "<short name>", "steps": [<step indexes, or acts>], "barItem": "<the list's item>", "reason": "<what is wrong, citing the step and the vendor's page>", "correction": "<what the author changes>" } ]
|
|
62
|
+
}
|
|
63
|
+
A correction naming a wait gives it as a life writes one: one number and one unit (\`915m\`, \`3d\`). A verdict with any ruling is not plausible. The authors apply every ruling; this is the one judging, and the verdict stands through their changes.`);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { parseExactJson } from '@volter/world-core/exact-json';
|
|
2
|
+
// The walks `life-coverage.ts` measures: one pack's customer life, walked with its beats on
|
|
3
|
+
// the in-memory World, and the vendor's published examples replayed (journeys/vendor-examples.json, vendor-examples.ts)
|
|
4
|
+
// on a World of their own: a line only the vendor's own example reaches is reached for the reason the vendor gives. Bun measures coverage only inside `bun test`, so the measurement runs the walk through
|
|
5
|
+
// a test file life-coverage writes, which registers them (coverageWalks), and reads the lcov it leaves; the walk holding
|
|
6
|
+
// is its precondition, not what it measures. This module is no test file itself, so a published package holds it.
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { listActions, observeTransitions, stateOf, type Actor, type DerivedManifest, type Transition } from '@volter/world-core/runtime';
|
|
10
|
+
import { inMemoryWorld, walk, type Failure, usePacks } from '../walk/behavior-journey.ts';
|
|
11
|
+
import { lanesOf, shapeChecker } from '../walk/shape.ts';
|
|
12
|
+
import { sharingLanesIn } from '../lanes.ts';
|
|
13
|
+
|
|
14
|
+
/** Registers, in the `bun test` run that imports it, the walks of `vendor` in the pack repository at `PACKS`. */
|
|
15
|
+
export async function coverageWalks(PACKS: string, vendor: string): Promise<void> {
|
|
16
|
+
const { expect, test } = await import('bun:test');
|
|
17
|
+
usePacks(PACKS);
|
|
18
|
+
const sharingLanes = (v: string): string[] => (v.includes('/') ? [] : sharingLanesIn(join(PACKS, v), v));
|
|
19
|
+
|
|
20
|
+
const journeys = join(PACKS, vendor, 'journeys');
|
|
21
|
+
const walks = [{ file: 'customer-life.json', beats: true }, { file: 'vendor-examples.json', beats: false }].filter((w) => existsSync(join(journeys, w.file)));
|
|
22
|
+
const seen = new Set<string>();
|
|
23
|
+
const undeclared = new Set<string>();
|
|
24
|
+
|
|
25
|
+
for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async () => {
|
|
26
|
+
const life = parseExactJson(readFileSync(join(journeys, w.file), 'utf8'));
|
|
27
|
+
const world = inMemoryWorld(`coverage-${vendor}-${n}`);
|
|
28
|
+
const failures: Failure[] = [];
|
|
29
|
+
// each declared transition the life moved through or was refused by, named by resource, field and its place
|
|
30
|
+
const { manifest } = (await import(join(PACKS, vendor, 'src', 'manifest.ts'))) as { manifest: DerivedManifest };
|
|
31
|
+
// the lanes over the vendor's state its serve factory serves are walked by this life, their machines with the vendor's
|
|
32
|
+
// ("Pack layout"); two manifests naming one resource's field would collide in the keys below, so that refuses
|
|
33
|
+
const laneManifests = await Promise.all(sharingLanes(vendor).map(async (l) => ((await import(join(PACKS, vendor, l, 'src', 'manifest.ts'))) as { manifest: DerivedManifest }).manifest));
|
|
34
|
+
// a vendor of lanes' root (a VendorManifest) declares no resources of its own: its lanes do
|
|
35
|
+
const allResources = [manifest, ...laneManifests].flatMap((mf) => Object.entries(mf.resources ?? {}));
|
|
36
|
+
const machines = new Map<string, unknown>();
|
|
37
|
+
for (const [resource, decl] of allResources) for (const [field, machine] of Object.entries(decl.state ?? {})) {
|
|
38
|
+
const name = `${resource}.${field}`;
|
|
39
|
+
const previous = machines.get(name);
|
|
40
|
+
// Lanes sharing one vendor import the same state declaration; distinct declarations still collide.
|
|
41
|
+
if (previous && previous !== machine) throw new Error(`${vendor}: ${name} is declared by two manifests (the vendor's and a lane's)`);
|
|
42
|
+
machines.set(name, machine);
|
|
43
|
+
}
|
|
44
|
+
const where = new Map<Transition, Array<{ at: string; field: string; type: string; service: string }>>();
|
|
45
|
+
for (const mf of [manifest, ...laneManifests]) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
46
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) m.transitions.forEach((t, i) => {
|
|
47
|
+
const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
|
|
48
|
+
const places = where.get(t) ?? [];
|
|
49
|
+
if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service)) where.set(t, [...places, place]);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const moves = new Map<string, Array<{ transition: Transition; from: string; field: string; actor: Actor; subject: string }>>();
|
|
53
|
+
const subjectKey = (service: string, type: string, id: string): string => JSON.stringify([service, type, id]);
|
|
54
|
+
observeTransitions((_decl, t, outcome, from, entry) => {
|
|
55
|
+
const places = where.get(t) ?? [];
|
|
56
|
+
if (outcome === 'refused') {
|
|
57
|
+
// A failed check on an already committed write is no refusal the application received.
|
|
58
|
+
if (entry) return;
|
|
59
|
+
const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
|
|
60
|
+
if (groups.size === 1) for (const p of places) seen.add(`${p.at}|refused|${from}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// a move the machine allowed on a step with no write behind it (a line protocol's session, whose stage is the
|
|
64
|
+
// connection's): reached, as a refusal given is, where its declaration is unambiguous
|
|
65
|
+
if (!entry) {
|
|
66
|
+
const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
|
|
67
|
+
if (groups.size === 1) for (const p of places) seen.add(`${p.at}|moved|${from}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if ((t.actor ?? 'api') !== entry.actor) return;
|
|
71
|
+
const { action, field, actor } = entry;
|
|
72
|
+
const matching = places.filter((p) => p.service === action.service && p.type === action.subject.type && p.field === field);
|
|
73
|
+
if (!matching.length) return;
|
|
74
|
+
for (const p of matching) seen.add(`${p.at}|moved|${from}`);
|
|
75
|
+
const on = moves.get(action.id) ?? [];
|
|
76
|
+
on.push({ transition: t, from, field, actor, subject: subjectKey(action.service, action.subject.type, action.subject.id) });
|
|
77
|
+
moves.set(action.id, on);
|
|
78
|
+
});
|
|
79
|
+
const root = world.root();
|
|
80
|
+
// what each step was answered, by the spec operation it called: a refusal is seen at the wire, whichever code gave it
|
|
81
|
+
const shape = await shapeChecker(join(PACKS, vendor));
|
|
82
|
+
const answered: Array<{ op: string; text: string; body: unknown }> = [];
|
|
83
|
+
try {
|
|
84
|
+
await walk(life, root, failures, { beats: w.beats, hooks: { after: (step, path, answer) => {
|
|
85
|
+
// a line step's refusals are the machine's own (observeTransitions); a request's are read off its answer
|
|
86
|
+
const op = 'method' in step ? shape?.operationOf(step.method, path, { ...(life.headers ?? {}), ...(step.headers ?? {}) })?.op.id : undefined;
|
|
87
|
+
if (op) answered.push({ op, text: JSON.stringify(answer.body ?? ''), body: answer.body });
|
|
88
|
+
} } });
|
|
89
|
+
// a declared refusal is reached when a step on its operation was answered with it: its code, or its message
|
|
90
|
+
// up to the first placeholder
|
|
91
|
+
// a code is a value of the answer (X's `89` at errors[].code), never a run of digits inside another value (a token)
|
|
92
|
+
const holds = (value: unknown, code: string): boolean => (value !== null && typeof value === 'object'
|
|
93
|
+
? Object.values(value as Record<string, unknown>).some((v) => holds(v, code))
|
|
94
|
+
: (typeof value === 'string' || typeof value === 'number') && String(value) === code);
|
|
95
|
+
const answers = (a: { text: string; body: unknown }, r: { code?: string | number; message: string }): boolean =>
|
|
96
|
+
r.code !== undefined ? holds(a.body, String(r.code)) : a.text.includes(r.message.split('{')[0]!.trim());
|
|
97
|
+
for (const [resource, decl] of allResources) {
|
|
98
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) {
|
|
99
|
+
m.transitions.forEach((t, i) => {
|
|
100
|
+
if (!t.operation) return;
|
|
101
|
+
const on = answered.filter((a) => a.op === t.operation);
|
|
102
|
+
if (t.refusal && on.some((a) => answers(a, t.refusal!))) seen.add(`${resource}.${field}#${i}|refused|*`);
|
|
103
|
+
for (const [from, r] of Object.entries(t.refusals ?? {})) if (on.some((a) => answers(a, r))) seen.add(`${resource}.${field}#${i}|refused|${from}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// every change the life made to a declared state field, from the World's log in order: a move is the declared
|
|
108
|
+
// transition that allows it (from its state to the next, by its operation when both name one); a change no
|
|
109
|
+
// transition allows is an undeclared move
|
|
110
|
+
const fieldsOf = new Map<string, Array<[string, string, (typeof manifest.resources)[string]['state'] extends infer S ? S extends Record<string, infer F> ? F : never : never]>>();
|
|
111
|
+
for (const mf of [manifest, ...laneManifests]) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
112
|
+
const key = JSON.stringify([mf.service ?? mf.vendor, decl.storedAs ?? resource]);
|
|
113
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) fieldsOf.set(key, [...(fieldsOf.get(key) ?? []), [resource, field, m]]);
|
|
114
|
+
}
|
|
115
|
+
const current = new Map<string, Record<string, unknown>>();
|
|
116
|
+
// the services the life wrote: the vendor's, or its lanes' (a vendor of lanes' root names none of its own)
|
|
117
|
+
const services = [...new Set([manifest, ...laneManifests].map((mf) => mf.service).filter((x): x is string => typeof x === 'string'))];
|
|
118
|
+
if (!services.length) services.push(manifest.vendor);
|
|
119
|
+
for (const a of services.flatMap((service) => listActions(service, root))) {
|
|
120
|
+
if (a.op !== 'set' || !a.fields) continue;
|
|
121
|
+
const key = subjectKey(a.service, a.subject.type, a.subject.id);
|
|
122
|
+
const prev = current.get(key);
|
|
123
|
+
const next = { ...(prev ?? {}), ...(a.fields as Record<string, unknown>) };
|
|
124
|
+
current.set(key, next);
|
|
125
|
+
if (!prev) continue;
|
|
126
|
+
const opId = typeof a.input?.operationId === 'string' ? a.input.operationId : undefined;
|
|
127
|
+
// resources stored as one type (a response the beta and the GA API both serve) share its fields: a
|
|
128
|
+
// change is declared when any of their machines allows it
|
|
129
|
+
const changed = new Map<string, { declared: boolean; name: string }>();
|
|
130
|
+
const valueOf = (row: Record<string, unknown>, field: string): unknown => field.split('.').reduce<unknown>((value, part) => value && typeof value === 'object' ? (value as Record<string, unknown>)[part] : undefined, row);
|
|
131
|
+
for (const [resource, field, m] of fieldsOf.get(JSON.stringify([a.service, a.subject.type])) ?? []) {
|
|
132
|
+
if (!(field.split('.')[0]! in (a.fields as Record<string, unknown>))) continue;
|
|
133
|
+
const from = stateOf(m, valueOf(prev, field));
|
|
134
|
+
const to = stateOf(m, valueOf(next, field));
|
|
135
|
+
if (from === to) continue;
|
|
136
|
+
const allows = (t: (typeof m.transitions)[number], at: string): boolean => (moves.get(a.id) ?? []).some((move) =>
|
|
137
|
+
move.transition === t && move.from === at && move.field === field && move.actor === (t.actor ?? 'api') &&
|
|
138
|
+
move.subject === subjectKey(a.service, a.subject.type, a.subject.id));
|
|
139
|
+
const i = m.transitions.findIndex((t) => allows(t, from) && (t.to ?? from) === to);
|
|
140
|
+
if (i >= 0) seen.add(`${resource}.${field}#${i}|moved|${from}`);
|
|
141
|
+
// one write may carry several declared moves the machine walked (a batch the vendor works from
|
|
142
|
+
// validating through to completed as a read first looks); each step was observed as it was asked
|
|
143
|
+
const reached = new Set([from]);
|
|
144
|
+
for (let grew = true; grew && !reached.has(to);) {
|
|
145
|
+
grew = false;
|
|
146
|
+
for (const at of [...reached]) for (const t of m.transitions) if (allows(t, at) && t.to && !reached.has(t.to)) { reached.add(t.to); grew = true; }
|
|
147
|
+
}
|
|
148
|
+
const was = changed.get(field);
|
|
149
|
+
changed.set(field, { declared: (was?.declared ?? false) || reached.has(to), name: was?.name ?? `${resource}.${field}: ${from} → ${to} by ${opId ?? a.operation ?? '(unnamed)'}` });
|
|
150
|
+
}
|
|
151
|
+
for (const c of changed.values()) if (!c.declared) undeclared.add(c.name);
|
|
152
|
+
}
|
|
153
|
+
} finally { world.restore(); observeTransitions(undefined); }
|
|
154
|
+
// each walk adds what it reached; the last one written holds them all
|
|
155
|
+
if (process.env.LIFE_TRANSITIONS) writeFileSync(process.env.LIFE_TRANSITIONS, JSON.stringify({ seen: [...seen].sort(), undeclared: [...undeclared].sort() }));
|
|
156
|
+
expect(failures.filter((f) => f.beat !== 'SHAPE')).toEqual([]);
|
|
157
|
+
// a walk's time grows with its steps and the state they build (Stripe's 609 example steps took 70 s uninstrumented,
|
|
158
|
+
// measured 2026-09-26, each request catching the World up over every row it holds)
|
|
159
|
+
}, 180_000);
|
|
160
|
+
|
|
161
|
+
// A pack's lanes reach its code through the pack's router (a management-API life making a database branch the data
|
|
162
|
+
// plane then serves): their lives and examples, walked here too, count toward the pack's code, not its state logic
|
|
163
|
+
// (each lane's machines are its own, measured when the lane is). A lane is measured alone, with no lanes of its own.
|
|
164
|
+
const PACK = join(PACKS, vendor);
|
|
165
|
+
const laneWalks = vendor.includes('/') ? [] : lanesOf(PACK).flatMap((dir) => [{ file: 'customer-life.json', beats: true }, { file: 'vendor-examples.json', beats: false }]
|
|
166
|
+
.filter((w) => existsSync(join(dir, 'journeys', w.file))).map((w) => ({ ...w, path: join(dir, 'journeys', w.file), lane: dir.split('/').slice(-2).join('/') })));
|
|
167
|
+
for (const [n, w] of laneWalks.entries()) test(`${vendor}: ${w.lane} ${w.file} walks for the pack's code`, async () => {
|
|
168
|
+
const world = inMemoryWorld(`coverage-${vendor}-lane-${n}`);
|
|
169
|
+
const failures: Failure[] = [];
|
|
170
|
+
try { await walk(parseExactJson(readFileSync(w.path, 'utf8')), world.root(), failures, { beats: w.beats }); } finally { world.restore(); }
|
|
171
|
+
expect(failures.filter((f) => f.beat !== 'SHAPE')).toEqual([]);
|
|
172
|
+
}, 180_000);
|
|
173
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// life-coverage — how much of a pack's hand-written code and declared state logic its customer life and its vendor's
|
|
3
|
+
// published examples (vendor-examples.ts) reach
|
|
4
|
+
// (docs/contributing/architecture.md, "Protocol 3", Coverage). Hand-written is every file under the pack's src/ on disk
|
|
5
|
+
// (its sharing lanes' too), but for the generated core (src/generated/) and the command line (src/cli.ts), which no
|
|
6
|
+
// World's fetch runs; a file of data (seed.ts, budget.ts) is in scope and has nothing to reach. State logic is every declared transition and every refusal its guard gives, read from the World's
|
|
7
|
+
// log and the answers. The standard is all of it: each item is reached, or recorded in journeys/unreachable.json with
|
|
8
|
+
// the reason no World reaches it; what is neither is missing, and a pack is done when nothing is missing.
|
|
9
|
+
//
|
|
10
|
+
// The instrument's limits, measured on Bun 1.3.10 (`bun test --coverage --coverage-reporter=lcov`): a line is counted
|
|
11
|
+
// by its first statement, so both sides of `a ? b : c` count once either runs; the last line of a block that never ran
|
|
12
|
+
// can read as run once (a function `if (x > 0) { return x; } const y = x * 7; return y - 1;` called with 5 marked
|
|
13
|
+
// `return y - 1` hit while `const y` read 0), so a lone unrun statement at a block's end can be counted reached; and a
|
|
14
|
+
// function declared below module-scope constants it follows was seen to mark every line above it unrun (aws's
|
|
15
|
+
// schedule.ts, until the function moved above them). A function that never runs still reads 0 on its first line, so
|
|
16
|
+
// an unreached function is never counted reached.
|
|
17
|
+
//
|
|
18
|
+
// Whether a recorded declaration ran in part is printed as advice, never counted: a `function`'s first line ran, or a
|
|
19
|
+
// body line before its last did, but measured on planetscale (life, 2026-09-26) an arrow constant read 0 on its first
|
|
20
|
+
// line and 140 on a property line inside it (SHOW_TABLES_FIELD, which nothing in that life calls), while arrow
|
|
21
|
+
// functions every UPDATE and GROUP BY runs (targetRows, computeAggregate) read 0 on their first line. The judge reads
|
|
22
|
+
// the advice; a record whose declaration did run in part is split, its unrun branch recorded as its own function.
|
|
23
|
+
//
|
|
24
|
+
// A walk that fails, and a file in scope (the pack's src/ on disk, its sharing lanes' too) that declares code yet
|
|
25
|
+
// measures no line, stop the measurement: neither is a file with nothing to reach.
|
|
26
|
+
//
|
|
27
|
+
// Usage: twin-standard coverage <vendor> [--write | --out <file>] [--prune] (from the pack repository's root; twin-world's
|
|
28
|
+
// bun scripts/life-coverage.ts runs the same) (--prune removes the stale records) (--write keeps journeys/coverage.json)
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import ts from 'typescript';
|
|
31
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
32
|
+
import { tmpdir } from 'node:os';
|
|
33
|
+
import { join, normalize, resolve } from 'node:path';
|
|
34
|
+
import type { DerivedManifest } from '@volter/world-core/runtime';
|
|
35
|
+
import { functionIndex, stateCoverage } from '../walk/journey-kit.ts';
|
|
36
|
+
import { sharingLanesIn } from '../lanes.ts';
|
|
37
|
+
import { packsRoot, toolPath } from './packs-root.ts';
|
|
38
|
+
const PACKS = packsRoot();
|
|
39
|
+
const sharingLanes = (v: string): string[] => (v.includes('/') ? [] : sharingLanesIn(join(PACKS, v), v));
|
|
40
|
+
|
|
41
|
+
const vendor = process.argv[2];
|
|
42
|
+
if (!vendor || vendor.startsWith('-')) { console.error('usage: twin-standard coverage <vendor> [--write | --out <file>] [--prune] (from the pack repository\'s root)'); process.exit(1); }
|
|
43
|
+
const PKG = join(PACKS, vendor);
|
|
44
|
+
// An unsuccessful current walk replaces stale retained coverage with explicit failed measurement evidence.
|
|
45
|
+
const invalidMeasurement = (error: string): never => {
|
|
46
|
+
const outAt = process.argv.includes('--out') ? process.argv[process.argv.indexOf('--out') + 1] : undefined;
|
|
47
|
+
if (process.argv.includes('--write') || outAt) writeFileSync(outAt ?? join(PKG, 'journeys', 'coverage.json'), `${JSON.stringify({ vendor, measured: false, error }, null, 2)}\n`);
|
|
48
|
+
console.error(error); process.exit(1);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// a lane of a multi-API vendor (`aws/secretsmanager`) is named with a slash
|
|
52
|
+
const dir = mkdtempSync(join(tmpdir(), `life-coverage-${vendor.replaceAll('/', '-')}-`));
|
|
53
|
+
const transitionsPath = join(dir, 'transitions.json');
|
|
54
|
+
// Bun measures coverage only inside `bun test`: a test file of the scratch directory's registers the walks
|
|
55
|
+
// (life-coverage-walk.ts), which a published package holds as a module, never as a test file of its own
|
|
56
|
+
const walkTest = join(dir, 'life-coverage.walk.test.ts');
|
|
57
|
+
writeFileSync(walkTest, `import { coverageWalks } from ${JSON.stringify(toolPath('life-coverage-walk'))};\nawait coverageWalks(${JSON.stringify(PACKS)}, ${JSON.stringify(vendor)});\n`);
|
|
58
|
+
const child = spawnSync(process.execPath, ['test', '--coverage', '--coverage-reporter=lcov', `--coverage-dir=${dir}`, walkTest], {
|
|
59
|
+
// run from the pack repository, so the pack's files lie under the walk's directory and bun measures them
|
|
60
|
+
cwd: PACKS, stdio: ['ignore', 'ignore', 'inherit'], env: { ...process.env, LIFE_TRANSITIONS: transitionsPath },
|
|
61
|
+
});
|
|
62
|
+
const lcovPath = join(dir, 'lcov.info');
|
|
63
|
+
if (!existsSync(lcovPath)) { invalidMeasurement(`${vendor}: the walk left no coverage`); }
|
|
64
|
+
// a walk that failed measured only what loaded before it failed: its coverage is no measure of the pack (a vendor of
|
|
65
|
+
// lanes' walk once threw before its fetch loaded, and the manifests alone read as all of its code, all of it run)
|
|
66
|
+
if (child.status !== 0) { invalidMeasurement(`${vendor}: the coverage walk failed (journey instrument exited ${child.status}); its coverage measures nothing`); }
|
|
67
|
+
|
|
68
|
+
/** What the pack records no World reaches: code by file and declaration, state logic by name, each with its reason. */
|
|
69
|
+
type Unreachable = { code?: Array<{ file: string; declarations: string[]; world?: string; why: string }>; state?: Array<{ item: string; world?: string; why: string }> };
|
|
70
|
+
/** Why no World reaches it (architecture, "Coverage"), each record naming one in `world`: a record
|
|
71
|
+
* naming none, or another, is no exemption and its code counts as missing. */
|
|
72
|
+
const WORLDS = new Set(['host-process', 'door', 'configured-otherwise', 'hostile-peer', 'bug-guard', 'vendor-backed', 'sealed-refusal']);
|
|
73
|
+
const ruled = (u: { world?: string }): boolean => u.world !== undefined && WORLDS.has(u.world);
|
|
74
|
+
const unreachablePath = join(PKG, 'journeys', 'unreachable.json');
|
|
75
|
+
const unreachable = existsSync(unreachablePath) ? (JSON.parse(readFileSync(unreachablePath, 'utf8')) as Unreachable) : {};
|
|
76
|
+
const recordedCode = (file: string, fn: string | undefined): boolean => (unreachable.code ?? []).some((u) => ruled(u) && u.file === file && u.declarations.includes(fn ?? '(module scope)'));
|
|
77
|
+
const recordedState = (item: string): boolean => (unreachable.state ?? []).some((u) => ruled(u) && u.item === item);
|
|
78
|
+
|
|
79
|
+
// ── code ─────────────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
const lcov = readFileSync(lcovPath, 'utf8');
|
|
81
|
+
const prefix = `${vendor}/src/`;
|
|
82
|
+
// the lanes over this vendor's state that its serve factory serves ("Pack layout"): their code and states are this
|
|
83
|
+
// life's to reach, measured here beside the vendor's own
|
|
84
|
+
const sharedLanes = sharingLanes(vendor);
|
|
85
|
+
const lanePrefixes = sharedLanes.map((l) => `${vendor}/${l}/src/`);
|
|
86
|
+
const inScope = (f: string): string | undefined => [prefix, ...lanePrefixes].find((p) => f.startsWith(p) && !f.startsWith(`${p}generated/`));
|
|
87
|
+
// the scope is the pack's hand-written files ON DISK (its src/ and its sharing lanes'), never the files the walk happened to
|
|
88
|
+
// load: a screen not wired into fetch.ts or a family missing from the handler map is in scope and measures nothing
|
|
89
|
+
const onDisk = (rel: string): string[] => {
|
|
90
|
+
const at = join(PACKS, rel);
|
|
91
|
+
if (!existsSync(at)) return [];
|
|
92
|
+
return readdirSync(at, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? onDisk(`${rel}${e.name}/`) : /\.tsx?$/.test(e.name) ? [`${rel}${e.name}`] : []));
|
|
93
|
+
};
|
|
94
|
+
const handWritten = [prefix, ...lanePrefixes].flatMap(onDisk)
|
|
95
|
+
.filter((f) => inScope(f) !== undefined && !/\.(test|uitest)\.tsx?$/.test(f) && !/\.d\.ts$/.test(f))
|
|
96
|
+
// the command line a person runs the twin with (a unit's src/cli.ts) is code no World's fetch runs; everything else is
|
|
97
|
+
// in scope, a unit's budget.ts (D8's data) and seed.ts (data) passing as data below
|
|
98
|
+
.filter((f) => !/^[^/]+(?:\/[^/]+)?\/src\/cli\.ts$/.test(f));
|
|
99
|
+
const readSource = (file: string): string | undefined => (existsSync(join(PACKS, file)) ? readFileSync(join(PACKS, file), 'utf8') : undefined);
|
|
100
|
+
const measuredFiles = stateCoverage(lcov, handWritten, readSource);
|
|
101
|
+
// a file in scope that measures no line is not dropped: one that declares code and reads 0 lines found was not loaded by
|
|
102
|
+
// the walk or not measured, an error of the measurement, never a file with nothing to reach. Data is not code: a module
|
|
103
|
+
// of only imports, re-exports, types and constants whose values are data (literals, with no function, call, `new`,
|
|
104
|
+
// accessor or tagged template in them, as the grade holds a manifest's data: twin-standard's manifest-data) has nothing a
|
|
105
|
+
// walk could run, loaded or not: `semantics/seed.ts`'s `seed: SeedCall[]`, which `volter-world init` copies into a World
|
|
106
|
+
// and sends as requests, never a module the twin's fetch loads. A function, a class, an enum or any other statement is
|
|
107
|
+
// code, so no handler hides as data.
|
|
108
|
+
const isCode = (n: ts.Node): boolean => ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n)
|
|
109
|
+
|| ts.isClassExpression(n) || ts.isTaggedTemplateExpression(n) || ts.isGetAccessor(n) || ts.isSetAccessor(n) || ts.isMethodDeclaration(n)
|
|
110
|
+
|| ts.forEachChild(n, isCode) === true;
|
|
111
|
+
const declaresCode = (file: string, text: string): boolean => ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS).statements.some((st) => {
|
|
112
|
+
if (ts.isImportDeclaration(st) || ts.isImportEqualsDeclaration(st) || ts.isTypeAliasDeclaration(st) || ts.isInterfaceDeclaration(st)) return false;
|
|
113
|
+
if (ts.isExportDeclaration(st)) return false;
|
|
114
|
+
// an ambient declaration (`declare const x: T`) is a type, nothing that runs
|
|
115
|
+
if (ts.canHaveModifiers(st) && ts.getModifiers(st)?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword)) return false;
|
|
116
|
+
// `export default <data>` is data as a constant's value is
|
|
117
|
+
if (ts.isExportAssignment(st)) return isCode(st.expression);
|
|
118
|
+
if (ts.isVariableStatement(st)) return st.declarationList.declarations.some((d) => !d.initializer || isCode(d.initializer));
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
const unmeasured = measuredFiles.filter((c) => c.lines.found === 0 && declaresCode(c.file, readSource(c.file) ?? ''));
|
|
122
|
+
if (unmeasured.length) { invalidMeasurement(`${vendor}: ${unmeasured.length} file(s) in the pack's scope declare code and measure no line (the walk did not load them, or bun did not measure them): ${unmeasured.map((c) => c.file).join(', ')}`); }
|
|
123
|
+
const files = measuredFiles.filter((c) => c.lines.found > 0).map((c) => {
|
|
124
|
+
// a lane's file is named from the vendor's src/ (`../fapi/src/semantics/client.ts`), as unreachable.json records it
|
|
125
|
+
const file = c.file.startsWith(prefix) ? c.file.slice(prefix.length) : `../${c.file.slice(`${vendor}/`.length)}`;
|
|
126
|
+
const recorded = c.never.filter((n) => recordedCode(file, n.fn));
|
|
127
|
+
const missing = c.never.filter((n) => !recordedCode(file, n.fn));
|
|
128
|
+
const size = (ns: typeof c.never): number => ns.reduce((s, n) => s + n.to - n.from + 1, 0);
|
|
129
|
+
return { file, lines: c.lines, recorded: size(recorded), missing, missingLines: size(missing) };
|
|
130
|
+
});
|
|
131
|
+
const total = files.reduce((t, c) => ({ found: t.found + c.lines.found, hit: t.hit + c.lines.hit, recorded: t.recorded + c.recorded, missing: t.missing + c.missingLines }), { found: 0, hit: 0, recorded: 0, missing: 0 });
|
|
132
|
+
|
|
133
|
+
// ── state logic ──────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
const { manifest } = (await import(join(PKG, 'src', 'manifest.ts'))) as { manifest: DerivedManifest };
|
|
135
|
+
const laneManifests = await Promise.all(sharedLanes.map(async (l) => ((await import(join(PKG, l, 'src', 'manifest.ts'))) as { manifest: DerivedManifest }).manifest));
|
|
136
|
+
const observed = existsSync(transitionsPath) ? (JSON.parse(readFileSync(transitionsPath, 'utf8')) as { seen: string[]; undeclared: string[] }) : { seen: [], undeclared: [] };
|
|
137
|
+
const seen = new Set<string>(observed.seen);
|
|
138
|
+
const was = (at: string, outcome: string, from?: string): boolean => [...seen].some((s) => s.startsWith(`${at}|${outcome}|`) && (from === undefined || s.endsWith(`|${from}`)));
|
|
139
|
+
const state: Array<{ item: string; reached: boolean }> = [];
|
|
140
|
+
for (const [resource, decl] of [manifest, ...laneManifests].flatMap((mf) => Object.entries(mf.resources ?? {}))) {
|
|
141
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) {
|
|
142
|
+
// transitions of one operation from the same states refuse as one rule (a confirm that lands on one of
|
|
143
|
+
// four outcomes refuses with the first of its four identical refusals), so their refusal is one item
|
|
144
|
+
const refusalOf = new Map<string, { item: string; reached: boolean }>();
|
|
145
|
+
m.transitions.forEach((t, i) => {
|
|
146
|
+
const at = `${resource}.${field}#${i}`;
|
|
147
|
+
const name = `${resource}.${field}: ${t.operation ?? t.actor ?? 'api'} ${t.from === '*' ? '*' : t.from.join('|')} → ${t.to ?? '(stays)'}`;
|
|
148
|
+
if (!(t.to === undefined && t.refusal !== undefined)) state.push({ item: `move ${name}`, reached: was(at, 'moved') });
|
|
149
|
+
const refusals = [...(t.refusal ? [['', t.refusal] as const] : []), ...Object.entries(t.refusals ?? {})];
|
|
150
|
+
for (const [from, r] of refusals) {
|
|
151
|
+
const key = JSON.stringify([t.operation ?? t.actor, t.from, from, r]);
|
|
152
|
+
const known = refusalOf.get(key);
|
|
153
|
+
const reached = from ? was(at, 'refused', from) : was(at, 'refused');
|
|
154
|
+
if (known) { known.reached ||= reached; continue; }
|
|
155
|
+
const item = { item: `refusal ${name}${from ? ` from ${from}` : ''}`, reached };
|
|
156
|
+
refusalOf.set(key, item);
|
|
157
|
+
state.push(item);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const stateMissing = state.filter((s) => !s.reached && !recordedState(s.item)).map((s) => s.item);
|
|
163
|
+
|
|
164
|
+
// ── stale records ────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// a record is a claim that no World reaches something: one that names a declaration, a file or a state item the pack
|
|
166
|
+
// no longer has, or one the walks now reach, is false, and counts as missing until it is corrected
|
|
167
|
+
const stale: string[] = [];
|
|
168
|
+
/** Records whose declaration may have run in part: to read, and split where it did (architecture.md, Coverage). */
|
|
169
|
+
const partlyRun: string[] = [];
|
|
170
|
+
const keptCode: NonNullable<Unreachable['code']> = [];
|
|
171
|
+
for (const u of unreachable.code ?? []) {
|
|
172
|
+
const keep: string[] = [];
|
|
173
|
+
const source = readSource(`${prefix}${u.file}`);
|
|
174
|
+
const names = new Set(source === undefined ? [] : functionIndex(source).map((f) => f.name));
|
|
175
|
+
const measured = files.find((f) => f.file === u.file);
|
|
176
|
+
const coverage = stateCoverage(lcov, [normalize(`${prefix}${u.file}`)], readSource)[0];
|
|
177
|
+
const unrun = new Set([...(measured?.missing ?? []), ...(coverage?.never ?? [])].map((n) => n.fn ?? '(module scope)'));
|
|
178
|
+
// a record is by declaration: one part of whose body ran is not unreachable, its unrun branch is (architecture.md,
|
|
179
|
+
// Coverage: "the branch moves into its own function first")
|
|
180
|
+
const partly = new Set(coverage?.ran ?? []);
|
|
181
|
+
for (const d of u.declarations) {
|
|
182
|
+
if (source === undefined) stale.push(`${u.file}: no such file`);
|
|
183
|
+
else if (d !== '(module scope)' && !names.has(d)) stale.push(`${u.file}: no declaration ${d}`);
|
|
184
|
+
else if (measured && !unrun.has(d)) stale.push(`${u.file}: ${d} is reached`);
|
|
185
|
+
else {
|
|
186
|
+
// advice, not a count: bun's line hits cannot always tell a declaration that ran in part (see the header)
|
|
187
|
+
if (measured && partly.has(d)) partlyRun.push(`${u.file}: ${d}`);
|
|
188
|
+
keep.push(d);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (keep.length) keptCode.push({ ...u, declarations: keep });
|
|
192
|
+
}
|
|
193
|
+
const keptState: NonNullable<Unreachable['state']> = [];
|
|
194
|
+
for (const u of unreachable.state ?? []) {
|
|
195
|
+
if (!state.some((s) => s.item === u.item)) stale.push(`state: no item ${u.item}`);
|
|
196
|
+
else if (state.some((s) => s.item === u.item && s.reached)) stale.push(`state: ${u.item} is reached`);
|
|
197
|
+
else keptState.push(u);
|
|
198
|
+
}
|
|
199
|
+
// `--prune` removes exactly the false records: a record removed wrongly shows again as missing, so pruning hides nothing
|
|
200
|
+
if (stale.length && process.argv.includes('--prune')) {
|
|
201
|
+
writeFileSync(unreachablePath, `${JSON.stringify({ ...unreachable, code: keptCode, state: keptState }, null, 2)}\n`);
|
|
202
|
+
console.log(`${vendor}: pruned ${stale.length} stale records from journeys/unreachable.json`);
|
|
203
|
+
stale.length = 0;
|
|
204
|
+
}
|
|
205
|
+
const stateRecorded = state.filter((s) => !s.reached && recordedState(s.item)).length;
|
|
206
|
+
|
|
207
|
+
// ── the report ───────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
const pct = (hit: number, found: number): string => (found ? `${((hit / found) * 100).toFixed(1)}%` : 'n/a');
|
|
209
|
+
const missing = (child.status === 0 ? 0 : 1) + total.missing + stateMissing.length + observed.undeclared.length + stale.length;
|
|
210
|
+
console.log(`${vendor}: ${missing === 0 ? 'meets the standard' : `${missing} missing`}${child.status === 0 ? '' : ' (the walk did not hold)'}`);
|
|
211
|
+
// a record whose reason is none the standard names exempts nothing: say which, so the author reaches it or names its World
|
|
212
|
+
const unruled = [...(unreachable.code ?? []).map((u) => `${u.file} ${u.declarations.join(', ')}`), ...(unreachable.state ?? []).map((u) => u.item)]
|
|
213
|
+
.filter((_, i) => !ruled([...(unreachable.code ?? []), ...(unreachable.state ?? [])][i]!));
|
|
214
|
+
if (unruled.length) console.log(` ${unruled.length} record(s) name no World of the standard (world: ${[...WORLDS].join(' | ')}), so they exempt nothing: ${unruled.slice(0, 6).join('; ')}${unruled.length > 6 ? ' …' : ''}`);
|
|
215
|
+
console.log(` code: the life and the vendor's examples run ${pct(total.hit, total.found)} of ${total.found} hand-written lines; ${total.recorded} recorded unreachable; ${total.missing} missing`);
|
|
216
|
+
console.log(` state: ${state.filter((s) => s.reached).length}/${state.length} declared moves and refusals reached; ${stateRecorded} recorded unreachable; ${stateMissing.length} missing; ${observed.undeclared.length} moves no declared transition allows`);
|
|
217
|
+
for (const u of observed.undeclared) console.log(` undeclared: ${u}`);
|
|
218
|
+
if (stale.length) console.log(` records: ${stale.length} stale`);
|
|
219
|
+
for (const r of stale) console.log(` stale ${r}`);
|
|
220
|
+
for (const s of stateMissing) console.log(` missing ${s}`);
|
|
221
|
+
if (partlyRun.length) console.log(` advice: ${partlyRun.length} recorded declaration(s) may have run in part (read each; split where it did):\n${partlyRun.map((p) => ` ${p}`).join('\n')}`);
|
|
222
|
+
for (const f of files.filter((c) => c.missingLines).sort((a, b) => b.missingLines - a.missingLines)) {
|
|
223
|
+
const byFn = new Map<string, number>();
|
|
224
|
+
for (const n of f.missing) byFn.set(n.fn ?? '(module scope)', (byFn.get(n.fn ?? '(module scope)') ?? 0) + n.to - n.from + 1);
|
|
225
|
+
console.log(` ${f.file} (${f.missingLines} lines): ${[...byFn].sort((a, b) => b[1] - a[1]).map(([fn, n]) => `${fn} ${n}`).join(', ')}`);
|
|
226
|
+
}
|
|
227
|
+
// `--write` keeps journeys/coverage.json; `--out <file>` writes the same report elsewhere (score-pack's own measurement,
|
|
228
|
+
// which leaves the checkout as it found it)
|
|
229
|
+
const outAt = process.argv.includes('--out') ? process.argv[process.argv.indexOf('--out') + 1] : undefined;
|
|
230
|
+
if (process.argv.includes('--write') || outAt) {
|
|
231
|
+
writeFileSync(outAt ?? join(PKG, 'journeys', 'coverage.json'), `${JSON.stringify({
|
|
232
|
+
vendor, missing, stale,
|
|
233
|
+
code: { lines: total.found, run: total.hit, recorded: total.recorded, missing: total.missing },
|
|
234
|
+
state: { declared: state.length, reached: state.filter((s) => s.reached).length, recorded: stateRecorded, missing: stateMissing, undeclared: observed.undeclared },
|
|
235
|
+
files: files.map((f) => ({ file: f.file, lines: f.lines, missing: f.missing.map((n) => ({ from: n.from, to: n.to, fn: n.fn })) })),
|
|
236
|
+
}, null, 2)}\n`);
|
|
237
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Where the contributor's tools (walk, examples, coverage, judge-brief) find the pack they are given: a vendor's directory
|
|
2
|
+
// under the pack repository they run from. A contributor runs them from their own repository's root (`twin-standard walk
|
|
3
|
+
// <vendor>`), so that root is the working directory; twin-world's scripts, which find Volter's pack repository beside
|
|
4
|
+
// their checkout, name it before loading a tool (setPacksRoot), and the kernel checkout a walk must measure
|
|
5
|
+
// (setKernelCheckout). Each tool reads both once, when it loads.
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
let packs = process.cwd();
|
|
9
|
+
let kernel: string | undefined;
|
|
10
|
+
|
|
11
|
+
/** The pack repository the tools read (`<root>/<vendor>/`). */
|
|
12
|
+
export const packsRoot = (): string => packs;
|
|
13
|
+
export function setPacksRoot(dir: string): void { packs = dir; }
|
|
14
|
+
|
|
15
|
+
/** The checkout whose kernel a walk measures, when one is required (twin-world's own scripts); a contributor's walk
|
|
16
|
+
* measures whichever kernel its repository resolves, and says which. */
|
|
17
|
+
export const kernelCheckout = (): string | undefined => kernel;
|
|
18
|
+
export function setKernelCheckout(dir: string): void { kernel = dir; }
|
|
19
|
+
|
|
20
|
+
/** A tool of this directory, by name, in the form this module was loaded as (source `.ts`, or the published `.js`). */
|
|
21
|
+
export const toolPath = (name: string): string => join(import.meta.dir, `${name}${import.meta.path.endsWith('.js') ? '.js' : '.ts'}`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Runs one contributor tool in a process of its own, as the tool that started it names the pack repository and the
|
|
2
|
+
// kernel checkout (packs-root.ts): bun run-tool.ts <packs root> <kernel checkout or ''> <tool path> <tool args…>
|
|
3
|
+
import { setKernelCheckout, setPacksRoot } from './packs-root.ts';
|
|
4
|
+
|
|
5
|
+
const [packs, checkout, tool, ...args] = process.argv.slice(2);
|
|
6
|
+
setPacksRoot(packs!);
|
|
7
|
+
if (checkout) setKernelCheckout(checkout);
|
|
8
|
+
// a tool is a script over process.argv: its first argument is its own
|
|
9
|
+
process.argv = [process.argv[0]!, tool!, ...args];
|
|
10
|
+
await import(tool!);
|