@volter/twin-standard 1.0.158 → 1.0.160
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 +3 -2
- package/client-sdks/package.json +1 -0
- package/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/client-sdks/package.json +1 -0
- package/dist/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/src/assessment.d.ts +13 -3
- package/dist/src/assessment.js +10 -6
- package/dist/src/browser-assessment.js +16 -13
- package/dist/src/check-sources.js +2 -2
- package/dist/src/clients/harness.d.ts +25 -4
- package/dist/src/clients/harness.js +184 -22
- package/dist/src/clients/jira.js +11 -2
- package/dist/src/clients/resend.first-use.json +2 -2
- package/dist/src/clients/run.js +19 -14
- package/dist/src/conformance.d.ts +3 -4
- package/dist/src/conformance.js +396 -203
- package/dist/src/consumer.d.ts +22 -1
- package/dist/src/consumer.js +91 -26
- package/dist/src/create-pack.d.ts +1 -1
- package/dist/src/create-pack.js +13 -12
- package/dist/src/credential-issuer-import.d.ts +3 -0
- package/dist/src/credential-issuer-import.js +42 -0
- package/dist/src/derive.js +6 -3
- package/dist/src/gate.js +4 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/pack-facts.d.ts +3 -0
- package/dist/src/pack-facts.js +36 -1
- package/dist/src/protocol-3.js +13 -8
- package/dist/src/spec-documents.d.ts +4 -0
- package/dist/src/spec-documents.js +110 -3
- package/dist/src/spec-ir-client.js +1 -1
- package/dist/src/spec-ir-commands.js +1 -1
- package/dist/src/spec-ir-lines.js +1 -1
- package/dist/src/spec-ir-proto.d.ts +1 -1
- package/dist/src/spec-patches.js +2 -2
- package/dist/src/tools/judge-brief.js +1 -2
- package/dist/src/tools/life-coverage-walk.js +38 -26
- package/dist/src/tools/life-coverage.js +2 -2
- package/dist/src/tools/packs-root.js +3 -3
- package/dist/src/tools/score-pack.js +7 -7
- package/dist/src/tools/vendor-examples.js +95 -85
- package/dist/src/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +22 -14
- package/dist/src/walk/behavior-journey.js +199 -133
- package/dist/src/walk/journey-kit.d.ts +5 -4
- package/dist/src/walk/journey-kit.js +6 -2
- package/dist/src/walk/replay.d.ts +23 -0
- package/dist/src/walk/replay.js +118 -0
- package/dist/src/walk/serve-pack.d.ts +2 -2
- package/dist/src/walk/serve-pack.js +44 -2
- package/dist/src/walk/shape.d.ts +26 -5
- package/dist/src/walk/shape.js +167 -57
- package/dist/src/walk/vendor-fetch.d.ts +7 -2
- package/dist/src/walk/vendor-fetch.js +13 -2
- package/dist/src/walk/walk-database.d.ts +9 -1
- package/dist/src/walk/walk-database.js +35 -13
- package/dist/src/walk/xml-view.d.ts +3 -2
- package/dist/src/walk/xml-view.js +5 -4
- package/package.json +6 -4
- package/src/assessment.ts +12 -8
- package/src/browser-assessment.ts +12 -5
- package/src/check-sources.ts +2 -2
- package/src/clients/harness.ts +142 -19
- package/src/clients/jira.ts +4 -2
- package/src/clients/resend.first-use.json +2 -2
- package/src/clients/run.ts +17 -15
- package/src/conformance.ts +317 -147
- package/src/consumer.ts +83 -21
- package/src/create-pack.ts +13 -12
- package/src/credential-issuer-import.ts +38 -0
- package/src/derive.ts +6 -4
- package/src/gate.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lanes.ts +7 -0
- package/src/pack-facts.ts +25 -2
- package/src/protocol-3.ts +12 -8
- package/src/spec-documents.ts +92 -3
- package/src/spec-ir-client.ts +1 -1
- package/src/spec-ir-commands.ts +1 -1
- package/src/spec-ir-lines.ts +1 -1
- package/src/spec-ir-proto.ts +1 -1
- package/src/spec-patches.ts +2 -2
- package/src/tools/judge-brief.ts +1 -2
- package/src/tools/life-coverage-walk.ts +32 -22
- package/src/tools/life-coverage.ts +2 -2
- package/src/tools/packs-root.ts +3 -3
- package/src/tools/score-pack.ts +8 -8
- package/src/tools/vendor-examples.ts +92 -80
- package/src/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +158 -109
- package/src/walk/journey-kit.ts +9 -7
- package/src/walk/replay.ts +103 -0
- package/src/walk/serve-pack.ts +30 -5
- package/src/walk/shape.ts +157 -55
- package/src/walk/vendor-fetch.ts +16 -4
- package/src/walk/walk-database.ts +41 -15
- package/src/walk/xml-view.ts +5 -4
package/dist/src/protocol-3.js
CHANGED
|
@@ -25,6 +25,7 @@ import ts from 'typescript';
|
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { sharingLanesIn } from "./lanes.js";
|
|
27
27
|
import { isPackArtifact } from "./pack-files.js";
|
|
28
|
+
import { credentialIssuerImports } from "./credential-issuer-import.js";
|
|
28
29
|
// A path relative to another with '/' between its parts on every platform: git lists files that way, and every rule
|
|
29
30
|
// here compares and splits on '/'; Windows' relative() joins with '\', which hid every lane's files and spec.
|
|
30
31
|
const rel = (from, to) => relative(from, to).split(sep).join('/');
|
|
@@ -190,7 +191,7 @@ function noReport(unit) {
|
|
|
190
191
|
/** An operation's family: the first segment of its path after a version (`/v1/organizations/{id}` is `organizations`), a
|
|
191
192
|
* dotted RPC method's first part (`chat.postMessage` is `chat`), a line command's name (architecture, "Pack layout"). */
|
|
192
193
|
function familyOf(path) {
|
|
193
|
-
// the interim rule ("What an author writes"), until
|
|
194
|
+
// the interim rule ("What an author writes"), until the derivation writes the spec's own grouping on each operation
|
|
194
195
|
const segs = path.split('/').filter((x) => x && !/^v?\d+(\.\d+)?$|^\d{4}-\d{2}-\d{2}$/.test(x) && !x.startsWith('{'));
|
|
195
196
|
if (!segs.length)
|
|
196
197
|
return 'operations'; // named in a header: every path is `/`
|
|
@@ -289,7 +290,7 @@ const criteria = [
|
|
|
289
290
|
},
|
|
290
291
|
{
|
|
291
292
|
id: 'derived-reproducible', section: 'form',
|
|
292
|
-
asks: '
|
|
293
|
+
asks: 'twin-standard derive regenerates src/generated/ byte for byte',
|
|
293
294
|
check(unit) {
|
|
294
295
|
const gen = join(unit.dir, 'src', 'generated');
|
|
295
296
|
if (!existsSync(gen))
|
|
@@ -309,10 +310,10 @@ const criteria = [
|
|
|
309
310
|
// a standing written from one checkout reads the same from any other
|
|
310
311
|
if (run.status === null || run.signal !== null || run.error)
|
|
311
312
|
return { pass: false, cacheable: false,
|
|
312
|
-
reason: `derive
|
|
313
|
+
reason: `derive interrupted: status=${run.status} signal=${run.signal} error=${run.error?.message ?? ''}` };
|
|
313
314
|
if (run.status !== 0)
|
|
314
|
-
return { ...fail(`derive
|
|
315
|
-
return changed.length ? fail(`derive
|
|
315
|
+
return { ...fail(`derive refused: ${String(run.stderr || run.stdout).split(unit.dir).join('.').split(tmp).join('<out>').trim().split('\n').slice(-2).join(' ').slice(0, 200)}`), cacheable: run.status === 2 };
|
|
316
|
+
return changed.length ? fail(`derive writes differently: ${changed.slice(0, 5).join(', ')}${changed.length > 5 ? ` and ${changed.length - 5} more` : ''}`) : pass(`${filesUnder(gen).length} files reproduced`);
|
|
316
317
|
},
|
|
317
318
|
},
|
|
318
319
|
{
|
|
@@ -387,7 +388,7 @@ const criteria = [
|
|
|
387
388
|
},
|
|
388
389
|
{
|
|
389
390
|
id: 'handler-contract', section: 'form',
|
|
390
|
-
asks: 'every file in semantics/ reaches no randomness, clock, environment, network or module but the kernel, the states
|
|
391
|
+
asks: 'every file in semantics/ reaches no randomness, clock, environment, network or module but the kernel, the states, the shared helpers and a public subpath of its declared credential issuer pinned at an exact version as a direct runtime dependency; each handler file is its family\'s and exports only its operations, named by the fixed rule',
|
|
391
392
|
check(unit) {
|
|
392
393
|
const dir = join(unit.dir, 'src', 'semantics');
|
|
393
394
|
if (!existsSync(dir))
|
|
@@ -395,6 +396,7 @@ const criteria = [
|
|
|
395
396
|
const ops = operationsOf(unit);
|
|
396
397
|
const families = new Set(ops.values());
|
|
397
398
|
const found = [];
|
|
399
|
+
const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
|
|
398
400
|
for (const f of filesUnder(dir).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x))) {
|
|
399
401
|
const text = stripComments(readFileSync(join(dir, f), 'utf8'));
|
|
400
402
|
const raw = readFileSync(join(dir, f), 'utf8');
|
|
@@ -413,7 +415,7 @@ const criteria = [
|
|
|
413
415
|
const engineModule = /^\.\.\/engine\/[A-Za-z0-9_-]+\.ts$/.test(from) || vendorPath(unit, from, 3, /^engine\/[A-Za-z0-9_-]+\.ts$/);
|
|
414
416
|
// a lane over its vendor's state takes the vendor's states and shared helpers ("Pack layout")
|
|
415
417
|
const vendorShared = vendorPath(unit, from, 3, /^(semantics\/(states|shared)|manifest)\.ts$/);
|
|
416
|
-
if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared)
|
|
418
|
+
if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared && !issuerImport(from))
|
|
417
419
|
found.push(`${f} imports ${from}`);
|
|
418
420
|
}
|
|
419
421
|
// the handler map is typed over the narrow context, so the compiler holds every handler it names to the contract
|
|
@@ -546,12 +548,13 @@ const criteria = [
|
|
|
546
548
|
},
|
|
547
549
|
{
|
|
548
550
|
id: 'kernel-contract', section: 'form',
|
|
549
|
-
asks: "every file of src/ imports the kernel through its root, the pack contract, and nothing past it: no `@volter/world-core/runtime` or other entry (cli.ts alone takes args and lifecycle), no node builtin, no other package but @volter/world-ui and the generated GraphQL library loader",
|
|
551
|
+
asks: "every file of src/ imports the kernel through its root, the pack contract, and nothing past it: no `@volter/world-core/runtime` or other entry (cli.ts alone takes args and lifecycle), no node builtin, no other package but @volter/world-ui, a public subpath of its declared credential issuer pinned at an exact version as a direct runtime dependency, and the generated GraphQL library loader",
|
|
550
552
|
check(unit) {
|
|
551
553
|
const src = join(unit.dir, 'src');
|
|
552
554
|
if (!existsSync(src))
|
|
553
555
|
return fail('src/ is missing');
|
|
554
556
|
const found = [];
|
|
557
|
+
const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
|
|
555
558
|
for (const f of filesUnder(src).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x) && !x.startsWith('generated/'))) {
|
|
556
559
|
// the file's imports, re-exports and dynamic imports as TypeScript reads them (a string that only reads like one,
|
|
557
560
|
// an error message's "from '<name>'", is none)
|
|
@@ -596,6 +599,8 @@ const criteria = [
|
|
|
596
599
|
continue;
|
|
597
600
|
if (from === '@volter/world-core' || from === '@volter/world-ui')
|
|
598
601
|
continue;
|
|
602
|
+
if (issuerImport(from))
|
|
603
|
+
continue;
|
|
599
604
|
if (f === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle'))
|
|
600
605
|
continue;
|
|
601
606
|
found.push(`${f} imports ${from}`);
|
|
@@ -4,6 +4,10 @@ export type SpecDocuments = {
|
|
|
4
4
|
};
|
|
5
5
|
/** The documents of `spec/openapi/`, by name, or undefined when the pack vendors one spec file. */
|
|
6
6
|
export declare function readSpecDocuments(specDir: string): SpecDocuments | undefined;
|
|
7
|
+
/** Response declarations as served, independent of their representation media or generated surface version. */
|
|
8
|
+
export declare function declaredResponseStatuses(specDir: string): Map<string, Set<string>>;
|
|
9
|
+
/** Declared schema fields, including nested objects that are not inferred as generated resources. */
|
|
10
|
+
export declare function schemaFieldNames(specDir: string): Set<string>;
|
|
7
11
|
/** The union of the documents as one OpenAPI document; each contradiction left unpatched is named, by pointer. */
|
|
8
12
|
export declare function mergeDocuments(docs: SpecDocuments): {
|
|
9
13
|
doc: Json;
|
|
@@ -9,6 +9,13 @@ import { parse as parseYaml } from 'yaml';
|
|
|
9
9
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { gunzipSync } from 'node:zlib';
|
|
12
|
+
import { applySpecPatch } from "./spec-patches.js";
|
|
13
|
+
import { slug } from "./spec-ir.js";
|
|
14
|
+
function readDocument(file) {
|
|
15
|
+
const bytes = readFileSync(file);
|
|
16
|
+
const text = (file.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
17
|
+
return /\.ya?ml(\.gz)?$/.test(file) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_, value) => typeof value === 'bigint' ? exactInteger(String(value)) : value)) : parseExactJson(text);
|
|
18
|
+
}
|
|
12
19
|
/** The documents of `spec/openapi/`, by name, or undefined when the pack vendors one spec file. */
|
|
13
20
|
export function readSpecDocuments(specDir) {
|
|
14
21
|
const dir = join(specDir, 'openapi');
|
|
@@ -16,14 +23,114 @@ export function readSpecDocuments(specDir) {
|
|
|
16
23
|
return undefined;
|
|
17
24
|
const documents = {};
|
|
18
25
|
for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
|
|
19
|
-
|
|
20
|
-
const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
21
|
-
documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = /\.ya?ml(\.gz)?$/.test(f) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_, v) => typeof v === 'bigint' ? exactInteger(String(v)) : v)) : parseExactJson(text);
|
|
26
|
+
documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = readDocument(join(dir, f));
|
|
22
27
|
}
|
|
23
28
|
if (Object.keys(documents).length < 2)
|
|
24
29
|
throw new Error(`${dir}: one API in several documents needs at least two; a single document is spec/openapi.{json,yaml}`);
|
|
25
30
|
return { documents };
|
|
26
31
|
}
|
|
32
|
+
const openapiFiles = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml'];
|
|
33
|
+
function readPatchedDocument(specDir, filenames) {
|
|
34
|
+
const file = filenames.map((name) => join(specDir, name)).find(existsSync);
|
|
35
|
+
const documents = file === undefined ? readSpecDocuments(specDir) : undefined;
|
|
36
|
+
let doc = file === undefined ? documents : readDocument(file);
|
|
37
|
+
if (doc === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const patches = join(specDir, 'patches.json');
|
|
40
|
+
if (existsSync(patches))
|
|
41
|
+
for (const patch of readDocument(patches).patches)
|
|
42
|
+
applySpecPatch(doc, patch);
|
|
43
|
+
if (documents !== undefined) {
|
|
44
|
+
const merged = mergeDocuments(doc);
|
|
45
|
+
if (merged.contradictions.length)
|
|
46
|
+
throw new Error(`OpenAPI response declarations conflict: ${merged.contradictions.join('; ')}`);
|
|
47
|
+
doc = merged.doc;
|
|
48
|
+
}
|
|
49
|
+
return doc;
|
|
50
|
+
}
|
|
51
|
+
/** Response declarations as served, independent of their representation media or generated surface version. */
|
|
52
|
+
export function declaredResponseStatuses(specDir) {
|
|
53
|
+
const doc = readPatchedDocument(specDir, openapiFiles);
|
|
54
|
+
const statuses = new Map();
|
|
55
|
+
if (doc === undefined)
|
|
56
|
+
return statuses;
|
|
57
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
58
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
59
|
+
if (!operation || typeof operation !== 'object' || !operation.responses)
|
|
60
|
+
continue;
|
|
61
|
+
statuses.set(operation.operationId || slug(method, path), new Set(Object.keys(operation.responses)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return statuses;
|
|
65
|
+
}
|
|
66
|
+
/** Declared schema fields, including nested objects that are not inferred as generated resources. */
|
|
67
|
+
export function schemaFieldNames(specDir) {
|
|
68
|
+
const doc = readPatchedDocument(specDir, ['smithy.json.gz', 'smithy.json', ...openapiFiles, 'discovery.json.gz', 'discovery.json']);
|
|
69
|
+
const fields = new Set();
|
|
70
|
+
if (doc === undefined)
|
|
71
|
+
return fields;
|
|
72
|
+
for (const shape of Object.values(doc.shapes ?? {})) {
|
|
73
|
+
if (shape.type !== 'structure' && shape.type !== 'union')
|
|
74
|
+
continue;
|
|
75
|
+
for (const [name, member] of Object.entries(shape.members ?? {})) {
|
|
76
|
+
fields.add(name);
|
|
77
|
+
for (const trait of ['smithy.api#jsonName', 'smithy.api#xmlName']) {
|
|
78
|
+
const wireName = member.traits?.[trait];
|
|
79
|
+
if (typeof wireName === 'string')
|
|
80
|
+
fields.add(wireName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const schema = (node) => {
|
|
85
|
+
if (Array.isArray(node)) {
|
|
86
|
+
for (const item of node)
|
|
87
|
+
schema(item);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!node || typeof node !== 'object')
|
|
91
|
+
return;
|
|
92
|
+
for (const [name, property] of Object.entries(node.properties ?? {})) {
|
|
93
|
+
fields.add(name);
|
|
94
|
+
if (typeof property?.xml?.name === 'string')
|
|
95
|
+
fields.add(property.xml.name);
|
|
96
|
+
schema(property);
|
|
97
|
+
}
|
|
98
|
+
for (const keyword of ['$defs', 'definitions', 'patternProperties', 'dependentSchemas']) {
|
|
99
|
+
for (const child of Object.values(node[keyword] ?? {}))
|
|
100
|
+
schema(child);
|
|
101
|
+
}
|
|
102
|
+
for (const keyword of ['items', 'additionalItems', 'additionalProperties', 'unevaluatedProperties', 'unevaluatedItems', 'not', 'contains', 'propertyNames', 'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'prefixItems'])
|
|
103
|
+
schema(node[keyword]);
|
|
104
|
+
};
|
|
105
|
+
for (const catalog of [doc.components?.schemas, doc.definitions, doc.schemas]) {
|
|
106
|
+
for (const node of Object.values(catalog ?? {}))
|
|
107
|
+
schema(node);
|
|
108
|
+
}
|
|
109
|
+
const body = (node) => {
|
|
110
|
+
schema(node?.schema);
|
|
111
|
+
for (const media of Object.values(node?.content ?? {}))
|
|
112
|
+
schema(media?.schema);
|
|
113
|
+
};
|
|
114
|
+
for (const catalog of [doc.components?.responses, doc.components?.requestBodies, doc.components?.parameters, doc.components?.headers, doc.responses, doc.parameters]) {
|
|
115
|
+
for (const node of Object.values(catalog ?? {}))
|
|
116
|
+
body(node);
|
|
117
|
+
}
|
|
118
|
+
for (const paths of [doc.paths, doc.webhooks, doc.components?.pathItems]) {
|
|
119
|
+
for (const item of Object.values(paths ?? {})) {
|
|
120
|
+
for (const parameter of item.parameters ?? [])
|
|
121
|
+
body(parameter);
|
|
122
|
+
for (const method of ['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace']) {
|
|
123
|
+
const operation = item[method];
|
|
124
|
+
for (const parameter of operation?.parameters ?? [])
|
|
125
|
+
body(parameter);
|
|
126
|
+
body(operation?.requestBody);
|
|
127
|
+
for (const response of Object.values(operation?.responses ?? {}))
|
|
128
|
+
body(response);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return fields;
|
|
133
|
+
}
|
|
27
134
|
/** A node without its words (description, summary, examples, and the tags a document files it under): what two
|
|
28
135
|
* documents must agree on. */
|
|
29
136
|
const substance = (n) => JSON.stringify(n, (k, v) => (['description', 'summary', 'example', 'examples', 'x-mint', 'tags'].includes(k) ? undefined : v && typeof v === 'object' && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b))) : v));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `spec/`, Creating a pack, step 2): its method, its URL as the client
|
|
7
7
|
// builds it (a label is the Python expression the client interpolates), the query keys it sends (literal, or the keys
|
|
8
8
|
// of the dict it urlencodes) and its body's keys, each typed by the client's own annotation of the value (`null`
|
|
9
|
-
// where the client does not say). A spec patch corrects the table as it corrects a JSON spec (derive
|
|
9
|
+
// where the client does not say). A spec patch corrects the table as it corrects a JSON spec (twin-standard derive): what the
|
|
10
10
|
// client builds as a raw string (a token's scopes) comes from the vendor's reference page that way.
|
|
11
11
|
//
|
|
12
12
|
// The calls at one method and path are one operation, their query and body keys together. The client says nothing
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// its group, the version it arrived in, its arity, its arguments, its flags and its reply's schema); a pack whose wire
|
|
4
4
|
// carries Redis commands (Upstash's REST API: a command and its arguments in the path or the body) vendors the files of
|
|
5
5
|
// the commands its vendor serves and derives its command surface from them, as a line protocol's surface is its RFC's
|
|
6
|
-
// command table (spec-ir-lines.ts). A spec patch corrects the data as it corrects a JSON spec (derive
|
|
6
|
+
// command table (spec-ir-lines.ts). A spec patch corrects the data as it corrects a JSON spec (twin-standard derive).
|
|
7
7
|
function argumentOf(a) {
|
|
8
8
|
return {
|
|
9
9
|
name: String(a.name),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A line protocol's spec as data: the commands a client sends and the replies each may be answered,
|
|
2
2
|
// read from the RFC's own table of them. The IR a byte-stream pack derives its surface from, as an HTTP
|
|
3
|
-
// pack derives its from an OpenAPI document; a spec patch corrects it the same way (derive
|
|
3
|
+
// pack derives its from an OpenAPI document; a spec patch corrects it the same way (twin-standard derive).
|
|
4
4
|
//
|
|
5
5
|
// SMTP's table is RFC 5321 §4.3.2 "Command-Reply Sequences": each command, then its intermediate (I),
|
|
6
6
|
// success (S) and error (E) codes, where `I: 354 -> data -> S: 250` says what answers the lines that
|
|
@@ -40,7 +40,7 @@ export type ProtoFile = {
|
|
|
40
40
|
};
|
|
41
41
|
/** One `.proto` file's declarations. */
|
|
42
42
|
export declare function parseProto(path: string, text: string): ProtoFile;
|
|
43
|
-
/** The vendored tree (path → text) as data, so a spec patch corrects it as it corrects a JSON spec (derive
|
|
43
|
+
/** The vendored tree (path → text) as data, so a spec patch corrects it as it corrects a JSON spec (twin-standard derive). */
|
|
44
44
|
export type ProtoSpec = {
|
|
45
45
|
format: 'proto';
|
|
46
46
|
files: ProtoFile[];
|
package/dist/src/spec-patches.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A pack's corrections to its vendor's spec (spec/patches.json): RFC 6902 operations (add, replace, remove, move, copy),
|
|
2
|
-
// applied to the parsed spec so the vendored file stays the vendor's own bytes.
|
|
3
|
-
// IR;
|
|
2
|
+
// applied to the parsed spec so the vendored file stays the vendor's own bytes. the derivation applies them before the
|
|
3
|
+
// IR; `twin-standard examples` reads the patched schemas, which are what the twin is derived from.
|
|
4
4
|
/** Apply one patch to `doc` in place. */
|
|
5
5
|
export function applySpecPatch(doc, p) {
|
|
6
6
|
const at = (pointer) => {
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// runtime). The verdict names its judge and the authors; the grade refuses one whose judge is an author
|
|
4
4
|
// (packages/twin-standard/src/verdict.ts). One judging per pack: the verdict stands through later changes.
|
|
5
5
|
//
|
|
6
|
-
// twin-standard judge-brief <vendor> --authors <author name>[,<agent>...] (from the pack repository's root
|
|
7
|
-
// bun scripts/judge-brief.ts runs the same)
|
|
6
|
+
// twin-standard judge-brief <vendor> --authors <author name>[,<agent>...] (from the pack repository's root)
|
|
8
7
|
import { attributionName } from "../verdict.js";
|
|
9
8
|
import { createHash } from 'node:crypto';
|
|
10
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -16,7 +16,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { listActions, observeTransitions, stateOf } from '@volter/world-core/runtime';
|
|
18
18
|
import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
|
|
19
|
-
import { lanesOf, shapeChecker } from "../walk/shape.js";
|
|
19
|
+
import { lanesOf, lineShape, shapeChecker } from "../walk/shape.js";
|
|
20
20
|
import { sharingLanesIn } from "../lanes.js";
|
|
21
21
|
/** Registers, in the `bun test` run that imports it, the walks of `vendor` in the pack repository at `PACKS`. */
|
|
22
22
|
export async function coverageWalks(PACKS, vendor) {
|
|
@@ -36,9 +36,12 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
36
36
|
const { manifest } = (await import(__rewriteRelativeImportExtension(join(PACKS, vendor, 'src', 'manifest.ts'))));
|
|
37
37
|
// the lanes over the vendor's state its serve factory serves are walked by this life, their machines with the vendor's
|
|
38
38
|
// ("Pack layout"); two manifests naming one resource's field would collide in the keys below, so that refuses
|
|
39
|
-
const
|
|
39
|
+
const units = [{ dir: join(PACKS, vendor), manifest, lane: undefined }, ...await Promise.all(sharingLanes(vendor).map(async (l) => {
|
|
40
|
+
const dir = join(PACKS, vendor, l);
|
|
41
|
+
return { dir, lane: l, manifest: (await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')))).manifest };
|
|
42
|
+
}))];
|
|
40
43
|
// a vendor of lanes' root (a VendorManifest) declares no resources of its own: its lanes do
|
|
41
|
-
const allResources =
|
|
44
|
+
const allResources = units.flatMap(({ manifest: mf }) => Object.entries(mf.resources ?? {}));
|
|
42
45
|
const machines = new Map();
|
|
43
46
|
for (const [resource, decl] of allResources)
|
|
44
47
|
for (const [field, machine] of Object.entries(decl.state ?? {})) {
|
|
@@ -50,23 +53,28 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
50
53
|
machines.set(name, machine);
|
|
51
54
|
}
|
|
52
55
|
const where = new Map();
|
|
53
|
-
|
|
56
|
+
const httpTransitions = new Set();
|
|
57
|
+
for (const { dir, manifest: mf } of units) {
|
|
58
|
+
const http = !lineShape(dir);
|
|
54
59
|
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
55
60
|
for (const [field, m] of Object.entries(decl.state ?? {}))
|
|
56
61
|
m.transitions.forEach((t, i) => {
|
|
62
|
+
if (http)
|
|
63
|
+
httpTransitions.add(t);
|
|
57
64
|
const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
|
|
58
65
|
const places = where.get(t) ?? [];
|
|
59
66
|
if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service))
|
|
60
67
|
where.set(t, [...places, place]);
|
|
61
68
|
});
|
|
62
69
|
}
|
|
70
|
+
}
|
|
63
71
|
const moves = new Map();
|
|
64
72
|
const subjectKey = (service, type, id) => JSON.stringify([service, type, id]);
|
|
65
73
|
observeTransitions((_decl, t, outcome, from, entry) => {
|
|
66
74
|
const places = where.get(t) ?? [];
|
|
67
75
|
if (outcome === 'refused') {
|
|
68
|
-
//
|
|
69
|
-
if (entry)
|
|
76
|
+
// HTTP refusals are credited from their received status and body; a line protocol owns its session's replies.
|
|
77
|
+
if (entry || httpTransitions.has(t))
|
|
70
78
|
return;
|
|
71
79
|
const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
|
|
72
80
|
if (groups.size === 1)
|
|
@@ -100,11 +108,14 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
100
108
|
const shape = await shapeChecker(join(PACKS, vendor));
|
|
101
109
|
const answered = [];
|
|
102
110
|
try {
|
|
103
|
-
await walk(life, root, failures, { beats: w.beats, hooks: { after: (
|
|
111
|
+
await walk(life, root, failures, { beats: w.beats, hooks: { after: (_step, _path, answer) => {
|
|
104
112
|
// a line step's refusals are the machine's own (observeTransitions); a request's are read off its answer
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
113
|
+
const request = answer.request;
|
|
114
|
+
if (!request)
|
|
115
|
+
return;
|
|
116
|
+
const hit = shape?.operationOf(request.method, request.url, request.headers);
|
|
117
|
+
if (hit)
|
|
118
|
+
answered.push({ op: hit.op.id, lane: hit.lane, status: answer.status, body: answer.wireBody });
|
|
108
119
|
} } });
|
|
109
120
|
// a declared refusal is reached when a step on its operation was answered with it: its code, or its message
|
|
110
121
|
// up to the first placeholder
|
|
@@ -112,26 +123,27 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
112
123
|
const holds = (value, code) => (value !== null && typeof value === 'object'
|
|
113
124
|
? Object.values(value).some((v) => holds(v, code))
|
|
114
125
|
: (typeof value === 'string' || typeof value === 'number') && String(value) === code);
|
|
115
|
-
const answers = (a, r) => r.code !== undefined ? holds(a.body, String(r.code)) : a.
|
|
116
|
-
for (const
|
|
117
|
-
for (const [
|
|
118
|
-
m.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const answers = (a, r) => a.status === r.status && (r.code !== undefined ? holds(a.body, String(r.code)) : JSON.stringify(a.body)?.includes(r.message.split('{')[0].trim()) === true);
|
|
127
|
+
for (const { manifest: mf, lane } of units)
|
|
128
|
+
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
129
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) {
|
|
130
|
+
m.transitions.forEach((t, i) => {
|
|
131
|
+
if (!t.operation)
|
|
132
|
+
return;
|
|
133
|
+
const on = answered.filter((a) => a.op === t.operation && a.lane === lane);
|
|
134
|
+
if (t.refusal && on.some((a) => answers(a, t.refusal)))
|
|
135
|
+
seen.add(`${resource}.${field}#${i}|refused|*`);
|
|
136
|
+
for (const [from, r] of Object.entries(t.refusals ?? {}))
|
|
137
|
+
if (on.some((a) => answers(a, r)))
|
|
138
|
+
seen.add(`${resource}.${field}#${i}|refused|${from}`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
128
141
|
}
|
|
129
|
-
}
|
|
130
142
|
// every change the life made to a declared state field, from the World's log in order: a move is the declared
|
|
131
143
|
// transition that allows it (from its state to the next, by its operation when both name one); a change no
|
|
132
144
|
// transition allows is an undeclared move
|
|
133
145
|
const fieldsOf = new Map();
|
|
134
|
-
for (const mf of
|
|
146
|
+
for (const { manifest: mf } of units)
|
|
135
147
|
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
136
148
|
const key = JSON.stringify([mf.service ?? mf.vendor, decl.storedAs ?? resource]);
|
|
137
149
|
for (const [field, m] of Object.entries(decl.state ?? {}))
|
|
@@ -139,7 +151,7 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
139
151
|
}
|
|
140
152
|
const current = new Map();
|
|
141
153
|
// 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(
|
|
154
|
+
const services = [...new Set(units.map(({ manifest: mf }) => mf.service).filter((x) => typeof x === 'string'))];
|
|
143
155
|
if (!services.length)
|
|
144
156
|
services.push(manifest.vendor);
|
|
145
157
|
for (const a of services.flatMap((service) => listActions(service, root))) {
|
|
@@ -32,8 +32,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
32
32
|
// A walk that fails, and a file in scope (the pack's src/ on disk, its sharing lanes' too) that declares code yet
|
|
33
33
|
// measures no line, stop the measurement: neither is a file with nothing to reach.
|
|
34
34
|
//
|
|
35
|
-
// Usage: twin-standard coverage <vendor> [--write | --out <file>] [--prune] (from the pack repository's root
|
|
36
|
-
//
|
|
35
|
+
// Usage: twin-standard coverage <vendor> [--write | --out <file>] [--prune] (from the pack repository's root)
|
|
36
|
+
// (--prune removes the stale records) (--write keeps journeys/coverage.json)
|
|
37
37
|
import { spawnSync } from 'node:child_process';
|
|
38
38
|
import ts from 'typescript';
|
|
39
39
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Where the contributor's tools (walk, examples, coverage, judge-brief) find the pack they are given: a vendor's directory
|
|
2
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;
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// <vendor>`), so that root is the working directory; a tool that runs another in a process of its own (run-tool.ts)
|
|
4
|
+
// names it before loading the tool (setPacksRoot), with the kernel checkout a walk must measure (setKernelCheckout).
|
|
5
|
+
// Each tool reads both once, when it loads.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
let packs = process.cwd();
|
|
8
8
|
let kernel;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// life per pack is walked through the pack's own fetch on the in-memory World, and the report says:
|
|
4
4
|
// - the life: whether a judge's plausible verdict names its current content (a verdict its author wrote is not one);
|
|
5
5
|
// - correctness: every check on every step (status, expectations, captures, SHAPE, the beats) and the life
|
|
6
|
-
// answering
|
|
6
|
+
// answering the same on a second fresh World under its declared engine outputs;
|
|
7
7
|
// - coverage: the hand-written code the life and the vendor's published examples run and the declared state logic
|
|
8
8
|
// they reach (life-coverage.ts), with what they never reach;
|
|
9
9
|
// - the vendor's published examples: replayed and judged (vendor-examples.ts);
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
// This is source-journey evidence, not installed readiness or pack completion. pack-done owns completion and consumes
|
|
13
13
|
// this report alongside the exact artifact's installed customer assessment in the one final run.
|
|
14
14
|
//
|
|
15
|
-
// Usage: twin-standard walk <vendor> [--write] [--report <path>] (from the pack repository's root
|
|
16
|
-
// bun scripts/score-pack.ts <vendor> runs the same)
|
|
15
|
+
// Usage: twin-standard walk <vendor> [--write] [--report <path>] (from the pack repository's root)
|
|
17
16
|
// --write keeps journeys/score.json and journeys/coverage.json; --report writes the report and what it lacks for done
|
|
18
17
|
// to <path> as JSON and changes nothing in the checkout
|
|
19
18
|
import { parseExactJson } from '@volter/world-core/exact-json';
|
|
@@ -26,6 +25,7 @@ import { dirname, join, sep } from 'node:path';
|
|
|
26
25
|
import { inMemoryWorld, walk, withPinnedWorldDraws, usePacks } from "../walk/behavior-journey.js";
|
|
27
26
|
import { expandFills, waitMs } from "../walk/journey-kit.js";
|
|
28
27
|
import { vendorFetch, vendorStream } from "../walk/vendor-fetch.js";
|
|
28
|
+
import { compareReplays } from "../walk/replay.js";
|
|
29
29
|
import { existenceProbe } from "../walk/journey-kit.js";
|
|
30
30
|
import { kernelCheckout, packsRoot, toolPath } from "./packs-root.js";
|
|
31
31
|
const PACKS = packsRoot();
|
|
@@ -85,7 +85,7 @@ await walk(life, world.root(), failures, { beats: true });
|
|
|
85
85
|
const answers = async () => {
|
|
86
86
|
const seen = [];
|
|
87
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(),
|
|
88
|
+
await withPinnedWorldDraws(() => walk(life, world.root(), failures, { beats: false, hooks: { after: (step, path, answer) => seen.push({ name: step.name ?? '', path, status: answer.status, body: answer.body, ...(answer.sql ? { sql: answer.sql } : {}), ...(answer.connection ? { connection: answer.connection } : {}) }) } }));
|
|
89
89
|
return seen;
|
|
90
90
|
};
|
|
91
91
|
const [first, second] = [await answers(), await answers()];
|
|
@@ -184,9 +184,9 @@ const flags = [];
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail
|
|
187
|
+
const replayComparison = compareReplays(first, second);
|
|
188
|
+
for (const detail of replayComparison.diagnostics)
|
|
189
|
+
failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail });
|
|
190
190
|
// ── breadth ──────────────────────────────────────────────────────────────────────────────────
|
|
191
191
|
const factory = await vendorFetch(PACKS, vendor);
|
|
192
192
|
const fetch = factory?.(world.root());
|