@volter/twin-standard 1.0.149 → 1.0.151
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 +5 -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 -1
- package/dist/src/conformance.js +256 -6
- package/dist/src/consumer.js +5 -1
- package/dist/src/create-pack.d.ts +5 -0
- package/dist/src/create-pack.js +31 -3
- package/dist/src/protocol-3.js +30 -2
- 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/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +6 -0
- package/dist/src/walk/behavior-journey.js +8 -6
- package/dist/src/walk/journey-kit.d.ts +2 -0
- package/dist/src/walk/journey-kit.js +6 -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 +192 -8
- package/src/consumer.ts +5 -1
- package/src/create-pack.ts +27 -3
- package/src/protocol-3.ts +24 -2
- 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/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +6 -2
- package/src/walk/journey-kit.ts +7 -2
- package/src/walk/rendered-scalar.ts +17 -0
- package/src/walk/sse-example.ts +37 -0
package/dist/src/create-pack.js
CHANGED
|
@@ -160,6 +160,9 @@ export function packFetchFile(dir, name) {
|
|
|
160
160
|
const hasLive = existsSync(join(semantics, 'live.ts'));
|
|
161
161
|
// a GraphQL API beside the REST one: its resolvers and the vendor's schema (graphql-wire.ts)
|
|
162
162
|
const hasGraphql = existsSync(join(semantics, 'graphql.ts')) && existsSync(join(dir, 'src', 'generated', 'graphql-sdl.gen.json'));
|
|
163
|
+
// a stream whose documents are BSON (the MongoDB wire): the pack's own bson library, resolved from the pack as graphql's is
|
|
164
|
+
const packageJson = existsSync(join(dir, 'package.json')) ? parseExactJson(readFileSync(join(dir, 'package.json'), 'utf8')) : {};
|
|
165
|
+
const hasBson = existsSync(join(dir, 'src', 'stream.ts')) && packageJson.dependencies?.bson !== undefined;
|
|
163
166
|
const screensDir = join(dir, 'src', 'screens');
|
|
164
167
|
const screens = existsSync(screensDir) ? readdirSync(screensDir).filter((n) => /\.tsx?$/.test(n) && !/\.(test|uitest)\.tsx?$/.test(n) && !/^shared\.tsx?$/.test(n)).sort() : [];
|
|
165
168
|
const id = (file) => file.replace(/\.tsx?$/, '');
|
|
@@ -205,6 +208,7 @@ export function packFetchFile(dir, name) {
|
|
|
205
208
|
...screens.map((f) => `import { screen as ${identifier(`screen_${id(f)}`)} } from './screens/${f}';`),
|
|
206
209
|
...laneImports,
|
|
207
210
|
'',
|
|
211
|
+
...(hasBson ? ["/** The pack's BSON library, resolved from the pack as graphql's loader is (architecture, the MongoDB wire). */", "export const loadBson = (): Promise<unknown> => import('bson');", ''] : []),
|
|
208
212
|
`export function ${name}(options: PackFetchOptions = {}): DerivedFetch {`,
|
|
209
213
|
' return createPackFetch({',
|
|
210
214
|
' manifest,',
|
|
@@ -507,6 +511,27 @@ export function laneInitFiles(vendor, lanes) {
|
|
|
507
511
|
}
|
|
508
512
|
return files;
|
|
509
513
|
}
|
|
514
|
+
/** A unit's fixed files as the manifest and the spec make them now (architecture, "Creating a pack"): a lane's fetch, and
|
|
515
|
+
* a pack's fetch, index, server and cli. A pack with a wire of its own (`src/stream.ts`, SMTP's and Redis's byte-stream
|
|
516
|
+
* door) writes its server and cli itself: they carry its listener beside the fetch, so they are not fixed. `--index`
|
|
517
|
+
* writes these and the grade's `fixed-reproducible` compares them byte for byte. */
|
|
518
|
+
export async function fixedFiles(dir) {
|
|
519
|
+
const files = { 'src/fetch.ts': packFetchFile(dir, fetchName(dir)) };
|
|
520
|
+
if (!existsSync(join(dir, 'package.json')))
|
|
521
|
+
return files;
|
|
522
|
+
const { manifest } = await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')));
|
|
523
|
+
const vendor = manifest?.vendor;
|
|
524
|
+
if (!vendor || !/^[a-z][a-z0-9-]*$/.test(vendor))
|
|
525
|
+
throw new Error('Pack manifest must declare its vendor identity independently of the npm package name');
|
|
526
|
+
files['src/index.ts'] = packIndexFile(dir, vendor);
|
|
527
|
+
if (!existsSync(join(dir, 'src', 'stream.ts'))) {
|
|
528
|
+
// a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form
|
|
529
|
+
const lanesOnly = !hasOwnSurface(dir) && readdirSync(dir).some((n) => existsSync(join(dir, n, 'src', 'manifest.ts')));
|
|
530
|
+
files['src/server.ts'] = serverFile(vendor, !lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json')));
|
|
531
|
+
files['src/cli.ts'] = cliFile(vendor);
|
|
532
|
+
}
|
|
533
|
+
return files;
|
|
534
|
+
}
|
|
510
535
|
export async function createPack(args) {
|
|
511
536
|
const [unit, ...rest] = args;
|
|
512
537
|
if (!unit || (!rest.includes('--index') && !rest.includes('--brief') && !rest.includes('--init'))) {
|
|
@@ -598,9 +623,12 @@ export async function createPack(args) {
|
|
|
598
623
|
writeFileSync(path, handlerIndex(dir));
|
|
599
624
|
console.log(`create-pack: wrote ${path}`);
|
|
600
625
|
}
|
|
601
|
-
// the
|
|
602
|
-
|
|
603
|
-
|
|
626
|
+
// the fixed files, again: a door file, the clock, a screen, a lane or a proto may have been added since, and a pack
|
|
627
|
+
// made by an earlier create-pack takes the current form
|
|
628
|
+
for (const [rel, text] of Object.entries(await fixedFiles(dir))) {
|
|
629
|
+
writeFileSync(join(dir, rel), text);
|
|
630
|
+
console.log(`create-pack: wrote ${join(dir, rel)}`);
|
|
631
|
+
}
|
|
604
632
|
// a pack's own index (a lane's is the vendor's to name): the descriptor with what the kernel derives from the surface
|
|
605
633
|
if (existsSync(join(dir, 'package.json'))) {
|
|
606
634
|
const { manifest } = await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')));
|
package/dist/src/protocol-3.js
CHANGED
|
@@ -315,6 +315,23 @@ const criteria = [
|
|
|
315
315
|
return changed.length ? fail(`derive-pack writes differently: ${changed.slice(0, 5).join(', ')}${changed.length > 5 ? ` and ${changed.length - 5} more` : ''}`) : pass(`${filesUnder(gen).length} files reproduced`);
|
|
316
316
|
},
|
|
317
317
|
},
|
|
318
|
+
{
|
|
319
|
+
id: 'fixed-reproducible', section: 'form',
|
|
320
|
+
asks: 'create-pack regenerates the fixed files (fetch, and a pack\'s index, server and cli) byte for byte',
|
|
321
|
+
async check(unit) {
|
|
322
|
+
// loaded when graded: create-pack reads this module's handler names, so a static import would be a cycle
|
|
323
|
+
const { fixedFiles } = await import("./create-pack.js");
|
|
324
|
+
let want;
|
|
325
|
+
try {
|
|
326
|
+
want = await fixedFiles(unit.dir);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
return fail(`create-pack cannot write the fixed files: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200));
|
|
330
|
+
}
|
|
331
|
+
const differ = Object.entries(want).filter(([rel, text]) => !existsSync(join(unit.dir, rel)) || readFileSync(join(unit.dir, rel), 'utf8') !== text).map(([rel]) => rel);
|
|
332
|
+
return differ.length ? fail(`create-pack --index writes differently: ${differ.join(', ')} (run it, never edit a fixed file)`) : pass(`${Object.keys(want).length} fixed files reproduced`);
|
|
333
|
+
},
|
|
334
|
+
},
|
|
318
335
|
{
|
|
319
336
|
id: 'layout', section: 'form',
|
|
320
337
|
asks: "the unit's files exactly Pack layout's tree: fixed names at src/'s root, semantics/ its fixed files and one per spec family, screens/ one per declared screen, journeys/ its closed set",
|
|
@@ -540,8 +557,8 @@ const criteria = [
|
|
|
540
557
|
// an error message's "from '<name>'", is none)
|
|
541
558
|
let text = readFileSync(join(src, f), 'utf8');
|
|
542
559
|
if (f === 'fetch.ts') {
|
|
543
|
-
// The generated fetch supplies the kernel wire with the pack-local parser
|
|
544
|
-
// Exempt only
|
|
560
|
+
// The generated fetch supplies the kernel wire with the pack-local library: graphql's parser, a stream's BSON.
|
|
561
|
+
// Exempt only those exact generated parts; other static or dynamic imports still count.
|
|
545
562
|
const source = ts.createSourceFile(f, text, ts.ScriptTarget.Latest, true);
|
|
546
563
|
const loaders = [];
|
|
547
564
|
const visit = (node) => {
|
|
@@ -556,6 +573,17 @@ const criteria = [
|
|
|
556
573
|
factory.arguments[0] === graphql.parent)
|
|
557
574
|
loaders.push({ start: node.getStart(source), end: node.end });
|
|
558
575
|
}
|
|
576
|
+
// The generated BSON loader of a stream's pack (architecture, the MongoDB wire): exactly
|
|
577
|
+
// `export const loadBson = (): Promise<unknown> => import('bson');`, BSON supplied as graphql is.
|
|
578
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
579
|
+
node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text === 'bson') {
|
|
580
|
+
const arrow = node.parent, declaration = arrow.parent, list = declaration?.parent, statement = list?.parent;
|
|
581
|
+
if (ts.isArrowFunction(arrow) && arrow.parameters.length === 0 && arrow.body === node &&
|
|
582
|
+
declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.name.text === 'loadBson' &&
|
|
583
|
+
statement && ts.isVariableStatement(statement) && statement.parent === source &&
|
|
584
|
+
statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
|
|
585
|
+
loaders.push({ start: node.getStart(source), end: node.end });
|
|
586
|
+
}
|
|
559
587
|
ts.forEachChild(node, visit);
|
|
560
588
|
};
|
|
561
589
|
visit(source);
|
package/dist/src/spec-ir.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type IrOperation = {
|
|
|
37
37
|
bodyEncoding: 'json' | 'form' | 'multipart' | 'xml' | 'none';
|
|
38
38
|
/** A form-encoded operation's number and boolean fields, by bracket path (formScalarsOf). */
|
|
39
39
|
scalars?: Record<string, 'integer' | 'number' | 'boolean'>;
|
|
40
|
+
/** A form-encoded operation's string fields that take only listed values, by bracket path (formEnumsOf). */
|
|
41
|
+
enums?: Record<string, string[]>;
|
|
40
42
|
successStatus: number;
|
|
41
43
|
/** Binary media types declared by each response status, independent of the twin’s response headers. */
|
|
42
44
|
responseMedia?: Record<string, string[]>;
|
package/dist/src/spec-ir.js
CHANGED
|
@@ -295,6 +295,47 @@ function formScalarsOf(doc, op, pathItem, encoding) {
|
|
|
295
295
|
visit(p, [name], 0);
|
|
296
296
|
return Object.keys(out).length ? Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b))) : undefined;
|
|
297
297
|
}
|
|
298
|
+
/** A form-encoded operation's string fields whose schema lists the values they take (`status`: active, inactive,
|
|
299
|
+
* canceled), by the bracket path formScalarsOf uses; a field another alternative leaves free (a string without an enum,
|
|
300
|
+
* or a number) takes any value and is not listed. */
|
|
301
|
+
function formEnumsOf(doc, op, pathItem, encoding) {
|
|
302
|
+
if (encoding !== 'form')
|
|
303
|
+
return undefined;
|
|
304
|
+
const out = {};
|
|
305
|
+
const visit = (schema, at, depth) => {
|
|
306
|
+
const s = deref(doc, schema);
|
|
307
|
+
if (!s || typeof s !== 'object' || depth > 12)
|
|
308
|
+
return;
|
|
309
|
+
const alts = [...(Array.isArray(s.anyOf) ? s.anyOf : []), ...(Array.isArray(s.oneOf) ? s.oneOf : [])].map((a) => deref(doc, a));
|
|
310
|
+
if (alts.length) {
|
|
311
|
+
const free = alts.some((a) => (a?.type === 'string' && !Array.isArray(a.enum)) || a?.type === 'integer' || a?.type === 'number' || a?.type === 'boolean');
|
|
312
|
+
const listed = alts.filter((a) => a?.type === 'string' && Array.isArray(a.enum)).flatMap((a) => a.enum.map(String));
|
|
313
|
+
if (!free && listed.length && at.length)
|
|
314
|
+
out[at.join('.')] = [...new Set(listed)];
|
|
315
|
+
for (const a of alts)
|
|
316
|
+
if (a?.type === 'object' || a?.type === 'array' || a?.properties)
|
|
317
|
+
visit(a, at, depth + 1);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (s.type === 'string' && Array.isArray(s.enum) && at.length) {
|
|
321
|
+
out[at.join('.')] = s.enum.map(String);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (s.type === 'array') {
|
|
325
|
+
visit(s.items, [...at, '[]'], depth + 1);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const [name, p] of Object.entries(propsOf(doc, s)))
|
|
329
|
+
visit(p, [...at, name], depth + 1);
|
|
330
|
+
};
|
|
331
|
+
for (const p of [...(pathItem.parameters ?? []), ...(op.parameters ?? [])].map((q) => deref(doc, q)))
|
|
332
|
+
if (p?.in === 'query')
|
|
333
|
+
visit(p.schema, [String(p.name)], 0);
|
|
334
|
+
const body = deref(doc, deref(doc, op.requestBody)?.content?.['application/x-www-form-urlencoded']?.schema);
|
|
335
|
+
for (const [name, p] of Object.entries(propsOf(doc, body)))
|
|
336
|
+
visit(p, [name], 0);
|
|
337
|
+
return Object.keys(out).length ? Object.fromEntries(Object.entries(out).sort(([x], [y]) => x.localeCompare(y))) : undefined;
|
|
338
|
+
}
|
|
298
339
|
/** The class of one operation, from its method, its path and what it answers. An operation this cannot place is `action`, never CRUD. */
|
|
299
340
|
/** A path and its discriminator as one key: `/responses/{id}` and `/responses/{id}?beta=true` are different reads. */
|
|
300
341
|
// a null value (the key present with any value) keys as the bare name
|
|
@@ -445,6 +486,7 @@ export function fromOpenAPI(spec) {
|
|
|
445
486
|
const credentials = [...new Set(security.flatMap((req) => { const names = Object.keys(req ?? {}); return names.length ? names : ['anonymous']; }))].sort();
|
|
446
487
|
const body = bodyOf(doc, op);
|
|
447
488
|
const scalars = formScalarsOf(doc, op, item, body.bodyEncoding);
|
|
489
|
+
const enums = formEnumsOf(doc, op, item, body.bodyEncoding);
|
|
448
490
|
const ownServer = String((op.servers ?? item.servers)?.[0]?.url ?? '');
|
|
449
491
|
const ownBase = /^https?:\/\//.test(ownServer) ? new URL(ownServer).pathname.replace(/\/+$/, '') : undefined;
|
|
450
492
|
raw.push({
|
|
@@ -456,6 +498,7 @@ export function fromOpenAPI(spec) {
|
|
|
456
498
|
...(ownBase !== undefined && ownBase !== docBase ? { basePath: ownBase } : {}),
|
|
457
499
|
...body,
|
|
458
500
|
...(scalars ? { scalars } : {}),
|
|
501
|
+
...(enums ? { enums } : {}),
|
|
459
502
|
successStatus: success.status,
|
|
460
503
|
...(Object.keys(responseMedia).length ? { responseMedia, ...(Object.keys(defaultResponseMedia).length ? { defaultResponseMedia } : {}) } : {}),
|
|
461
504
|
...(success.empty ? { emptySuccess: true } : {}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
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.js";
|
|
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.js";
|
|
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
|
+
const vendor = process.argv[2];
|
|
18
|
+
const authorsAt = process.argv.indexOf('--authors');
|
|
19
|
+
const authors = authorsAt > 0 ? (process.argv[authorsAt + 1] ?? '').split(',').map((a) => a.trim()).filter(Boolean).map((name) => attributionName(name)) : [];
|
|
20
|
+
if (!vendor || vendor.startsWith('-') || !authors.length) {
|
|
21
|
+
console.error('usage: twin-standard judge-brief <vendor> --authors <author name>[,<agent>...] (from the pack repository\'s root)');
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const journeys = join(PACKS, vendor, 'journeys');
|
|
25
|
+
if (!existsSync(journeys)) {
|
|
26
|
+
console.error(`judge-brief: ${vendor} has no journeys/ in ${PACKS}`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
// the list of what a judge rejects, as architecture holds it
|
|
30
|
+
const architecture = existsSync(ARCHITECTURE) ? readFileSync(ARCHITECTURE, 'utf8') : await fetch(PUBLISHED_ARCHITECTURE).then((r) => (r.ok ? r.text() : ''), () => '');
|
|
31
|
+
const listAt = architecture.indexOf('#### What a judge rejects\n');
|
|
32
|
+
if (listAt < 0) {
|
|
33
|
+
console.error(`judge-brief: the architecture (${existsSync(ARCHITECTURE) ? ARCHITECTURE : PUBLISHED_ARCHITECTURE}) has no "#### What a judge rejects" section`);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
const listFrom = architecture.indexOf('\n- ', listAt);
|
|
37
|
+
const listTo = architecture.indexOf('\n\n', listFrom + 1);
|
|
38
|
+
const rejects = architecture.slice(listFrom + 1, listTo < 0 ? undefined : listTo).trim();
|
|
39
|
+
const sha = (file) => createHash('sha256').update(readFileSync(file)).digest('hex');
|
|
40
|
+
const judged = [
|
|
41
|
+
{ file: 'customer-life.outline.md', verdict: 'customer-life.outline.verdict.json', what: "the life's outline: the customer's story in acts" },
|
|
42
|
+
{ file: 'customer-life.json', verdict: 'customer-life.verdict.json', what: 'the life: every step an application or person takes, its expectations' },
|
|
43
|
+
{ file: 'vendor-examples.json', verdict: 'vendor-examples.verdict.json', what: "the published examples: the vendor's own documented requests and answers, replayed" },
|
|
44
|
+
].filter((j) => existsSync(join(journeys, j.file)));
|
|
45
|
+
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
|
|
46
|
+
the run that wrote them. Judge them against the vendor and against the list below; do not edit any judged file, and run no
|
|
47
|
+
test. Read what you need: the pack's spec (${vendor}/spec/, SOURCE.md naming every page read), its demand
|
|
48
|
+
(${vendor}/journeys/demand.json: what the applications call), its decision table (journeys/decisions.json), and the
|
|
49
|
+
vendor's own documentation the spec cites.
|
|
50
|
+
|
|
51
|
+
What you judge (in ${vendor}/journeys/), each with the sha256 your verdict names:
|
|
52
|
+
${judged.map((j) => `- ${j.file} (${j.what}): sha256 ${sha(join(journeys, j.file))}`).join('\n')}
|
|
53
|
+
|
|
54
|
+
What a judge rejects (docs/contributing/architecture.md, "What a judge rejects"):
|
|
55
|
+
${rejects}
|
|
56
|
+
|
|
57
|
+
For each file write its verdict beside it, as JSON:
|
|
58
|
+
${judged.map((j) => `- ${j.verdict}`).join('\n')}
|
|
59
|
+
{
|
|
60
|
+
"judge": { "agent": "<what you are: your runtime and model>", "name": "<your author name or reviewer role>" },
|
|
61
|
+
"authors": ${JSON.stringify(authors)},
|
|
62
|
+
"sha256": "<the judged file's sha256 above>",
|
|
63
|
+
"judgedAt": "<today, YYYY-MM-DD>",
|
|
64
|
+
"plausible": <true only when no ruling stands>,
|
|
65
|
+
"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>" } ]
|
|
66
|
+
}
|
|
67
|
+
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,220 @@
|
|
|
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
|
+
import { parseExactJson } from '@volter/world-core/exact-json';
|
|
10
|
+
// The walks `life-coverage.ts` measures: one pack's customer life, walked with its beats on
|
|
11
|
+
// the in-memory World, and the vendor's published examples replayed (journeys/vendor-examples.json, vendor-examples.ts)
|
|
12
|
+
// 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
|
|
13
|
+
// a test file life-coverage writes, which registers them (coverageWalks), and reads the lcov it leaves; the walk holding
|
|
14
|
+
// is its precondition, not what it measures. This module is no test file itself, so a published package holds it.
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { listActions, observeTransitions, stateOf } from '@volter/world-core/runtime';
|
|
18
|
+
import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
|
|
19
|
+
import { lanesOf, shapeChecker } from "../walk/shape.js";
|
|
20
|
+
import { sharingLanesIn } from "../lanes.js";
|
|
21
|
+
/** Registers, in the `bun test` run that imports it, the walks of `vendor` in the pack repository at `PACKS`. */
|
|
22
|
+
export async function coverageWalks(PACKS, vendor) {
|
|
23
|
+
const { expect, test } = await import('bun:test');
|
|
24
|
+
usePacks(PACKS);
|
|
25
|
+
const sharingLanes = (v) => (v.includes('/') ? [] : sharingLanesIn(join(PACKS, v), v));
|
|
26
|
+
const journeys = join(PACKS, vendor, 'journeys');
|
|
27
|
+
const walks = [{ file: 'customer-life.json', beats: true }, { file: 'vendor-examples.json', beats: false }].filter((w) => existsSync(join(journeys, w.file)));
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const undeclared = new Set();
|
|
30
|
+
for (const [n, w] of walks.entries())
|
|
31
|
+
test(`${vendor}: ${w.file} walks`, async () => {
|
|
32
|
+
const life = parseExactJson(readFileSync(join(journeys, w.file), 'utf8'));
|
|
33
|
+
const world = inMemoryWorld(`coverage-${vendor}-${n}`);
|
|
34
|
+
const failures = [];
|
|
35
|
+
// each declared transition the life moved through or was refused by, named by resource, field and its place
|
|
36
|
+
const { manifest } = (await import(__rewriteRelativeImportExtension(join(PACKS, vendor, 'src', 'manifest.ts'))));
|
|
37
|
+
// the lanes over the vendor's state its serve factory serves are walked by this life, their machines with the vendor's
|
|
38
|
+
// ("Pack layout"); two manifests naming one resource's field would collide in the keys below, so that refuses
|
|
39
|
+
const laneManifests = await Promise.all(sharingLanes(vendor).map(async (l) => (await import(__rewriteRelativeImportExtension(join(PACKS, vendor, l, 'src', 'manifest.ts')))).manifest));
|
|
40
|
+
// a vendor of lanes' root (a VendorManifest) declares no resources of its own: its lanes do
|
|
41
|
+
const allResources = [manifest, ...laneManifests].flatMap((mf) => Object.entries(mf.resources ?? {}));
|
|
42
|
+
const machines = new Map();
|
|
43
|
+
for (const [resource, decl] of allResources)
|
|
44
|
+
for (const [field, machine] of Object.entries(decl.state ?? {})) {
|
|
45
|
+
const name = `${resource}.${field}`;
|
|
46
|
+
const previous = machines.get(name);
|
|
47
|
+
// Lanes sharing one vendor import the same state declaration; distinct declarations still collide.
|
|
48
|
+
if (previous && previous !== machine)
|
|
49
|
+
throw new Error(`${vendor}: ${name} is declared by two manifests (the vendor's and a lane's)`);
|
|
50
|
+
machines.set(name, machine);
|
|
51
|
+
}
|
|
52
|
+
const where = new Map();
|
|
53
|
+
for (const mf of [manifest, ...laneManifests])
|
|
54
|
+
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
55
|
+
for (const [field, m] of Object.entries(decl.state ?? {}))
|
|
56
|
+
m.transitions.forEach((t, i) => {
|
|
57
|
+
const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
|
|
58
|
+
const places = where.get(t) ?? [];
|
|
59
|
+
if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service))
|
|
60
|
+
where.set(t, [...places, place]);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const moves = new Map();
|
|
64
|
+
const subjectKey = (service, type, id) => JSON.stringify([service, type, id]);
|
|
65
|
+
observeTransitions((_decl, t, outcome, from, entry) => {
|
|
66
|
+
const places = where.get(t) ?? [];
|
|
67
|
+
if (outcome === 'refused') {
|
|
68
|
+
// A failed check on an already committed write is no refusal the application received.
|
|
69
|
+
if (entry)
|
|
70
|
+
return;
|
|
71
|
+
const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
|
|
72
|
+
if (groups.size === 1)
|
|
73
|
+
for (const p of places)
|
|
74
|
+
seen.add(`${p.at}|refused|${from}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// a move the machine allowed on a step with no write behind it (a line protocol's session, whose stage is the
|
|
78
|
+
// connection's): reached, as a refusal given is, where its declaration is unambiguous
|
|
79
|
+
if (!entry) {
|
|
80
|
+
const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
|
|
81
|
+
if (groups.size === 1)
|
|
82
|
+
for (const p of places)
|
|
83
|
+
seen.add(`${p.at}|moved|${from}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if ((t.actor ?? 'api') !== entry.actor)
|
|
87
|
+
return;
|
|
88
|
+
const { action, field, actor } = entry;
|
|
89
|
+
const matching = places.filter((p) => p.service === action.service && p.type === action.subject.type && p.field === field);
|
|
90
|
+
if (!matching.length)
|
|
91
|
+
return;
|
|
92
|
+
for (const p of matching)
|
|
93
|
+
seen.add(`${p.at}|moved|${from}`);
|
|
94
|
+
const on = moves.get(action.id) ?? [];
|
|
95
|
+
on.push({ transition: t, from, field, actor, subject: subjectKey(action.service, action.subject.type, action.subject.id) });
|
|
96
|
+
moves.set(action.id, on);
|
|
97
|
+
});
|
|
98
|
+
const root = world.root();
|
|
99
|
+
// what each step was answered, by the spec operation it called: a refusal is seen at the wire, whichever code gave it
|
|
100
|
+
const shape = await shapeChecker(join(PACKS, vendor));
|
|
101
|
+
const answered = [];
|
|
102
|
+
try {
|
|
103
|
+
await walk(life, root, failures, { beats: w.beats, hooks: { after: (step, path, answer) => {
|
|
104
|
+
// a line step's refusals are the machine's own (observeTransitions); a request's are read off its answer
|
|
105
|
+
const op = 'method' in step ? shape?.operationOf(step.method, path, { ...(life.headers ?? {}), ...(step.headers ?? {}) })?.op.id : undefined;
|
|
106
|
+
if (op)
|
|
107
|
+
answered.push({ op, text: JSON.stringify(answer.body ?? ''), body: answer.body });
|
|
108
|
+
} } });
|
|
109
|
+
// a declared refusal is reached when a step on its operation was answered with it: its code, or its message
|
|
110
|
+
// up to the first placeholder
|
|
111
|
+
// a code is a value of the answer (X's `89` at errors[].code), never a run of digits inside another value (a token)
|
|
112
|
+
const holds = (value, code) => (value !== null && typeof value === 'object'
|
|
113
|
+
? Object.values(value).some((v) => holds(v, code))
|
|
114
|
+
: (typeof value === 'string' || typeof value === 'number') && String(value) === code);
|
|
115
|
+
const answers = (a, r) => r.code !== undefined ? holds(a.body, String(r.code)) : a.text.includes(r.message.split('{')[0].trim());
|
|
116
|
+
for (const [resource, decl] of allResources) {
|
|
117
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) {
|
|
118
|
+
m.transitions.forEach((t, i) => {
|
|
119
|
+
if (!t.operation)
|
|
120
|
+
return;
|
|
121
|
+
const on = answered.filter((a) => a.op === t.operation);
|
|
122
|
+
if (t.refusal && on.some((a) => answers(a, t.refusal)))
|
|
123
|
+
seen.add(`${resource}.${field}#${i}|refused|*`);
|
|
124
|
+
for (const [from, r] of Object.entries(t.refusals ?? {}))
|
|
125
|
+
if (on.some((a) => answers(a, r)))
|
|
126
|
+
seen.add(`${resource}.${field}#${i}|refused|${from}`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// every change the life made to a declared state field, from the World's log in order: a move is the declared
|
|
131
|
+
// transition that allows it (from its state to the next, by its operation when both name one); a change no
|
|
132
|
+
// transition allows is an undeclared move
|
|
133
|
+
const fieldsOf = new Map();
|
|
134
|
+
for (const mf of [manifest, ...laneManifests])
|
|
135
|
+
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
136
|
+
const key = JSON.stringify([mf.service ?? mf.vendor, decl.storedAs ?? resource]);
|
|
137
|
+
for (const [field, m] of Object.entries(decl.state ?? {}))
|
|
138
|
+
fieldsOf.set(key, [...(fieldsOf.get(key) ?? []), [resource, field, m]]);
|
|
139
|
+
}
|
|
140
|
+
const current = new Map();
|
|
141
|
+
// the services the life wrote: the vendor's, or its lanes' (a vendor of lanes' root names none of its own)
|
|
142
|
+
const services = [...new Set([manifest, ...laneManifests].map((mf) => mf.service).filter((x) => typeof x === 'string'))];
|
|
143
|
+
if (!services.length)
|
|
144
|
+
services.push(manifest.vendor);
|
|
145
|
+
for (const a of services.flatMap((service) => listActions(service, root))) {
|
|
146
|
+
if (a.op !== 'set' || !a.fields)
|
|
147
|
+
continue;
|
|
148
|
+
const key = subjectKey(a.service, a.subject.type, a.subject.id);
|
|
149
|
+
const prev = current.get(key);
|
|
150
|
+
const next = { ...(prev ?? {}), ...a.fields };
|
|
151
|
+
current.set(key, next);
|
|
152
|
+
if (!prev)
|
|
153
|
+
continue;
|
|
154
|
+
const opId = typeof a.input?.operationId === 'string' ? a.input.operationId : undefined;
|
|
155
|
+
// resources stored as one type (a response the beta and the GA API both serve) share its fields: a
|
|
156
|
+
// change is declared when any of their machines allows it
|
|
157
|
+
const changed = new Map();
|
|
158
|
+
const valueOf = (row, field) => field.split('.').reduce((value, part) => value && typeof value === 'object' ? value[part] : undefined, row);
|
|
159
|
+
for (const [resource, field, m] of fieldsOf.get(JSON.stringify([a.service, a.subject.type])) ?? []) {
|
|
160
|
+
if (!(field.split('.')[0] in a.fields))
|
|
161
|
+
continue;
|
|
162
|
+
const from = stateOf(m, valueOf(prev, field));
|
|
163
|
+
const to = stateOf(m, valueOf(next, field));
|
|
164
|
+
if (from === to)
|
|
165
|
+
continue;
|
|
166
|
+
const allows = (t, at) => (moves.get(a.id) ?? []).some((move) => move.transition === t && move.from === at && move.field === field && move.actor === (t.actor ?? 'api') &&
|
|
167
|
+
move.subject === subjectKey(a.service, a.subject.type, a.subject.id));
|
|
168
|
+
const i = m.transitions.findIndex((t) => allows(t, from) && (t.to ?? from) === to);
|
|
169
|
+
if (i >= 0)
|
|
170
|
+
seen.add(`${resource}.${field}#${i}|moved|${from}`);
|
|
171
|
+
// one write may carry several declared moves the machine walked (a batch the vendor works from
|
|
172
|
+
// validating through to completed as a read first looks); each step was observed as it was asked
|
|
173
|
+
const reached = new Set([from]);
|
|
174
|
+
for (let grew = true; grew && !reached.has(to);) {
|
|
175
|
+
grew = false;
|
|
176
|
+
for (const at of [...reached])
|
|
177
|
+
for (const t of m.transitions)
|
|
178
|
+
if (allows(t, at) && t.to && !reached.has(t.to)) {
|
|
179
|
+
reached.add(t.to);
|
|
180
|
+
grew = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const was = changed.get(field);
|
|
184
|
+
changed.set(field, { declared: (was?.declared ?? false) || reached.has(to), name: was?.name ?? `${resource}.${field}: ${from} → ${to} by ${opId ?? a.operation ?? '(unnamed)'}` });
|
|
185
|
+
}
|
|
186
|
+
for (const c of changed.values())
|
|
187
|
+
if (!c.declared)
|
|
188
|
+
undeclared.add(c.name);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
world.restore();
|
|
193
|
+
observeTransitions(undefined);
|
|
194
|
+
}
|
|
195
|
+
// each walk adds what it reached; the last one written holds them all
|
|
196
|
+
if (process.env.LIFE_TRANSITIONS)
|
|
197
|
+
writeFileSync(process.env.LIFE_TRANSITIONS, JSON.stringify({ seen: [...seen].sort(), undeclared: [...undeclared].sort() }));
|
|
198
|
+
expect(failures.filter((f) => f.beat !== 'SHAPE')).toEqual([]);
|
|
199
|
+
// a walk's time grows with its steps and the state they build (Stripe's 609 example steps took 70 s uninstrumented,
|
|
200
|
+
// measured 2026-09-26, each request catching the World up over every row it holds)
|
|
201
|
+
}, 180_000);
|
|
202
|
+
// A pack's lanes reach its code through the pack's router (a management-API life making a database branch the data
|
|
203
|
+
// plane then serves): their lives and examples, walked here too, count toward the pack's code, not its state logic
|
|
204
|
+
// (each lane's machines are its own, measured when the lane is). A lane is measured alone, with no lanes of its own.
|
|
205
|
+
const PACK = join(PACKS, vendor);
|
|
206
|
+
const laneWalks = vendor.includes('/') ? [] : lanesOf(PACK).flatMap((dir) => [{ file: 'customer-life.json', beats: true }, { file: 'vendor-examples.json', beats: false }]
|
|
207
|
+
.filter((w) => existsSync(join(dir, 'journeys', w.file))).map((w) => ({ ...w, path: join(dir, 'journeys', w.file), lane: dir.split('/').slice(-2).join('/') })));
|
|
208
|
+
for (const [n, w] of laneWalks.entries())
|
|
209
|
+
test(`${vendor}: ${w.lane} ${w.file} walks for the pack's code`, async () => {
|
|
210
|
+
const world = inMemoryWorld(`coverage-${vendor}-lane-${n}`);
|
|
211
|
+
const failures = [];
|
|
212
|
+
try {
|
|
213
|
+
await walk(parseExactJson(readFileSync(w.path, 'utf8')), world.root(), failures, { beats: w.beats });
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
world.restore();
|
|
217
|
+
}
|
|
218
|
+
expect(failures.filter((f) => f.beat !== 'SHAPE')).toEqual([]);
|
|
219
|
+
}, 180_000);
|
|
220
|
+
}
|