@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/src/spec-documents.ts
CHANGED
|
@@ -10,24 +10,113 @@ import { parse as parseYaml } from 'yaml';
|
|
|
10
10
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { gunzipSync } from 'node:zlib';
|
|
13
|
+
import { applySpecPatch, type SpecPatch } from './spec-patches.ts';
|
|
14
|
+
import { slug } from './spec-ir.ts';
|
|
13
15
|
|
|
14
16
|
type Json = any;
|
|
15
17
|
export type SpecDocuments = { documents: Record<string, Json> };
|
|
16
18
|
|
|
19
|
+
function readDocument(file: string): Json {
|
|
20
|
+
const bytes = readFileSync(file);
|
|
21
|
+
const text = (file.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
22
|
+
return /\.ya?ml(\.gz)?$/.test(file) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_, value) => typeof value === 'bigint' ? exactInteger(String(value)) : value)) : parseExactJson(text);
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
/** The documents of `spec/openapi/`, by name, or undefined when the pack vendors one spec file. */
|
|
18
26
|
export function readSpecDocuments(specDir: string): SpecDocuments | undefined {
|
|
19
27
|
const dir = join(specDir, 'openapi');
|
|
20
28
|
if (!existsSync(dir)) return undefined;
|
|
21
29
|
const documents: Record<string, Json> = {};
|
|
22
30
|
for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
|
|
23
|
-
|
|
24
|
-
const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
25
|
-
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);
|
|
31
|
+
documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = readDocument(join(dir, f));
|
|
26
32
|
}
|
|
27
33
|
if (Object.keys(documents).length < 2) throw new Error(`${dir}: one API in several documents needs at least two; a single document is spec/openapi.{json,yaml}`);
|
|
28
34
|
return { documents };
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
const openapiFiles = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml'];
|
|
38
|
+
|
|
39
|
+
function readPatchedDocument(specDir: string, filenames: string[]): Json | undefined {
|
|
40
|
+
const file = filenames.map((name) => join(specDir, name)).find(existsSync);
|
|
41
|
+
const documents = file === undefined ? readSpecDocuments(specDir) : undefined;
|
|
42
|
+
let doc = file === undefined ? documents : readDocument(file);
|
|
43
|
+
if (doc === undefined) return undefined;
|
|
44
|
+
const patches = join(specDir, 'patches.json');
|
|
45
|
+
if (existsSync(patches)) for (const patch of (readDocument(patches) as { patches: SpecPatch[] }).patches) applySpecPatch(doc, patch);
|
|
46
|
+
if (documents !== undefined) {
|
|
47
|
+
const merged = mergeDocuments(doc);
|
|
48
|
+
if (merged.contradictions.length) throw new Error(`OpenAPI response declarations conflict: ${merged.contradictions.join('; ')}`);
|
|
49
|
+
doc = merged.doc;
|
|
50
|
+
}
|
|
51
|
+
return doc;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Response declarations as served, independent of their representation media or generated surface version. */
|
|
55
|
+
export function declaredResponseStatuses(specDir: string): Map<string, Set<string>> {
|
|
56
|
+
const doc = readPatchedDocument(specDir, openapiFiles);
|
|
57
|
+
const statuses = new Map<string, Set<string>>();
|
|
58
|
+
if (doc === undefined) return statuses;
|
|
59
|
+
for (const [path, item] of Object.entries(doc.paths ?? {}) as Array<[string, Record<string, { operationId?: string; responses?: Record<string, unknown> }>]>) {
|
|
60
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
61
|
+
if (!operation || typeof operation !== 'object' || !operation.responses) continue;
|
|
62
|
+
statuses.set(operation.operationId || slug(method, path), new Set(Object.keys(operation.responses)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return statuses;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Declared schema fields, including nested objects that are not inferred as generated resources. */
|
|
69
|
+
export function schemaFieldNames(specDir: string): Set<string> {
|
|
70
|
+
const doc = readPatchedDocument(specDir, ['smithy.json.gz', 'smithy.json', ...openapiFiles, 'discovery.json.gz', 'discovery.json']);
|
|
71
|
+
const fields = new Set<string>();
|
|
72
|
+
if (doc === undefined) return fields;
|
|
73
|
+
for (const shape of Object.values(doc.shapes ?? {}) as Json[]) {
|
|
74
|
+
if (shape.type !== 'structure' && shape.type !== 'union') continue;
|
|
75
|
+
for (const [name, member] of Object.entries(shape.members ?? {}) as Array<[string, Json]>) {
|
|
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') fields.add(wireName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const schema = (node: Json): void => {
|
|
84
|
+
if (Array.isArray(node)) { for (const item of node) schema(item); return; }
|
|
85
|
+
if (!node || typeof node !== 'object') return;
|
|
86
|
+
for (const [name, property] of Object.entries(node.properties ?? {}) as Array<[string, Json]>) {
|
|
87
|
+
fields.add(name);
|
|
88
|
+
if (typeof property?.xml?.name === 'string') fields.add(property.xml.name);
|
|
89
|
+
schema(property);
|
|
90
|
+
}
|
|
91
|
+
for (const keyword of ['$defs', 'definitions', 'patternProperties', 'dependentSchemas']) {
|
|
92
|
+
for (const child of Object.values(node[keyword] ?? {})) schema(child);
|
|
93
|
+
}
|
|
94
|
+
for (const keyword of ['items', 'additionalItems', 'additionalProperties', 'unevaluatedProperties', 'unevaluatedItems', 'not', 'contains', 'propertyNames', 'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'prefixItems']) schema(node[keyword]);
|
|
95
|
+
};
|
|
96
|
+
for (const catalog of [doc.components?.schemas, doc.definitions, doc.schemas]) {
|
|
97
|
+
for (const node of Object.values(catalog ?? {})) schema(node);
|
|
98
|
+
}
|
|
99
|
+
const body = (node: Json): void => {
|
|
100
|
+
schema(node?.schema);
|
|
101
|
+
for (const media of Object.values(node?.content ?? {}) as Json[]) schema(media?.schema);
|
|
102
|
+
};
|
|
103
|
+
for (const catalog of [doc.components?.responses, doc.components?.requestBodies, doc.components?.parameters, doc.components?.headers, doc.responses, doc.parameters]) {
|
|
104
|
+
for (const node of Object.values(catalog ?? {})) body(node);
|
|
105
|
+
}
|
|
106
|
+
for (const paths of [doc.paths, doc.webhooks, doc.components?.pathItems]) {
|
|
107
|
+
for (const item of Object.values(paths ?? {}) as Json[]) {
|
|
108
|
+
for (const parameter of item.parameters ?? []) body(parameter);
|
|
109
|
+
for (const method of ['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace']) {
|
|
110
|
+
const operation = item[method];
|
|
111
|
+
for (const parameter of operation?.parameters ?? []) body(parameter);
|
|
112
|
+
body(operation?.requestBody);
|
|
113
|
+
for (const response of Object.values(operation?.responses ?? {})) body(response);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return fields;
|
|
118
|
+
}
|
|
119
|
+
|
|
31
120
|
/** A node without its words (description, summary, examples, and the tags a document files it under): what two
|
|
32
121
|
* documents must agree on. */
|
|
33
122
|
const substance = (n: Json): string => 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));
|
package/src/spec-ir-client.ts
CHANGED
|
@@ -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
|
package/src/spec-ir-commands.ts
CHANGED
|
@@ -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
|
|
|
8
8
|
export type CommandArgument = { name: string; type: string; optional?: boolean; multiple?: boolean; token?: string; arguments?: CommandArgument[] };
|
|
9
9
|
/** A command: `id` as a client names it (`SET`, `CLIENT GETNAME` for a container's subcommand); `arity` as Redis gives
|
package/src/spec-ir-lines.ts
CHANGED
|
@@ -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
|
package/src/spec-ir-proto.ts
CHANGED
|
@@ -161,7 +161,7 @@ export function parseProto(path: string, text: string): ProtoFile {
|
|
|
161
161
|
return file;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** The vendored tree (path → text) as data, so a spec patch corrects it as it corrects a JSON spec (derive
|
|
164
|
+
/** The vendored tree (path → text) as data, so a spec patch corrects it as it corrects a JSON spec (twin-standard derive). */
|
|
165
165
|
export type ProtoSpec = { format: 'proto'; files: ProtoFile[] };
|
|
166
166
|
export function readProtoFiles(files: Record<string, string>): ProtoSpec {
|
|
167
167
|
return { format: 'proto', files: Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([path, text]) => parseProto(path, text)) };
|
package/src/spec-patches.ts
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
|
|
|
5
5
|
export type SpecPatch = { op: string; path: string; from?: string; value?: unknown };
|
|
6
6
|
|
package/src/tools/judge-brief.ts
CHANGED
|
@@ -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.ts';
|
|
9
8
|
import { createHash } from 'node:crypto';
|
|
10
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -6,9 +6,9 @@ import { parseExactJson } from '@volter/world-core/exact-json';
|
|
|
6
6
|
// is its precondition, not what it measures. This module is no test file itself, so a published package holds it.
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
-
import { listActions, observeTransitions, stateOf, type Actor, type DerivedManifest, type Transition } from '@volter/world-core/runtime';
|
|
9
|
+
import { listActions, observeTransitions, stateOf, type Actor, type DerivedManifest, type ErrorSpec, type Transition } from '@volter/world-core/runtime';
|
|
10
10
|
import { inMemoryWorld, walk, type Failure, usePacks } from '../walk/behavior-journey.ts';
|
|
11
|
-
import { lanesOf, shapeChecker } from '../walk/shape.ts';
|
|
11
|
+
import { lanesOf, lineShape, shapeChecker } from '../walk/shape.ts';
|
|
12
12
|
import { sharingLanesIn } from '../lanes.ts';
|
|
13
13
|
|
|
14
14
|
/** Registers, in the `bun test` run that imports it, the walks of `vendor` in the pack repository at `PACKS`. */
|
|
@@ -30,9 +30,12 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
|
|
|
30
30
|
const { manifest } = (await import(join(PACKS, vendor, 'src', 'manifest.ts'))) as { manifest: DerivedManifest };
|
|
31
31
|
// the lanes over the vendor's state its serve factory serves are walked by this life, their machines with the vendor's
|
|
32
32
|
// ("Pack layout"); two manifests naming one resource's field would collide in the keys below, so that refuses
|
|
33
|
-
const
|
|
33
|
+
const units = [{ dir: join(PACKS, vendor), manifest, lane: undefined }, ...await Promise.all(sharingLanes(vendor).map(async (l) => {
|
|
34
|
+
const dir = join(PACKS, vendor, l);
|
|
35
|
+
return { dir, lane: l, manifest: ((await import(join(dir, 'src', 'manifest.ts'))) as { manifest: DerivedManifest }).manifest };
|
|
36
|
+
}))];
|
|
34
37
|
// a vendor of lanes' root (a VendorManifest) declares no resources of its own: its lanes do
|
|
35
|
-
const allResources =
|
|
38
|
+
const allResources = units.flatMap(({ manifest: mf }) => Object.entries(mf.resources ?? {}));
|
|
36
39
|
const machines = new Map<string, unknown>();
|
|
37
40
|
for (const [resource, decl] of allResources) for (const [field, machine] of Object.entries(decl.state ?? {})) {
|
|
38
41
|
const name = `${resource}.${field}`;
|
|
@@ -42,20 +45,25 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
|
|
|
42
45
|
machines.set(name, machine);
|
|
43
46
|
}
|
|
44
47
|
const where = new Map<Transition, Array<{ at: string; field: string; type: string; service: string }>>();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const httpTransitions = new Set<Transition>();
|
|
49
|
+
for (const { dir, manifest: mf } of units) {
|
|
50
|
+
const http = !lineShape(dir);
|
|
51
|
+
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
52
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) m.transitions.forEach((t, i) => {
|
|
53
|
+
if (http) httpTransitions.add(t);
|
|
54
|
+
const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
|
|
55
|
+
const places = where.get(t) ?? [];
|
|
56
|
+
if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service)) where.set(t, [...places, place]);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
const moves = new Map<string, Array<{ transition: Transition; from: string; field: string; actor: Actor; subject: string }>>();
|
|
53
61
|
const subjectKey = (service: string, type: string, id: string): string => JSON.stringify([service, type, id]);
|
|
54
62
|
observeTransitions((_decl, t, outcome, from, entry) => {
|
|
55
63
|
const places = where.get(t) ?? [];
|
|
56
64
|
if (outcome === 'refused') {
|
|
57
|
-
//
|
|
58
|
-
if (entry) return;
|
|
65
|
+
// HTTP refusals are credited from their received status and body; a line protocol owns its session's replies.
|
|
66
|
+
if (entry || httpTransitions.has(t)) return;
|
|
59
67
|
const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
|
|
60
68
|
if (groups.size === 1) for (const p of places) seen.add(`${p.at}|refused|${from}`);
|
|
61
69
|
return;
|
|
@@ -79,12 +87,14 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
|
|
|
79
87
|
const root = world.root();
|
|
80
88
|
// what each step was answered, by the spec operation it called: a refusal is seen at the wire, whichever code gave it
|
|
81
89
|
const shape = await shapeChecker(join(PACKS, vendor));
|
|
82
|
-
const answered: Array<{ op: string;
|
|
90
|
+
const answered: Array<{ op: string; lane?: string; status: number; body: unknown }> = [];
|
|
83
91
|
try {
|
|
84
|
-
await walk(life, root, failures, { beats: w.beats, hooks: { after: (
|
|
92
|
+
await walk(life, root, failures, { beats: w.beats, hooks: { after: (_step, _path, answer) => {
|
|
85
93
|
// a line step's refusals are the machine's own (observeTransitions); a request's are read off its answer
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
94
|
+
const request = answer.request;
|
|
95
|
+
if (!request) return;
|
|
96
|
+
const hit = shape?.operationOf(request.method, request.url, request.headers);
|
|
97
|
+
if (hit) answered.push({ op: hit.op.id, lane: hit.lane, status: answer.status, body: answer.wireBody });
|
|
88
98
|
} } });
|
|
89
99
|
// a declared refusal is reached when a step on its operation was answered with it: its code, or its message
|
|
90
100
|
// up to the first placeholder
|
|
@@ -92,13 +102,13 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
|
|
|
92
102
|
const holds = (value: unknown, code: string): boolean => (value !== null && typeof value === 'object'
|
|
93
103
|
? Object.values(value as Record<string, unknown>).some((v) => holds(v, code))
|
|
94
104
|
: (typeof value === 'string' || typeof value === 'number') && String(value) === code);
|
|
95
|
-
const answers = (a: {
|
|
96
|
-
r.code !== undefined ? holds(a.body, String(r.code)) : a.
|
|
97
|
-
for (const [resource, decl] of
|
|
105
|
+
const answers = (a: { status: number; body: unknown }, r: ErrorSpec): boolean =>
|
|
106
|
+
a.status === r.status && (r.code !== undefined ? holds(a.body, String(r.code)) : JSON.stringify(a.body)?.includes(r.message.split('{')[0]!.trim()) === true);
|
|
107
|
+
for (const { manifest: mf, lane } of units) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
98
108
|
for (const [field, m] of Object.entries(decl.state ?? {})) {
|
|
99
109
|
m.transitions.forEach((t, i) => {
|
|
100
110
|
if (!t.operation) return;
|
|
101
|
-
const on = answered.filter((a) => a.op === t.operation);
|
|
111
|
+
const on = answered.filter((a) => a.op === t.operation && a.lane === lane);
|
|
102
112
|
if (t.refusal && on.some((a) => answers(a, t.refusal!))) seen.add(`${resource}.${field}#${i}|refused|*`);
|
|
103
113
|
for (const [from, r] of Object.entries(t.refusals ?? {})) if (on.some((a) => answers(a, r))) seen.add(`${resource}.${field}#${i}|refused|${from}`);
|
|
104
114
|
});
|
|
@@ -108,13 +118,13 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
|
|
|
108
118
|
// transition that allows it (from its state to the next, by its operation when both name one); a change no
|
|
109
119
|
// transition allows is an undeclared move
|
|
110
120
|
const fieldsOf = new Map<string, Array<[string, string, (typeof manifest.resources)[string]['state'] extends infer S ? S extends Record<string, infer F> ? F : never : never]>>();
|
|
111
|
-
for (const mf of
|
|
121
|
+
for (const { manifest: mf } of units) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
112
122
|
const key = JSON.stringify([mf.service ?? mf.vendor, decl.storedAs ?? resource]);
|
|
113
123
|
for (const [field, m] of Object.entries(decl.state ?? {})) fieldsOf.set(key, [...(fieldsOf.get(key) ?? []), [resource, field, m]]);
|
|
114
124
|
}
|
|
115
125
|
const current = new Map<string, Record<string, unknown>>();
|
|
116
126
|
// the services the life wrote: the vendor's, or its lanes' (a vendor of lanes' root names none of its own)
|
|
117
|
-
const services = [...new Set(
|
|
127
|
+
const services = [...new Set(units.map(({ manifest: mf }) => mf.service).filter((x): x is string => typeof x === 'string'))];
|
|
118
128
|
if (!services.length) services.push(manifest.vendor);
|
|
119
129
|
for (const a of services.flatMap((service) => listActions(service, root))) {
|
|
120
130
|
if (a.op !== 'set' || !a.fields) continue;
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
// A walk that fails, and a file in scope (the pack's src/ on disk, its sharing lanes' too) that declares code yet
|
|
25
25
|
// measures no line, stop the measurement: neither is a file with nothing to reach.
|
|
26
26
|
//
|
|
27
|
-
// Usage: twin-standard coverage <vendor> [--write | --out <file>] [--prune] (from the pack repository's root
|
|
28
|
-
//
|
|
27
|
+
// Usage: twin-standard coverage <vendor> [--write | --out <file>] [--prune] (from the pack repository's root)
|
|
28
|
+
// (--prune removes the stale records) (--write keeps journeys/coverage.json)
|
|
29
29
|
import { spawnSync } from 'node:child_process';
|
|
30
30
|
import ts from 'typescript';
|
|
31
31
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
package/src/tools/packs-root.ts
CHANGED
|
@@ -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
|
|
|
8
8
|
let packs = process.cwd();
|
package/src/tools/score-pack.ts
CHANGED
|
@@ -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
|
|
|
@@ -27,6 +26,7 @@ import { dirname, join, sep } from 'node:path';
|
|
|
27
26
|
import { inMemoryWorld, walk, withPinnedWorldDraws, type Failure, usePacks } from '../walk/behavior-journey.ts';
|
|
28
27
|
import { expandFills, waitMs, type Journey } from '../walk/journey-kit.ts';
|
|
29
28
|
import { vendorFetch, vendorStream } from '../walk/vendor-fetch.ts';
|
|
29
|
+
import { compareReplays, type ReplayStep } from '../walk/replay.ts';
|
|
30
30
|
import { existenceProbe } from '../walk/journey-kit.ts';
|
|
31
31
|
import { kernelCheckout, packsRoot, toolPath } from './packs-root.ts';
|
|
32
32
|
const PACKS = packsRoot();
|
|
@@ -73,10 +73,10 @@ const judged = life.steps.length === 0 || !verdict ? 'unjudged' : dependent ? de
|
|
|
73
73
|
const world = inMemoryWorld(`score-${vendor}`);
|
|
74
74
|
const failures: Failure[] = [];
|
|
75
75
|
await walk(life, world.root(), failures, { beats: true });
|
|
76
|
-
const answers = async (): Promise<
|
|
77
|
-
const seen:
|
|
76
|
+
const answers = async (): Promise<ReplayStep[]> => {
|
|
77
|
+
const seen: ReplayStep[] = [];
|
|
78
78
|
// the World's own draws (its seed, its keys) are the same in both: two Worlds differ only by what is beyond them (R9)
|
|
79
|
-
await withPinnedWorldDraws(() => walk(life, world.root(),
|
|
79
|
+
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 } : {}) }) } }));
|
|
80
80
|
return seen;
|
|
81
81
|
};
|
|
82
82
|
const [first, second] = [await answers(), await answers()];
|
|
@@ -166,8 +166,8 @@ const flags: Flag[] = [];
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
169
|
+
const replayComparison = compareReplays(first, second);
|
|
170
|
+
for (const detail of replayComparison.diagnostics) failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail });
|
|
171
171
|
|
|
172
172
|
// ── breadth ──────────────────────────────────────────────────────────────────────────────────
|
|
173
173
|
const factory = await vendorFetch(PACKS, vendor);
|