@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,264 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// score-pack — a Protocol 3 pack's report (docs/contributing/architecture.md, "Protocol 3", Scoring). One customer
|
|
3
|
+
// life per pack is walked through the pack's own fetch on the in-memory World, and the report says:
|
|
4
|
+
// - the life: whether a judge's plausible verdict names its current content (a verdict its author wrote is not one);
|
|
5
|
+
// - correctness: every check on every step (status, expectations, captures, SHAPE, the beats) and the life
|
|
6
|
+
// answering byte for byte the same on a second fresh World;
|
|
7
|
+
// - coverage: the hand-written code the life and the vendor's published examples run and the declared state logic
|
|
8
|
+
// they reach (life-coverage.ts), with what they never reach;
|
|
9
|
+
// - the vendor's published examples: replayed and judged (vendor-examples.ts);
|
|
10
|
+
// - breadth: the spec's operations (a line protocol's commands) by what serves them (a handler, the generated
|
|
11
|
+
// core, a legacy fallback, nothing).
|
|
12
|
+
// This is source-journey evidence, not installed readiness or pack completion. pack-done owns completion and consumes
|
|
13
|
+
// this report alongside the exact artifact's installed customer assessment in the one final run.
|
|
14
|
+
//
|
|
15
|
+
// Usage: twin-standard walk <vendor> [--write] [--report <path>] (from the pack repository's root; twin-world's
|
|
16
|
+
// bun scripts/score-pack.ts <vendor> runs the same)
|
|
17
|
+
// --write keeps journeys/score.json and journeys/coverage.json; --report writes the report and what it lacks for done
|
|
18
|
+
// to <path> as JSON and changes nothing in the checkout
|
|
19
|
+
import { parseExactJson } from '@volter/world-core/exact-json';
|
|
20
|
+
import { notIndependent } from "../verdict.js";
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
25
|
+
import { dirname, join, sep } from 'node:path';
|
|
26
|
+
import { inMemoryWorld, walk, withPinnedWorldDraws, usePacks } from "../walk/behavior-journey.js";
|
|
27
|
+
import { expandFills, waitMs } from "../walk/journey-kit.js";
|
|
28
|
+
import { vendorFetch, vendorStream } from "../walk/vendor-fetch.js";
|
|
29
|
+
import { existenceProbe } from "../walk/journey-kit.js";
|
|
30
|
+
import { kernelCheckout, packsRoot, toolPath } from "./packs-root.js";
|
|
31
|
+
const PACKS = packsRoot();
|
|
32
|
+
usePacks(PACKS);
|
|
33
|
+
const vendor = process.argv[2];
|
|
34
|
+
if (!vendor || vendor.startsWith('-')) {
|
|
35
|
+
console.error('usage: twin-standard walk <vendor> [--write] [--report <path>] (from the pack repository\'s root)');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
// The surface measured: the kernel the pack repository resolves, named in the report. twin-world's own scripts require
|
|
39
|
+
// it to be their checkout's (linked in development, decision 0029): a pack repository whose node_modules resolves
|
|
40
|
+
// @volter/* into another checkout scores the pack against someone else's kernel, no measurement of this one.
|
|
41
|
+
const kernel = (() => {
|
|
42
|
+
try {
|
|
43
|
+
return realpathSync(join(PACKS, 'node_modules', '@volter', 'world-core'));
|
|
44
|
+
}
|
|
45
|
+
catch { /* resolved through a parent's node_modules */ }
|
|
46
|
+
try {
|
|
47
|
+
let at = dirname(realpathSync(createRequire(join(PACKS, 'package.json')).resolve('@volter/world-core')));
|
|
48
|
+
while (!existsSync(join(at, 'package.json')) && dirname(at) !== at)
|
|
49
|
+
at = dirname(at);
|
|
50
|
+
return at;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return '(unresolved)';
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
const checkout = kernelCheckout();
|
|
57
|
+
if (checkout && !kernel.startsWith(realpathSync(checkout) + sep)) { // the platform's separator: '/' never prefixes a Windows path
|
|
58
|
+
console.error(`score-pack: @volter/world-core resolves to ${kernel}, outside this checkout (${realpathSync(checkout)}); link the pack repository's node_modules/@volter/* to this checkout's packages before scoring`);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
const PKG = join(PACKS, vendor);
|
|
62
|
+
const lifePath = join(PKG, 'journeys', 'customer-life.json');
|
|
63
|
+
if (!existsSync(lifePath)) {
|
|
64
|
+
console.error(`${vendor}: no journeys/customer-life.json`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
// ── the life and its verdict ─────────────────────────────────────────────────────────────────
|
|
68
|
+
const text = readFileSync(lifePath, 'utf8');
|
|
69
|
+
// its fills written out once, so the steps the walk hands back are the life's own (a sitting is found by position)
|
|
70
|
+
const life = expandFills(parseExactJson(text));
|
|
71
|
+
const verdictPath = join(PKG, 'journeys', 'customer-life.verdict.json');
|
|
72
|
+
const verdict = existsSync(verdictPath) ? parseExactJson(readFileSync(verdictPath, 'utf8')) : undefined;
|
|
73
|
+
// A verdict is a judge's in another session: one its author wrote (marked self-judged) rules nothing, however it reads.
|
|
74
|
+
// The dispatcher stamps the judge’s author name (`judge.agent`) and every author of the life
|
|
75
|
+
// (`authors`) consistently when it commits the verdict; a verdict without them,
|
|
76
|
+
// or whose judge is among the authors, is not independent.
|
|
77
|
+
// one judging per pack: the verdict stands, and the life's changes since (its rulings applied, a review's fixes, steps for
|
|
78
|
+
// coverage) keep it (architecture, "Writing a life", Judge)
|
|
79
|
+
const dependent = verdict ? notIndependent(verdict, life.author) : undefined;
|
|
80
|
+
const judged = life.steps.length === 0 || !verdict ? 'unjudged' : dependent ? dependent : verdict.plausible ? 'plausible' : 'rejected';
|
|
81
|
+
// ── correctness ──────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
const world = inMemoryWorld(`score-${vendor}`);
|
|
83
|
+
const failures = [];
|
|
84
|
+
await walk(life, world.root(), failures, { beats: true });
|
|
85
|
+
const answers = async () => {
|
|
86
|
+
const seen = [];
|
|
87
|
+
// the World's own draws (its seed, its keys) are the same in both: two Worlds differ only by what is beyond them (R9)
|
|
88
|
+
await withPinnedWorldDraws(() => walk(life, world.root(), [], { beats: false, hooks: { after: (step, p, a) => seen.push(`${step.name ?? ''}\u0000${p} ${a.status} ${JSON.stringify(a.body)}`) } }));
|
|
89
|
+
return seen;
|
|
90
|
+
};
|
|
91
|
+
const [first, second] = [await answers(), await answers()];
|
|
92
|
+
const flags = [];
|
|
93
|
+
// CRED: a life where steps act with different credentials carries no life-wide default one; a default leaks one actor's
|
|
94
|
+
// credential into every step that forgot its own (a judge found octocat's token on a server's call).
|
|
95
|
+
{
|
|
96
|
+
const auth = (h) => Object.entries(h ?? {}).find(([k]) => k.toLowerCase() === 'authorization')?.[1];
|
|
97
|
+
const own = new Set(life.steps.flatMap((st) => { const a = auth(st.headers); return a ? [a] : []; }));
|
|
98
|
+
const fallback = auth(life.headers);
|
|
99
|
+
if (fallback && own.size > 0) {
|
|
100
|
+
const leaning = life.steps.filter((st) => 'method' in st && !auth(st.headers));
|
|
101
|
+
if (leaning.length)
|
|
102
|
+
flags.push({ step: leaning[0].name, pattern: 'CRED', detail: `${leaning.length} steps act with the life-wide default credential in a life of ${own.size + 1} credentials; give each its actor's own` });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
{
|
|
106
|
+
const walked = [];
|
|
107
|
+
const refusedAnswer = (a) => {
|
|
108
|
+
const b = a.body;
|
|
109
|
+
return a.status >= 400 || (!!b && typeof b === 'object' && (b.ok === false || (Array.isArray(b.errors) && b.errors.length > 0 && b.data === undefined) || typeof b.error === 'string'));
|
|
110
|
+
};
|
|
111
|
+
// CRED too: a credential a person was shown once (a key on a dashboard, a token in a mail) is read off what showed it
|
|
112
|
+
// (a capture), never typed in: a literal equal to what an earlier answer showed means the life knows how the twin mints it
|
|
113
|
+
const shown = new Set();
|
|
114
|
+
let literalFlags = 0;
|
|
115
|
+
await walk(life, inMemoryWorld(`flags-${vendor}`).root(), [], { beats: false, hooks: { after: (step, path, answer) => {
|
|
116
|
+
const s = step;
|
|
117
|
+
for (const [k, v] of Object.entries(s.headers ?? {})) {
|
|
118
|
+
if (!/^(authorization|x-api-key|api-key|cookie)$/i.test(k) || v.includes('{'))
|
|
119
|
+
continue;
|
|
120
|
+
const secret = v.replace(/^(bearer|token|basic)\s+/i, '').replace(/^[a-z_]+=/i, '');
|
|
121
|
+
if (secret.length >= 16 && shown.has(secret) && literalFlags++ < 3)
|
|
122
|
+
flags.push({ step: s.name, pattern: 'CRED', detail: `sends as a literal the credential an earlier answer showed (${secret.slice(0, 12)}…): capture it where it was shown` });
|
|
123
|
+
}
|
|
124
|
+
const text = typeof answer.body === 'string' ? answer.body : JSON.stringify(answer.body ?? '');
|
|
125
|
+
// a token ends before trailing punctuation (a key closing a sentence); a JWT's inner dots stay
|
|
126
|
+
for (const t of text.match(/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g) ?? [])
|
|
127
|
+
if (t.length >= 16)
|
|
128
|
+
shown.add(t);
|
|
129
|
+
const method = s.method ?? (s.mysql !== undefined || s.signIn ? `MySQL ${s.link ?? 'mysql'}` : s.connection ? `RESP ${s.connection}` : s.socket ? `socket ${s.open ? 'open' : s.close ? 'close' : 'send'}` : s.git ? `git ${s.git}${s.delete ? ' delete' : ''}` : `line ${s.on}`);
|
|
130
|
+
const body = JSON.stringify(s.mysql !== undefined || s.signIn ? { mysql: s.mysql ?? null, values: s.values ?? null, signIn: s.signIn?.user ?? null } : s.resp ?? s.body ?? s.send ?? (s.ref ? { ref: s.ref } : null));
|
|
131
|
+
// an operation named in a header (AWS's X-Amz-Target) is part of what the request is; who sends it is not
|
|
132
|
+
const named = Object.entries(s.headers ?? {}).filter(([k]) => !/^(authorization|stripe-account|x-slack-|cookie)/i.test(k)).sort().map(([k, v]) => `${k}=${v}`).join('&');
|
|
133
|
+
// and so is the region an AWS request is signed for (its credential scope), which says where it goes
|
|
134
|
+
const region = /Credential=[^/]+\/\d{8}\/([a-z0-9-]+)\//.exec((s.headers ?? {}).authorization ?? (life.headers ?? {}).authorization ?? '')?.[1];
|
|
135
|
+
const where = named || region ? `${path} [${[named, region ? `region=${region}` : ''].filter(Boolean).join('&')}]` : path;
|
|
136
|
+
walked.push({ index: life.steps.indexOf(step), name: s.name, key: `${method} ${where} ${body}`, path: where, method, body, refused: s.socket ? (answer.status !== 101 && answer.status !== 1000) : refusedAnswer(answer), status: answer.status });
|
|
137
|
+
} } });
|
|
138
|
+
const sitting = (i) => life.steps.slice(0, i).filter((x) => 'wait' in x).length;
|
|
139
|
+
// a client asking whether something exists before it makes it is not a refusal (existenceProbe, journey-kit)
|
|
140
|
+
const probe = (n) => existenceProbe(walked.map((w) => ({ ...w, sitting: sitting(w.index) })), n);
|
|
141
|
+
let run = [];
|
|
142
|
+
const endRun = () => { if (run.length >= 3)
|
|
143
|
+
flags.push({ step: run[0].name, pattern: 'RUN', detail: `${run.length} refusals in a row, from "${run[0].name.slice(0, 50)}" to "${run.at(-1).name.slice(0, 50)}"` }); run = []; };
|
|
144
|
+
walked.forEach((w, n) => {
|
|
145
|
+
if (w.refused && !probe(n))
|
|
146
|
+
run.push(w);
|
|
147
|
+
else if (!w.refused)
|
|
148
|
+
endRun();
|
|
149
|
+
const earlier = walked.slice(0, n).filter((e) => sitting(e.index) === sitting(w.index));
|
|
150
|
+
if (w.refused && earlier.some((e) => !e.refused && e.key === w.key))
|
|
151
|
+
flags.push({ step: w.name, pattern: 'REPEAT', detail: `repeats a request that succeeded earlier in the sitting (${w.method} ${w.path.slice(0, 60)})` });
|
|
152
|
+
if (!w.refused && /^(POST|PUT|PATCH)$/.test(w.method)) {
|
|
153
|
+
const writes = earlier.filter((e) => !e.refused && e.method === w.method && e.path === w.path);
|
|
154
|
+
if (writes.length >= 2 && writes.at(-2).body === w.body && writes.at(-1).body !== w.body)
|
|
155
|
+
flags.push({ step: w.name, pattern: 'TOGGLE', detail: `writes ${w.path.slice(0, 60)} back to what it was two writes before, in one sitting` });
|
|
156
|
+
}
|
|
157
|
+
if (!w.refused && w.method === 'DELETE' && earlier.some((e) => !e.refused && (e.method === 'PUT' || e.method === 'POST') && e.path === w.path))
|
|
158
|
+
flags.push({ step: w.name, pattern: 'TOGGLE', detail: `removes ${w.path.slice(0, 60)}, made earlier in the same sitting` });
|
|
159
|
+
});
|
|
160
|
+
endRun();
|
|
161
|
+
// a time of day a step names that its hour on the World clock contradicts (the clock starts at startsAt or the default and
|
|
162
|
+
// moves only by `wait` steps; a wait's name tells what happens once it has passed)
|
|
163
|
+
const hours = [[/\bmorning\b/i, 5, 12], [/\blunch\b/i, 11, 15], [/\bafternoon\b/i, 12, 18], [/\bevening\b/i, 17, 23]];
|
|
164
|
+
const WEEKDAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
165
|
+
const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];
|
|
166
|
+
const DATED = /\b(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),? (\d{1,2}) (January|February|March|April|May|June|July|August|September|October|November|December)(?:,? (\d{4}))?/g;
|
|
167
|
+
let clock = Date.parse(life.startsAt ?? '2026-01-01T00:00:00Z');
|
|
168
|
+
for (const step of life.steps) {
|
|
169
|
+
if ('wait' in step)
|
|
170
|
+
clock += waitMs(step.wait);
|
|
171
|
+
const hour = new Date(clock).getUTCHours();
|
|
172
|
+
for (const [word, from, to] of hours) {
|
|
173
|
+
if (word.test(step.name) && (hour < from || hour >= to))
|
|
174
|
+
flags.push({ step: step.name, pattern: 'TIME', detail: `says ${word.source.replace(/\\b/g, '')} at ${String(hour).padStart(2, '0')}:00 on the World clock` });
|
|
175
|
+
}
|
|
176
|
+
// a weekday named with its date is a fact of the calendar, and a wait's name says the date the clock reaches
|
|
177
|
+
for (const m of step.name.matchAll(DATED)) {
|
|
178
|
+
const [, weekday, day, month, year] = m;
|
|
179
|
+
const at = new Date(Date.UTC(year ? Number(year) : new Date(clock).getUTCFullYear(), MONTHS.indexOf(month.toLowerCase()), Number(day)));
|
|
180
|
+
if (WEEKDAYS[at.getUTCDay()] !== weekday.toLowerCase())
|
|
181
|
+
flags.push({ step: step.name, pattern: 'TIME', detail: `says ${weekday} ${day} ${month}, which is a ${WEEKDAYS[at.getUTCDay()]}` });
|
|
182
|
+
else if ('wait' in step && at.toISOString().slice(0, 10) !== new Date(clock).toISOString().slice(0, 10))
|
|
183
|
+
flags.push({ step: step.name, pattern: 'TIME', detail: `its wait reaches ${new Date(clock).toISOString().slice(0, 10)}, not ${weekday} ${day} ${month}` });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const differs = first.findIndex((line, i) => line !== second[i]);
|
|
188
|
+
if (differs >= 0 || first.length !== second.length)
|
|
189
|
+
failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail: `a second fresh World answered differently at "${(first[differs] ?? second[differs] ?? first.at(-1) ?? '').split('\u0000')[0]}" (the ${differs + 1}th answered step; waits are not counted)` });
|
|
190
|
+
// ── breadth ──────────────────────────────────────────────────────────────────────────────────
|
|
191
|
+
const factory = await vendorFetch(PACKS, vendor);
|
|
192
|
+
const fetch = factory?.(world.root());
|
|
193
|
+
// a line protocol's commands are served on its stream
|
|
194
|
+
const stream = (await vendorStream(PACKS, vendor))?.(world.root());
|
|
195
|
+
const owners = fetch?.owners?.() ?? stream?.owners?.() ?? {};
|
|
196
|
+
world.restore();
|
|
197
|
+
const breadth = {};
|
|
198
|
+
for (const o of Object.values(owners))
|
|
199
|
+
breadth[o] = (breadth[o] ?? 0) + 1;
|
|
200
|
+
const operations = Object.keys(owners).length;
|
|
201
|
+
// ── coverage ─────────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// the measurement is kept (journeys/coverage.json) only with --write; a plain run leaves the checkout as it found it
|
|
203
|
+
const keep = process.argv.includes('--write');
|
|
204
|
+
const coveragePath = keep ? join(PKG, 'journeys', 'coverage.json') : join(mkdtempSync(join(tmpdir(), 'score-coverage-')), 'coverage.json');
|
|
205
|
+
// each tool in a process of its own, run from the pack repository, naming it as this one does
|
|
206
|
+
const tool = (name, args) => spawnSync(process.execPath, [toolPath('run-tool'), PACKS, checkout ?? '', toolPath(name), ...args], { cwd: PACKS, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
207
|
+
const measured = tool('life-coverage', [vendor, ...(keep ? ['--write'] : ['--out', coveragePath])]);
|
|
208
|
+
// a measurement that failed reports so, never the last one it kept
|
|
209
|
+
const coverage = measured.status === 0 && existsSync(coveragePath) ? parseExactJson(readFileSync(coveragePath, 'utf8')) : undefined;
|
|
210
|
+
// ── the vendor's published examples (vendor-examples.ts) ────────────────────────────────────────
|
|
211
|
+
const replay = tool('vendor-examples', [vendor]);
|
|
212
|
+
const replayOut = String(replay.stdout).trim().split('\n');
|
|
213
|
+
const replayLine = replayOut.at(-1) ?? '';
|
|
214
|
+
// replayed and judged are two lacks: the replay is the twin's, the verdict a judge's (pack-done's (d) and (f))
|
|
215
|
+
const examples = {
|
|
216
|
+
done: replay.status === 0,
|
|
217
|
+
replayed: replayOut.some((l) => l.trim() === 'replayed: yes'),
|
|
218
|
+
verdict: replayOut.find((l) => l.trim().startsWith('verdict: '))?.trim().slice('verdict: '.length) ?? 'not measured',
|
|
219
|
+
failures: replayOut.filter((line) => /^\s*(?:FAIL|GAP)\s/.test(line)).map((line) => line.trim()),
|
|
220
|
+
line: replayLine.replace(/^\S+\s+/, ''),
|
|
221
|
+
};
|
|
222
|
+
const pct = (hit, found) => (found ? `${((hit / found) * 100).toFixed(1)}%` : 'n/a');
|
|
223
|
+
const score = {
|
|
224
|
+
scope: 'source-customer-life-and-published-examples',
|
|
225
|
+
vendor,
|
|
226
|
+
life: { steps: life.steps.length, verdict: judged, judge: verdict?.judge?.agent ?? verdict?.judge?.name ?? null, authors: verdict?.authors ?? [] },
|
|
227
|
+
failures: failures.map((f) => ({ step: f.step, beat: f.beat, detail: f.detail })),
|
|
228
|
+
flags,
|
|
229
|
+
coverage: coverage ?? null,
|
|
230
|
+
examples,
|
|
231
|
+
breadth: { operations, ...breadth },
|
|
232
|
+
};
|
|
233
|
+
console.log(` measured through: ${kernel}`);
|
|
234
|
+
console.log(`${vendor}: life of ${life.steps.length} steps, ${judged}; ${failures.length} checks failing`);
|
|
235
|
+
if (!coverage)
|
|
236
|
+
console.log(` coverage: NOT MEASURED — life-coverage failed: ${String(measured.stderr).trim().split('\n').slice(-3).join(' ')}`);
|
|
237
|
+
if (coverage)
|
|
238
|
+
console.log(` coverage: ${coverage.missing === 0 ? 'meets the standard' : `${coverage.missing} missing`} — hand-written lines ${pct(coverage.code.run, coverage.code.lines)} run, ${coverage.code.recorded} recorded unreachable, ${coverage.code.missing} missing; state logic ${coverage.state.reached}/${coverage.state.declared} reached, ${coverage.state.recorded} recorded, ${coverage.state.missing.length} missing, ${coverage.state.undeclared.length} undeclared moves`);
|
|
239
|
+
console.log(` ${examples.line || 'examples: not measured'}`);
|
|
240
|
+
console.log(` breadth: ${operations} spec operations — ${Object.entries(breadth).map(([k, n]) => `${n} ${k}`).join(', ')}`);
|
|
241
|
+
for (const f of failures.slice(0, 20))
|
|
242
|
+
console.log(` FAIL ${f.beat} ${f.step.slice(0, 60)}: ${f.detail.slice(0, 140)}`);
|
|
243
|
+
console.log(` judge flags: ${flags.length ? `${flags.length} (${['REPEAT', 'RUN', 'TOGGLE', 'TIME', 'CRED'].map((p) => `${flags.filter((f) => f.pattern === p).length} ${p}`).join(', ')}) — what judges reject on sight; resolve each or give it a cause the story shows` : 'none'}`);
|
|
244
|
+
for (const f of flags.slice(0, 30))
|
|
245
|
+
console.log(` FLAG ${f.pattern} ${f.step.slice(0, 60)}: ${f.detail}`);
|
|
246
|
+
// Source requirements used by pack-done's report step; keep the field name for existing report consumers.
|
|
247
|
+
const missingForDone = [
|
|
248
|
+
judged !== 'plausible' ? `a plausible verdict from another session (now: ${judged})` : '',
|
|
249
|
+
failures.length ? `${failures.length} checks failing` : '',
|
|
250
|
+
flags.length ? `${flags.length} judge flags` : '',
|
|
251
|
+
!coverage ? 'coverage not measured' : coverage.missing ? `${coverage.missing} coverage items missing` : '',
|
|
252
|
+
breadth.legacy ? `${breadth.legacy} operations on a legacy fallback` : '',
|
|
253
|
+
examples.replayed ? '' : "the vendor's published examples not all replayed",
|
|
254
|
+
examples.verdict === 'plausible' ? '' : `a plausible verdict on the vendor's published examples (now: ${examples.verdict})`,
|
|
255
|
+
].filter(Boolean);
|
|
256
|
+
console.log(` source journey: ${missingForDone.length ? `incomplete — ${missingForDone.join('; ')}` : 'meets the source requirements'}`);
|
|
257
|
+
console.log(' pack completion: not assessed — pack-done consumes the exact installed assessment in the final run');
|
|
258
|
+
const reportAt = process.argv.indexOf('--report');
|
|
259
|
+
if (reportAt >= 0 && process.argv[reportAt + 1]) {
|
|
260
|
+
writeFileSync(process.argv[reportAt + 1], `${JSON.stringify({ ...score, missingForDone }, null, 2)}\n`);
|
|
261
|
+
}
|
|
262
|
+
if (process.argv.includes('--write')) {
|
|
263
|
+
writeFileSync(join(PKG, 'journeys', 'score.json'), `${JSON.stringify(score, null, 2)}\n`);
|
|
264
|
+
}
|