@volter/twin-standard 1.0.29 → 1.0.31
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/dist/src/archive-source.d.ts +13 -0
- package/dist/src/archive-source.js +176 -0
- package/dist/src/check-sources.d.ts +19 -2
- package/dist/src/check-sources.js +95 -37
- package/dist/src/cli.js +52 -9
- package/dist/src/clients/ai-gateway.d.ts +1 -0
- package/dist/src/clients/ai-gateway.js +3 -0
- package/dist/src/clients/anthropic-messages.d.ts +10 -0
- package/dist/src/clients/anthropic-messages.js +52 -0
- package/dist/src/clients/anthropic.d.ts +1 -0
- package/dist/src/clients/anthropic.js +3 -0
- package/dist/src/clients/aws.d.ts +2 -0
- package/dist/src/clients/aws.js +137 -0
- package/dist/src/clients/cerebras.d.ts +1 -0
- package/dist/src/clients/cerebras.js +3 -0
- package/dist/src/clients/clerk.d.ts +2 -0
- package/dist/src/clients/clerk.js +99 -0
- package/dist/src/clients/cloudflare.d.ts +2 -0
- package/dist/src/clients/cloudflare.js +86 -0
- package/dist/src/clients/elevenlabs.d.ts +2 -0
- package/dist/src/clients/elevenlabs.js +39 -0
- package/dist/src/clients/gemini.d.ts +2 -0
- package/dist/src/clients/gemini.js +92 -0
- package/dist/src/clients/github.d.ts +2 -0
- package/dist/src/clients/github.js +71 -0
- package/dist/src/clients/groq.d.ts +1 -0
- package/dist/src/clients/groq.js +3 -0
- package/dist/src/clients/harness.d.ts +36 -0
- package/dist/src/clients/harness.js +74 -0
- package/dist/src/clients/merge.d.ts +1 -0
- package/dist/src/clients/merge.js +8 -0
- package/dist/src/clients/openai-compatible.d.ts +9 -0
- package/dist/src/clients/openai-compatible.js +51 -0
- package/dist/src/clients/openai.d.ts +2 -0
- package/dist/src/clients/openai.js +65 -0
- package/dist/src/clients/openrouter.d.ts +1 -0
- package/dist/src/clients/openrouter.js +3 -0
- package/dist/src/clients/perplexity.d.ts +1 -0
- package/dist/src/clients/perplexity.js +3 -0
- package/dist/src/clients/planetscale.d.ts +2 -0
- package/dist/src/clients/planetscale.js +109 -0
- package/dist/src/clients/posthog.d.ts +2 -0
- package/dist/src/clients/posthog.js +145 -0
- package/dist/src/clients/run.d.ts +1 -0
- package/dist/src/clients/run.js +25 -0
- package/dist/src/clients/sentry.d.ts +2 -0
- package/dist/src/clients/sentry.js +167 -0
- package/dist/src/clients/slack.d.ts +2 -0
- package/dist/src/clients/slack.js +107 -0
- package/dist/src/clients/stripe.d.ts +2 -0
- package/dist/src/clients/stripe.js +161 -0
- package/dist/src/clients/tinybird.d.ts +2 -0
- package/dist/src/clients/tinybird.js +71 -0
- package/dist/src/clients/tunnel.d.ts +2 -0
- package/dist/src/clients/tunnel.js +99 -0
- package/dist/src/clients/upstash.d.ts +2 -0
- package/dist/src/clients/upstash.js +143 -0
- package/dist/src/clients/volteridentity.d.ts +2 -0
- package/dist/src/clients/volteridentity.js +128 -0
- package/dist/src/clients/xai.d.ts +2 -0
- package/dist/src/clients/xai.js +23 -0
- package/dist/src/conformance.d.ts +33 -0
- package/dist/src/conformance.js +520 -0
- package/dist/src/derive.js +113 -22
- package/dist/src/gate.js +24 -17
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +5 -3
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/p3-rules.js +0 -2
- package/dist/src/pack-files.d.ts +2 -0
- package/dist/src/pack-files.js +4 -0
- package/dist/src/protocol-3.d.ts +1 -0
- package/dist/src/protocol-3.js +83 -24
- package/dist/src/published.d.ts +6 -0
- package/dist/src/published.js +65 -11
- package/dist/src/spec-documents.js +2 -1
- package/dist/src/spec-ir-client.d.ts +5 -1
- package/dist/src/spec-ir-client.js +12 -2
- package/dist/src/spec-ir.d.ts +7 -2
- package/dist/src/spec-ir.js +42 -10
- package/dist/src/verdict.d.ts +14 -0
- package/dist/src/verdict.js +30 -0
- package/dist/src/walk/behavior-journey.d.ts +47 -0
- package/dist/src/walk/behavior-journey.js +1138 -0
- package/dist/src/walk/git-client.d.ts +26 -0
- package/dist/src/walk/git-client.js +139 -0
- package/dist/src/walk/journey-kit.d.ts +445 -0
- package/dist/src/walk/journey-kit.js +743 -0
- package/dist/src/walk/shape.d.ts +54 -0
- package/dist/src/walk/shape.js +210 -0
- package/dist/src/walk/sse-example.d.ts +2 -0
- package/dist/src/walk/sse-example.js +32 -0
- package/dist/src/walk/vendor-fetch.d.ts +20 -0
- package/dist/src/walk/vendor-fetch.js +92 -0
- package/dist/src/walk/walk-database.d.ts +12 -0
- package/dist/src/walk/walk-database.js +96 -0
- package/dist/src/walk/xml-view.d.ts +2 -0
- package/dist/src/walk/xml-view.js +55 -0
- package/package.json +39 -3
- package/src/archive-source.ts +141 -0
- package/src/check-sources.ts +83 -33
- package/src/cli.ts +40 -8
- package/src/clients/ai-gateway.ts +4 -0
- package/src/clients/anthropic-messages.ts +60 -0
- package/src/clients/anthropic.ts +4 -0
- package/src/clients/aws.ts +119 -0
- package/src/clients/cerebras.ts +4 -0
- package/src/clients/clerk.ts +97 -0
- package/src/clients/cloudflare.ts +80 -0
- package/src/clients/elevenlabs.ts +38 -0
- package/src/clients/gemini.ts +89 -0
- package/src/clients/github.ts +67 -0
- package/src/clients/groq.ts +4 -0
- package/src/clients/harness.ts +101 -0
- package/src/clients/merge.ts +9 -0
- package/src/clients/openai-compatible.ts +59 -0
- package/src/clients/openai.ts +66 -0
- package/src/clients/openrouter.ts +4 -0
- package/src/clients/perplexity.ts +4 -0
- package/src/clients/planetscale.ts +98 -0
- package/src/clients/posthog.ts +132 -0
- package/src/clients/run.ts +18 -0
- package/src/clients/sentry.ts +157 -0
- package/src/clients/slack.ts +98 -0
- package/src/clients/stripe.ts +129 -0
- package/src/clients/tinybird.ts +69 -0
- package/src/clients/tunnel.ts +79 -0
- package/src/clients/upstash.ts +137 -0
- package/src/clients/volteridentity.ts +108 -0
- package/src/clients/xai.ts +25 -0
- package/src/conformance.ts +446 -0
- package/src/derive.ts +87 -16
- package/src/gate.ts +22 -15
- package/src/index.ts +5 -3
- package/src/lanes.ts +7 -0
- package/src/p3-rules.ts +0 -2
- package/src/pack-files.ts +4 -0
- package/src/protocol-3.ts +77 -22
- package/src/published.ts +59 -11
- package/src/spec-documents.ts +2 -1
- package/src/spec-ir-client.ts +15 -4
- package/src/spec-ir.ts +43 -5
- package/src/verdict.ts +28 -0
- package/src/walk/behavior-journey.ts +806 -0
- package/src/walk/git-client.ts +138 -0
- package/src/walk/journey-kit.ts +858 -0
- package/src/walk/shape.ts +208 -0
- package/src/walk/sse-example.ts +25 -0
- package/src/walk/vendor-fetch.ts +80 -0
- package/src/walk/walk-database.ts +97 -0
- package/src/walk/xml-view.ts +44 -0
package/dist/src/protocol-3.js
CHANGED
|
@@ -12,7 +12,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
12
12
|
// Version 1 checks what the repository's tools can decide today. Criteria the grade section names that no tool
|
|
13
13
|
// decides yet (a credential stored as issued; hollow capabilities; the invariants row) are listed in PLANNED and
|
|
14
14
|
// counted in no grade until a version of this standard checks them.
|
|
15
|
+
import { unpublishedUnitFiles } from "./conformance.js";
|
|
16
|
+
import { attributionName, notIndependent } from "./verdict.js";
|
|
15
17
|
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
19
|
import { createHash } from 'node:crypto';
|
|
17
20
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
18
21
|
import { tmpdir } from 'node:os';
|
|
@@ -21,6 +24,7 @@ import { gunzipSync } from 'node:zlib';
|
|
|
21
24
|
import ts from 'typescript';
|
|
22
25
|
import { fileURLToPath } from 'node:url';
|
|
23
26
|
import { sharingLanesIn } from "./lanes.js";
|
|
27
|
+
import { isPackArtifact } from "./pack-files.js";
|
|
24
28
|
/** The lanes over the unit's vendor's state, when the unit is one of them ("Pack layout"), else none: a lane over its
|
|
25
29
|
* vendor's state takes the vendor's states, shared helpers, manifest and engine from the vendor's src/, and nothing of
|
|
26
30
|
* another lane. */
|
|
@@ -49,31 +53,69 @@ catch {
|
|
|
49
53
|
} };
|
|
50
54
|
/** What spec/ holds beside the vendored documents: provenance, corrections, the grouping, the examples index, recordings. */
|
|
51
55
|
const SPEC_METADATA = /^(SOURCE\.md|sources\.json|patches\.json|grouping\.json|doc-examples\.json|README\.md|[^/]*\.sha256|recordings\/.*)$/;
|
|
52
|
-
/** A tool in spec/ is not a vendored document
|
|
56
|
+
/** A tool in spec/ is not a vendored document: never hashed, and outside the layout unless it is the script that wrote
|
|
57
|
+
* the document from the vendor's reference page, which SOURCE.md names (architecture, "Creating a pack", step 2). */
|
|
53
58
|
const SPEC_TOOL = /\.(py|sh|ts|js|mjs|cjs|rb)$/;
|
|
59
|
+
/** spec/'s tools SOURCE.md names: the writers of a document a vendor publishes only as a page. */
|
|
60
|
+
function namedSpecTools(unit) {
|
|
61
|
+
const path = join(unit.dir, 'spec', 'SOURCE.md');
|
|
62
|
+
const source = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
63
|
+
return new Set(filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !f.includes('/') && source.includes(`\`${f}\``)));
|
|
64
|
+
}
|
|
54
65
|
/** The vendored documents: every file under spec/ that is not its metadata. */
|
|
55
66
|
function specFiles(unit) {
|
|
56
67
|
const dir = join(unit.dir, 'spec');
|
|
57
68
|
return filesUnder(dir).filter((f) => !SPEC_METADATA.test(f) && !SPEC_TOOL.test(f)).map((f) => join(dir, f));
|
|
58
69
|
}
|
|
59
|
-
/**
|
|
70
|
+
/** The checkout a directory is in: the nearest ancestor holding `.git` (a directory, or a worktree's file). */
|
|
71
|
+
function checkoutOf(dir) {
|
|
72
|
+
for (let d = dir;; d = dirname(d)) {
|
|
73
|
+
if (existsSync(join(d, '.git')))
|
|
74
|
+
return d;
|
|
75
|
+
if (dirname(d) === d)
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Grading fans out over a checkout's directories. A caller explicitly owns the immutable listing for one pass;
|
|
80
|
+
// nested gates share it, while concurrent passes have separate snapshots and direct checks always read git afresh.
|
|
81
|
+
const checkoutFiles = new AsyncLocalStorage();
|
|
82
|
+
export function withCheckoutFiles(fn) {
|
|
83
|
+
return checkoutFiles.getStore() ? fn() : checkoutFiles.run(new Map(), fn);
|
|
84
|
+
}
|
|
85
|
+
/** Files relative to a directory: Git's tree in a checkout (one listing per checkout per pass), the disk tree of an
|
|
86
|
+
* unpacked package outside one. A failed Git listing refuses; it never reads as an empty tree. */
|
|
60
87
|
function filesUnder(dir, base = dir) {
|
|
61
88
|
if (!existsSync(dir))
|
|
62
89
|
return [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
const top = checkoutOf(dir);
|
|
91
|
+
if (!top)
|
|
92
|
+
return allFilesUnder(dir, base, true);
|
|
93
|
+
const listing = checkoutFiles.getStore();
|
|
94
|
+
let files = listing?.get(top);
|
|
95
|
+
if (!files) {
|
|
96
|
+
// tracked and not-yet-added files alike (T0 runs before the commit); ignored runtime state (.volter) never
|
|
97
|
+
const run = spawnSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { cwd: top, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 });
|
|
98
|
+
if (run.status !== 0 || run.error) {
|
|
99
|
+
throw new Error(`git ls-files failed: directory=${JSON.stringify(top)} status=${run.status} signal=${JSON.stringify(run.signal)} error=${JSON.stringify(run.error?.message ?? null)} stderr=${JSON.stringify(String(run.stderr ?? ''))}`);
|
|
100
|
+
}
|
|
101
|
+
files = [...new Set(String(run.stdout).split('\0').filter(Boolean))];
|
|
102
|
+
listing?.set(top, files);
|
|
103
|
+
}
|
|
104
|
+
const under = relative(top, dir);
|
|
105
|
+
const mine = under === '' ? files : files.filter((f) => f.startsWith(`${under}/`)).map((f) => f.slice(under.length + 1));
|
|
106
|
+
return mine.map((f) => relative(base, join(dir, f))).filter((f) => existsSync(join(base, f)));
|
|
67
107
|
}
|
|
68
|
-
/**
|
|
69
|
-
function allFilesUnder(dir, base = dir) {
|
|
108
|
+
/** Disk files outside a checkout, or the complete output of the grader's own derivation. */
|
|
109
|
+
function allFilesUnder(dir, base = dir, excludeArtifacts = false) {
|
|
70
110
|
if (!existsSync(dir))
|
|
71
111
|
return [];
|
|
72
112
|
const out = [];
|
|
73
113
|
for (const name of readdirSync(dir)) {
|
|
74
114
|
const path = join(dir, name);
|
|
115
|
+
if (excludeArtifacts && isPackArtifact(relative(base, path)))
|
|
116
|
+
continue;
|
|
75
117
|
if (statSync(path).isDirectory())
|
|
76
|
-
out.push(...allFilesUnder(path, base));
|
|
118
|
+
out.push(...allFilesUnder(path, base, excludeArtifacts));
|
|
77
119
|
else
|
|
78
120
|
out.push(relative(base, path));
|
|
79
121
|
}
|
|
@@ -97,15 +139,28 @@ const isLane = (unit) => unit.name.includes('/');
|
|
|
97
139
|
function srcFileInForm(unit, name) {
|
|
98
140
|
if (name.endsWith('.d.ts'))
|
|
99
141
|
return true; // the types of a vendor SDK the pack's tests drive
|
|
100
|
-
return (isLane(unit) ? ['manifest.ts', 'index.ts', 'fetch.ts', 'budget.ts'] : ['manifest.ts', 'index.ts', 'cli.ts', 'fetch.ts', 'server.ts', 'budget.ts']).includes(name);
|
|
142
|
+
return (isLane(unit) ? ['manifest.ts', 'index.ts', 'fetch.ts', 'budget.ts'] : ['manifest.ts', 'index.ts', 'cli.ts', 'fetch.ts', 'server.ts', 'stream.ts', 'budget.ts']).includes(name);
|
|
101
143
|
}
|
|
102
144
|
/** The fixed files of semantics/ besides the families' handler files and their tests. */
|
|
103
|
-
const SEMANTICS_FIXED = new Set(['states.ts', 'index.ts', 'shared.ts', 'doors.ts', 'seed.ts', 'around.ts', 'events.ts', 'clock.ts', 'tenant.ts', 'scenario.ts', 'gap.ts', 'graphql.ts', 'sockets.ts', 'board.ts']);
|
|
145
|
+
const SEMANTICS_FIXED = new Set(['answer-views.ts', 'states.ts', 'index.ts', 'shared.ts', 'doors.ts', 'seed.ts', 'around.ts', 'events.ts', 'clock.ts', 'tenant.ts', 'scenario.ts', 'gap.ts', 'graphql.ts', 'sockets.ts', 'board.ts']);
|
|
104
146
|
/** journeys/'s closed set. */
|
|
105
147
|
const JOURNEY_FILES = new Set(['customer-life.outline.md', 'customer-life.outline.verdict.json', 'customer-life.json', 'customer-life.verdict.json', 'decisions.json',
|
|
106
148
|
'vendor-examples.json', 'vendor-examples.verdict.json', 'demand.json', 'unreachable.json', 'score.json', 'coverage.json',
|
|
107
149
|
// a generative pack's life names the World's scenario it walks under (architecture, "Writing a life")
|
|
108
150
|
'scenario.json']);
|
|
151
|
+
/** Only scenario basenames the two walks explicitly declare extend the closed layout. */
|
|
152
|
+
function journeyFiles(unit) {
|
|
153
|
+
const files = new Set(JOURNEY_FILES);
|
|
154
|
+
for (const name of ['customer-life.json', 'vendor-examples.json']) {
|
|
155
|
+
try {
|
|
156
|
+
const journey = JSON.parse(readFileSync(join(unit.dir, 'journeys', name), 'utf8'));
|
|
157
|
+
if (typeof journey.scenario === 'string' && /^[a-z0-9][a-z0-9.-]*\.json$/i.test(journey.scenario))
|
|
158
|
+
files.add(journey.scenario);
|
|
159
|
+
}
|
|
160
|
+
catch { /* malformed or absent journeys get no additional files */ }
|
|
161
|
+
}
|
|
162
|
+
return files;
|
|
163
|
+
}
|
|
109
164
|
/** Entries at the unit's root the form does not name (a vendor's lanes are units of their own). */
|
|
110
165
|
function rootOutsideForm(unit) {
|
|
111
166
|
return entriesOf(unit.dir).filter((name) => {
|
|
@@ -144,7 +199,7 @@ const FORBIDDEN_REACH = [
|
|
|
144
199
|
[/\bprocess\.env\b|\bBun\.env\b|\bDeno\.env\b/, 'the environment'],
|
|
145
200
|
[/(^|[^\w])fetch\s*\(|\bXMLHttpRequest\b|\bnode:(net|http|https|tls|dgram)\b/m, 'the network'],
|
|
146
201
|
[/\bimport\s*\(|\brequire\s*\(/, 'a module loaded at run time'],
|
|
147
|
-
[/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
|
|
202
|
+
[/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b(?!\s*[.?])|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
|
|
148
203
|
[/\bSemanticsContext\b|\bSemantics\b|\bsemanticsContext\b/, "the kernel's wide context (a handler is typed Handler, over HandlerContext; a lane reaches its vendor's through ctx.over)"],
|
|
149
204
|
];
|
|
150
205
|
/** A handler's name for an operationId: the id with every character a JavaScript name cannot hold made `_`, and a
|
|
@@ -215,7 +270,7 @@ const criteria = [
|
|
|
215
270
|
const patches = doc?.patches;
|
|
216
271
|
if (!Array.isArray(patches))
|
|
217
272
|
return fail('spec/patches.json is not { patches: [...] }');
|
|
218
|
-
const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || /https?:\/\//.test(p.why))).length;
|
|
273
|
+
const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || p.source?.recording || /https?:\/\//.test(p.why))).length;
|
|
219
274
|
return bare ? fail(`${bare} of ${patches.length} corrections lack a reason or a source`) : pass(`${patches.length} corrections, each sourced`);
|
|
220
275
|
},
|
|
221
276
|
},
|
|
@@ -261,7 +316,8 @@ const criteria = [
|
|
|
261
316
|
const families = new Set(operationsOf(unit).values());
|
|
262
317
|
const outside = [
|
|
263
318
|
...rootOutsideForm(unit),
|
|
264
|
-
...
|
|
319
|
+
...unpublishedUnitFiles(unit.dir),
|
|
320
|
+
...filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !namedSpecTools(unit).has(f)).map((f) => `spec/${f}`),
|
|
265
321
|
...entriesOf(src).filter((name) => statSync(join(src, name)).isDirectory() ? !LAYOUT_DIRS.has(name) : !srcFileInForm(unit, name)).map((n) => `src/${n}`),
|
|
266
322
|
...filesUnder(join(src, 'semantics')).filter((f) => {
|
|
267
323
|
if (f.includes('/'))
|
|
@@ -270,7 +326,7 @@ const criteria = [
|
|
|
270
326
|
return !(SEMANTICS_FIXED.has(f) || SEMANTICS_FIXED.has(`${stem}.ts`) || (families.has(stem.replace(/-ops$/, '')) && /\.(test\.)?ts$/.test(f)));
|
|
271
327
|
}).map((f) => `src/semantics/${f}`),
|
|
272
328
|
// a lane over its vendor's state has no journeys of its own: the vendor's life walks it ("Pack layout")
|
|
273
|
-
...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !
|
|
329
|
+
...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !journeyFiles(unit).has(f)).map((f) => `journeys/${f}`),
|
|
274
330
|
];
|
|
275
331
|
// screens/: one file per screen the manifest declares, named by its id
|
|
276
332
|
if (existsSync(join(src, 'screens'))) {
|
|
@@ -353,6 +409,12 @@ const criteria = [
|
|
|
353
409
|
found.push(`clock.ts exports ${names.join(', ') || 'nothing'}, not only clock`);
|
|
354
410
|
continue;
|
|
355
411
|
}
|
|
412
|
+
if (f === 'answer-views.ts') {
|
|
413
|
+
const names = [...text.matchAll(/^export\s+const\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
414
|
+
if (names.join() !== 'answerViews' || /^export\s+(?:async\s+)?function\s|^export\s+(let|var|class)\s|^export\s*\{/m.test(text))
|
|
415
|
+
found.push(`answer-views.ts exports ${names.join(', ') || 'nothing'}, not only answerViews`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
356
418
|
if (f === 'board.ts') {
|
|
357
419
|
const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
358
420
|
if (names.join() !== 'board' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text))
|
|
@@ -605,18 +667,15 @@ const criteria = [
|
|
|
605
667
|
const verdict = readJson(join(journeys, 'customer-life.outline.verdict.json'));
|
|
606
668
|
if (!existsSync(outline))
|
|
607
669
|
return fail('no journeys/customer-life.outline.md');
|
|
670
|
+
const author = /^Author:\s*(.+?)\.?\s*$/m.exec(readFileSync(outline, 'utf8'))?.[1];
|
|
671
|
+
attributionName(author);
|
|
608
672
|
if (!verdict)
|
|
609
673
|
return fail('no outline verdict');
|
|
610
674
|
if (!verdict.plausible)
|
|
611
675
|
return fail('judged not plausible');
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return fail('the verdict does not name its judge (judge.agent)');
|
|
616
|
-
if (!verdict.authors?.length)
|
|
617
|
-
return fail('the verdict does not name the authors, so the judge is not shown to be another session');
|
|
618
|
-
if (verdict.authors.includes(verdict.judge.agent))
|
|
619
|
-
return fail('the judge is among the authors');
|
|
676
|
+
const dependent = notIndependent(verdict);
|
|
677
|
+
if (dependent)
|
|
678
|
+
return fail(dependent);
|
|
620
679
|
return pass(`plausible, judge ${verdict.judge.agent}`);
|
|
621
680
|
},
|
|
622
681
|
},
|
|
@@ -627,7 +686,7 @@ const criteria = [
|
|
|
627
686
|
const r = report(unit);
|
|
628
687
|
if (!r)
|
|
629
688
|
return fail(noReport(unit));
|
|
630
|
-
return r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
|
|
689
|
+
return r.life.steps > 0 && r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
|
|
631
690
|
},
|
|
632
691
|
},
|
|
633
692
|
{
|
package/dist/src/published.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConformanceResult } from './conformance.js';
|
|
1
2
|
export type PublishedGate = {
|
|
2
3
|
spec: string;
|
|
3
4
|
failures: Array<{
|
|
@@ -7,6 +8,11 @@ export type PublishedGate = {
|
|
|
7
8
|
};
|
|
8
9
|
/** The vendor a package twins: its name without its scope and the `twin-` prefix (`@volter/twin-supabase` is supabase). */
|
|
9
10
|
export declare const vendorOfPackage: (name: string) => string;
|
|
11
|
+
/** The conformance of `spec` as the registry serves it: the published tarball with its own dependencies installed,
|
|
12
|
+
* served and read against the kernel (kernelDir). */
|
|
13
|
+
export declare function conformPublished(spec: string, opts?: {
|
|
14
|
+
registry?: string;
|
|
15
|
+
}): Promise<ConformanceResult[]>;
|
|
10
16
|
/** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
|
|
11
17
|
export declare function gatePublished(spec: string, opts?: {
|
|
12
18
|
registry?: string;
|
package/dist/src/published.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// twins come from"): its tarball fetched from the registry, unpacked under its vendor's name (so its lanes are read as
|
|
3
3
|
// the vendor's), and every unit in it — the pack and each lane — put through the gate.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { existsSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync } from 'node:fs';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
6
7
|
import { tmpdir } from 'node:os';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { conformance } from "./conformance.js";
|
|
8
10
|
import { p3Gate } from "./gate.js";
|
|
9
11
|
/** The vendor a package twins: its name without its scope and the `twin-` prefix (`@volter/twin-supabase` is supabase). */
|
|
10
12
|
export const vendorOfPackage = (name) => name.replace(/^@[^/]+\//, '').replace(/^twin-/, '');
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
+
/** `spec` (`<name>@<version>`) as the registry serves it, unpacked under its vendor's name in a scratch directory, handed
|
|
14
|
+
* to `use`; throws when it cannot be fetched or unpacked. */
|
|
15
|
+
async function withPublished(spec, opts, use) {
|
|
13
16
|
const tmp = mkdtempSync(join(tmpdir(), 'twin-standard-'));
|
|
14
17
|
try {
|
|
15
18
|
const pack = spawnSync('npm', ['pack', spec, '--pack-destination', tmp, '--silent', ...(opts.registry ? ['--registry', opts.registry] : [])], { encoding: 'utf8' });
|
|
@@ -22,15 +25,66 @@ export async function gatePublished(spec, opts = {}) {
|
|
|
22
25
|
const name = JSON.parse(readFileSync(join(tmp, 'package', 'package.json'), 'utf8')).name;
|
|
23
26
|
const dir = join(tmp, vendorOfPackage(name));
|
|
24
27
|
renameSync(join(tmp, 'package'), dir);
|
|
25
|
-
|
|
26
|
-
const results = await p3Gate([dir, ...lanes]);
|
|
27
|
-
const unit = (u) => u.slice(tmp.length + 1);
|
|
28
|
-
return {
|
|
29
|
-
spec,
|
|
30
|
-
failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })),
|
|
31
|
-
};
|
|
28
|
+
return await use(dir, tmp);
|
|
32
29
|
}
|
|
33
30
|
finally {
|
|
34
31
|
rmSync(tmp, { recursive: true, force: true });
|
|
35
32
|
}
|
|
36
33
|
}
|
|
34
|
+
/** A platform package a published pack is served with (`@volter/world-core`, `@volter/world-ui`): the one beside this
|
|
35
|
+
* standard (twin-world's, where the catalog clones it), else one the caller installed. Found by its package's entry,
|
|
36
|
+
* walked up to its root (an `exports` that names no `./package.json` is refused by Node). */
|
|
37
|
+
function platformDir(name) {
|
|
38
|
+
for (const from of [import.meta.url, join(process.cwd(), 'package.json')]) {
|
|
39
|
+
let at;
|
|
40
|
+
try {
|
|
41
|
+
at = dirname(createRequire(from).resolve(name));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (; at !== dirname(at); at = dirname(at)) {
|
|
47
|
+
const pkg = join(at, 'package.json');
|
|
48
|
+
if (existsSync(pkg) && JSON.parse(readFileSync(pkg, 'utf8')).name === name)
|
|
49
|
+
return at;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/** The conformance of `spec` as the registry serves it: the published tarball with its own dependencies installed,
|
|
55
|
+
* served and read against the kernel (kernelDir). */
|
|
56
|
+
export async function conformPublished(spec, opts = {}) {
|
|
57
|
+
const kernel = platformDir('@volter/world-core');
|
|
58
|
+
if (!kernel)
|
|
59
|
+
throw new Error('no @volter/world-core to serve the pack with: install it beside the standard');
|
|
60
|
+
return withPublished(spec, opts, async (dir) => {
|
|
61
|
+
// what the pack depends on: the platform's packages (`@volter/*`) the same as the kernel, every other (react for its
|
|
62
|
+
// screens, a database client) installed as an application installing the pack gets it
|
|
63
|
+
const deps = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies ?? {};
|
|
64
|
+
const platform = Object.keys(deps).filter((d) => d.startsWith('@volter/'));
|
|
65
|
+
const others = Object.entries(deps).filter(([d]) => !d.startsWith('@volter/')).map(([d, v]) => `${d}@${v}`);
|
|
66
|
+
if (others.length) {
|
|
67
|
+
const install = spawnSync('npm', ['install', '--no-save', '--omit=dev', '--omit=peer', '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock', ...(opts.registry ? ['--registry', opts.registry] : []), ...others], { cwd: dirname(dir), encoding: 'utf8' });
|
|
68
|
+
if (install.status !== 0)
|
|
69
|
+
throw new Error(`${spec}: its dependencies do not install (${String(install.stderr).trim().split('\n').at(-1) ?? ''})`);
|
|
70
|
+
}
|
|
71
|
+
mkdirSync(join(dir, 'node_modules', '@volter'), { recursive: true });
|
|
72
|
+
for (const name of ['@volter/world-core', ...platform]) {
|
|
73
|
+
const at = name === '@volter/world-core' ? kernel : platformDir(name);
|
|
74
|
+
if (!at)
|
|
75
|
+
throw new Error(`${spec}: depends on ${name}, which is not beside the standard`);
|
|
76
|
+
rmSync(join(dir, 'node_modules', name), { recursive: true, force: true });
|
|
77
|
+
symlinkSync(at, join(dir, 'node_modules', name));
|
|
78
|
+
}
|
|
79
|
+
return conformance(dir);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
|
|
83
|
+
export async function gatePublished(spec, opts = {}) {
|
|
84
|
+
return withPublished(spec, opts, async (dir, tmp) => {
|
|
85
|
+
const lanes = readdirSync(dir).filter((l) => existsSync(join(dir, l, 'src', 'manifest.ts')) && !existsSync(join(dir, l, 'package.json'))).map((l) => join(dir, l));
|
|
86
|
+
const results = await p3Gate([dir, ...lanes]);
|
|
87
|
+
const unit = (u) => u.slice(tmp.length + 1);
|
|
88
|
+
return { spec, failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })) };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
|
|
1
2
|
// One API published as several OpenAPI documents (Upstash publishes QStash's and Workflow's, both served on
|
|
2
3
|
// qstash-{region}.upstash.io with one key set, and sharing endpoints): the pack vendors each as published under
|
|
3
4
|
// `spec/openapi/<name>.{json,yaml}[.gz]`, patches address a document as `/documents/<name>/...`, and the documents are
|
|
@@ -17,7 +18,7 @@ export function readSpecDocuments(specDir) {
|
|
|
17
18
|
for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
|
|
18
19
|
const bytes = readFileSync(join(dir, f));
|
|
19
20
|
const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
20
|
-
documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = /\.ya?ml(\.gz)?$/.test(f) ? parseYaml(text) :
|
|
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);
|
|
21
22
|
}
|
|
22
23
|
if (Object.keys(documents).length < 2)
|
|
23
24
|
throw new Error(`${dir}: one API in several documents needs at least two; a single document is spec/openapi.{json,yaml}`);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { SpecIR } from './spec-ir.js';
|
|
1
|
+
import type { IrOperation, IrResource, SpecIR } from './spec-ir.js';
|
|
2
2
|
export type ClientCall = {
|
|
3
3
|
/** the client method that makes the call, and its line */
|
|
4
4
|
client: string;
|
|
5
5
|
line: number;
|
|
6
6
|
method: string;
|
|
7
7
|
url: string;
|
|
8
|
+
/** Response declarations supplied by cited reference-page patches, absent from extracted calls. */
|
|
9
|
+
answers?: IrOperation['answers'];
|
|
10
|
+
resource?: string;
|
|
8
11
|
query?: Record<string, string | null>;
|
|
9
12
|
body?: {
|
|
10
13
|
encoding: 'json' | 'form' | 'raw';
|
|
@@ -16,5 +19,6 @@ export type ClientOpsSpec = {
|
|
|
16
19
|
format: 'client';
|
|
17
20
|
version: string;
|
|
18
21
|
calls: ClientCall[];
|
|
22
|
+
resources?: IrResource[];
|
|
19
23
|
};
|
|
20
24
|
export declare function fromClientCalls(spec: ClientOpsSpec): SpecIR;
|
|
@@ -36,6 +36,14 @@ export function fromClientCalls(spec) {
|
|
|
36
36
|
byRoute.set(key, { first: call, path, labels, calls: [call] });
|
|
37
37
|
}
|
|
38
38
|
const operations = [...byRoute.values()].map(({ first, path, labels, calls }) => {
|
|
39
|
+
const declared = (key) => {
|
|
40
|
+
const values = calls.map((c) => c[key]).filter((v) => v !== undefined);
|
|
41
|
+
if (values.some((v) => JSON.stringify(v) !== JSON.stringify(values[0])))
|
|
42
|
+
throw new Error(`client response declarations conflict at ${first.method} ${path}: ${key}`);
|
|
43
|
+
return values[0];
|
|
44
|
+
};
|
|
45
|
+
const answers = declared('answers');
|
|
46
|
+
const resource = declared('resource') ?? answers?.resource;
|
|
39
47
|
const query = new Map();
|
|
40
48
|
const body = new Map();
|
|
41
49
|
const encodings = new Set();
|
|
@@ -62,7 +70,9 @@ export function fromClientCalls(spec) {
|
|
|
62
70
|
body: [...body.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
63
71
|
bodyEncoding,
|
|
64
72
|
successStatus: 200,
|
|
65
|
-
class: 'action',
|
|
73
|
+
class: first.method.toUpperCase() === 'GET' && answers ? (answers.list ? 'list' : 'retrieve') : 'action',
|
|
74
|
+
...(answers ? { answers } : {}),
|
|
75
|
+
...(resource ? { resource } : {}),
|
|
66
76
|
};
|
|
67
77
|
});
|
|
68
78
|
// two routes whose first calls share a client method (one method calling twice) keep distinct ids
|
|
@@ -73,5 +83,5 @@ export function fromClientCalls(spec) {
|
|
|
73
83
|
if (n > 1)
|
|
74
84
|
o.id = `${o.id}_${n}`;
|
|
75
85
|
}
|
|
76
|
-
return { format: 'client', version: spec.version, resources: [], operations };
|
|
86
|
+
return { format: 'client', version: spec.version, resources: spec.resources ?? [], operations };
|
|
77
87
|
}
|
package/dist/src/spec-ir.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
type Json = any;
|
|
2
2
|
export type OperationClass = 'create' | 'retrieve' | 'list' | 'update' | 'delete' | 'action' | 'computed' | 'non-resource';
|
|
3
|
+
/** Binary media whose resource shape is the response bytes, rather than JSON metadata. */
|
|
4
|
+
export declare const binaryMedia: (media: string) => boolean;
|
|
3
5
|
export declare const CRUD_CLASSES: ReadonlySet<OperationClass>;
|
|
4
6
|
/** A parameter or body field: its name, type and whether it is required, and the numeric bounds the spec states
|
|
5
7
|
* (`minimum`, `maximum`), which a strict body (the manifest's `body.strict`, `ctx.fields`) refuses outside of. */
|
|
@@ -7,8 +9,8 @@ export type IrParam = {
|
|
|
7
9
|
name: string;
|
|
8
10
|
type: string;
|
|
9
11
|
required: boolean;
|
|
10
|
-
minimum?: number;
|
|
11
|
-
maximum?: number;
|
|
12
|
+
minimum?: number | string;
|
|
13
|
+
maximum?: number | string;
|
|
12
14
|
};
|
|
13
15
|
export type IrOperation = {
|
|
14
16
|
/** The spec's operationId, or the `method_path` slug when the spec has none. */
|
|
@@ -36,6 +38,9 @@ export type IrOperation = {
|
|
|
36
38
|
/** A form-encoded operation's number and boolean fields, by bracket path (formScalarsOf). */
|
|
37
39
|
scalars?: Record<string, 'integer' | 'number' | 'boolean'>;
|
|
38
40
|
successStatus: number;
|
|
41
|
+
/** Binary media types declared by each response status, independent of the twin’s response headers. */
|
|
42
|
+
responseMedia?: Record<string, string[]>;
|
|
43
|
+
defaultResponseMedia?: Record<string, string>;
|
|
39
44
|
/** the success is documented with no content at all (a 200 with nothing in it) */
|
|
40
45
|
emptySuccess?: true;
|
|
41
46
|
/** The success answer may be a server-sent event stream (`text/event-stream`). */
|
package/dist/src/spec-ir.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// reads an AWS Smithy JSON model
|
|
5
|
-
// (OpenAPI 3.x; Stripe's vendor extensions `x-resourceId` and `x-expandableFields` are read where
|
|
6
|
-
// present). Pure and deterministic: the same document always yields the same IR, byte for byte.
|
|
1
|
+
import { isExactIntegerValue } from '@volter/world-core/exact-json';
|
|
2
|
+
/** Binary media whose resource shape is the response bytes, rather than JSON metadata. */
|
|
3
|
+
export const binaryMedia = (media) => /^(?:application\/(?:octet-stream|pdf|zip|gzip)|(?:image|audio|video)\/[^/]+)$/.test(media.toLowerCase());
|
|
7
4
|
export const CRUD_CLASSES = new Set(['create', 'retrieve', 'list', 'update', 'delete']);
|
|
8
5
|
/** A schema's stated numeric bounds, when it states any. */
|
|
9
6
|
function boundsOf(doc, schema) {
|
|
10
7
|
const s = deref(doc, schema);
|
|
11
|
-
return { ...(typeof s?.minimum === 'number' ? { minimum: s.minimum } : {}), ...(typeof s?.maximum === 'number' ? { maximum: s.maximum } : {}) };
|
|
8
|
+
return { ...((typeof s?.minimum === 'number' || typeof s?.minimum === 'string' && isExactIntegerValue(s.minimum)) ? { minimum: s.minimum } : {}), ...((typeof s?.maximum === 'number' || typeof s?.maximum === 'string' && isExactIntegerValue(s.maximum)) ? { maximum: s.maximum } : {}) };
|
|
12
9
|
}
|
|
13
10
|
// every operation an OpenAPI path item may hold (a HEAD is as published as a GET: Vercel's /v8/artifacts/{hash})
|
|
14
11
|
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
|
|
@@ -180,6 +177,19 @@ function successOf(doc, op, resources) {
|
|
|
180
177
|
const listed = items ? resourcesOfSchema(doc, items, resources) : [];
|
|
181
178
|
if (listed.length)
|
|
182
179
|
return { status, answers: answer(listed, true) };
|
|
180
|
+
// a single object under `data` (X's create: a thin data object beside partial errors); a `data` that is an array, named
|
|
181
|
+
// by reference or inline, is a list's envelope, read below
|
|
182
|
+
if (s?.properties?.data && deref(doc, s.properties.data)?.type !== 'array') {
|
|
183
|
+
const payload = s.properties.data;
|
|
184
|
+
const name = refName(payload);
|
|
185
|
+
const found = resourcesOfSchema(doc, payload, resources);
|
|
186
|
+
if (found.length)
|
|
187
|
+
return { status, answers: answer(found, false, 'data') };
|
|
188
|
+
if (name && propsOf(doc, payload).id)
|
|
189
|
+
return { status, answers: answer([name], false, 'data') };
|
|
190
|
+
// A data payload without a modeled identity cannot make the accompanying errors the resource.
|
|
191
|
+
return { status };
|
|
192
|
+
}
|
|
183
193
|
// An envelope that holds exactly one resource, or one list of them, under a named property, beside
|
|
184
194
|
// at most a few scalars (`ok`, a cursor). An object with an `id` of its own, or with other objects
|
|
185
195
|
// in it, is not an envelope: it is something that embeds another (a Pages site, a branch).
|
|
@@ -218,7 +228,17 @@ function paramsOf(doc, op, pathItem) {
|
|
|
218
228
|
return { pathParams, query };
|
|
219
229
|
}
|
|
220
230
|
function bodyOf(doc, op) {
|
|
221
|
-
const
|
|
231
|
+
const request = deref(doc, op.requestBody);
|
|
232
|
+
const content = request?.content ?? {};
|
|
233
|
+
// A whole binary body is the payload itself, not a JSON object with no fields. Keep the existing blob contract so
|
|
234
|
+
// the kernel leaves its bytes opaque even when a caller labels them application/json.
|
|
235
|
+
const representations = Object.values(content);
|
|
236
|
+
const binary = representations.length > 0 && representations.every((media) => {
|
|
237
|
+
const schema = deref(doc, media?.schema);
|
|
238
|
+
return schema?.type === 'string' && schema.format === 'binary';
|
|
239
|
+
});
|
|
240
|
+
if (binary)
|
|
241
|
+
return { body: [{ name: 'body', type: 'blob', required: request?.required === true }], bodyEncoding: 'none' };
|
|
222
242
|
const mediaOf = { json: 'application/json', form: 'application/x-www-form-urlencoded', multipart: 'multipart/form-data' };
|
|
223
243
|
const encoding = content[mediaOf.json] ? 'json' : content[mediaOf.form] ? 'form' : content[mediaOf.multipart] ? 'multipart' : 'none';
|
|
224
244
|
if (encoding === 'none')
|
|
@@ -384,8 +404,8 @@ export function fromOpenAPI(spec) {
|
|
|
384
404
|
if (d?.type === 'array')
|
|
385
405
|
return named(d.items);
|
|
386
406
|
const props = propsOf(doc, schema);
|
|
387
|
-
if (props.data
|
|
388
|
-
return named(deref(doc, props.data).items);
|
|
407
|
+
if (props.data)
|
|
408
|
+
return deref(doc, props.data)?.type === 'array' ? named(deref(doc, props.data).items) : named(props.data);
|
|
389
409
|
const held = Object.values(props).filter((p) => !envelopeMeta(doc, p)).map((p) => (deref(doc, p)?.type === 'array' ? named(deref(doc, p).items) : named(p))).filter((n) => n.length);
|
|
390
410
|
return held.length === 1 ? held[0] : [];
|
|
391
411
|
};
|
|
@@ -408,6 +428,17 @@ export function fromOpenAPI(spec) {
|
|
|
408
428
|
if (!op)
|
|
409
429
|
continue;
|
|
410
430
|
const success = successOf(doc, op, resourceBySchema);
|
|
431
|
+
const responseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
|
|
432
|
+
const media = Object.keys(deref(doc, response)?.content ?? {}).filter(binaryMedia).sort();
|
|
433
|
+
return media.length ? [[code, media]] : [];
|
|
434
|
+
}));
|
|
435
|
+
const defaultResponseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
|
|
436
|
+
if (!responseMedia[code])
|
|
437
|
+
return [];
|
|
438
|
+
const media = Object.keys(deref(doc, response)?.content ?? {});
|
|
439
|
+
const defaultMedia = media.includes('application/json') ? 'application/json' : media.length === 1 ? media[0] : undefined;
|
|
440
|
+
return defaultMedia ? [[code, defaultMedia]] : [];
|
|
441
|
+
}));
|
|
411
442
|
const streams = Object.entries(op.responses ?? {}).some(([code, r]) => /^2\d\d$/.test(code) && deref(doc, r)?.content?.['text/event-stream']);
|
|
412
443
|
const security = (op.security ?? doc.security ?? []);
|
|
413
444
|
const scopes = [...new Set(security.flatMap((req) => Object.values(req ?? {}).flat()))].sort();
|
|
@@ -426,6 +457,7 @@ export function fromOpenAPI(spec) {
|
|
|
426
457
|
...body,
|
|
427
458
|
...(scalars ? { scalars } : {}),
|
|
428
459
|
successStatus: success.status,
|
|
460
|
+
...(Object.keys(responseMedia).length ? { responseMedia, ...(Object.keys(defaultResponseMedia).length ? { defaultResponseMedia } : {}) } : {}),
|
|
429
461
|
...(success.empty ? { emptySuccess: true } : {}),
|
|
430
462
|
...(streams ? { streams: true } : {}),
|
|
431
463
|
...(scopes.length ? { scopes } : {}),
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Committed attribution names people or author roles, never execution identities. */
|
|
2
|
+
export declare function attributionName(name: string | undefined): string | undefined;
|
|
3
|
+
export type Verdict = {
|
|
4
|
+
judge?: {
|
|
5
|
+
agent?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
session?: string;
|
|
8
|
+
review?: string;
|
|
9
|
+
};
|
|
10
|
+
authors?: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Why the verdict is no verdict, or undefined when it rules. `author` is the name the judged file itself names as
|
|
13
|
+
* its author (a life's `author`). */
|
|
14
|
+
export declare function notIndependent(verdict: Verdict, author?: string): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// A JUDGE'S INDEPENDENCE (the grade's outline-judged and life-judged, the published examples' verdict): a verdict rules
|
|
2
|
+
// only when it names its judge (`judge.agent`) and the authors of what it judges (`authors`), and the judge is none of
|
|
3
|
+
// them. The dispatcher stamps author names consistently; a judge started independently
|
|
4
|
+
// (scripts/judge-brief.ts prints its brief) names its reviewer role (`judge.name`). One judging per pack: the
|
|
5
|
+
// verdict stands, and later changes to what it judged keep it (architecture, "Writing a life", Judge).
|
|
6
|
+
/** Committed attribution names people or author roles, never execution identities. */
|
|
7
|
+
export function attributionName(name) {
|
|
8
|
+
if (name === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (/\bcx-|\bt_[0-9a-f]{8}|wt\/|\/root(?:\/|$|[: )])|\b(?:pane|session)[ -:]|[0-9a-f]{8}-[0-9a-f-]{27}|\b[0-9a-f]{16,64}\b/i.test(name))
|
|
11
|
+
throw new Error('attribution must use an author name, not an execution identifier');
|
|
12
|
+
return name;
|
|
13
|
+
}
|
|
14
|
+
/** Why the verdict is no verdict, or undefined when it rules. `author` is the name the judged file itself names as
|
|
15
|
+
* its author (a life's `author`). */
|
|
16
|
+
export function notIndependent(verdict, author) {
|
|
17
|
+
const agent = attributionName(verdict.judge?.agent);
|
|
18
|
+
verdict.authors?.forEach(attributionName);
|
|
19
|
+
attributionName(author);
|
|
20
|
+
attributionName(verdict.judge?.session);
|
|
21
|
+
attributionName(verdict.judge?.review);
|
|
22
|
+
const session = attributionName(verdict.judge?.name ?? verdict.judge?.session);
|
|
23
|
+
if (/self-judged/i.test(session ?? '') || (!!author && session === author))
|
|
24
|
+
return 'self-judged (not a verdict)';
|
|
25
|
+
if (!agent || !verdict.authors?.length)
|
|
26
|
+
return 'unattributed (the dispatcher has not stamped judge.agent and authors)';
|
|
27
|
+
if (verdict.authors.includes(agent))
|
|
28
|
+
return `the judge is among the authors (${agent})`;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type TwinStream } from '@volter/world-core/runtime';
|
|
2
|
+
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type RespStep } from './journey-kit.js';
|
|
3
|
+
export declare function usePacks(dir: string): void;
|
|
4
|
+
/** The walk's World: the twin's own code on its in-memory World and blob stores (the backends a serverless
|
|
5
|
+
* entry runs on), so a walk of any length is state changes in memory and never touches the disk; the World
|
|
6
|
+
* clock, pinned to 2026-01-01, is world state in the same store, and the World is sealed. Each root is a fresh key
|
|
7
|
+
* space in it.
|
|
8
|
+
* Restored after ONE run: this module is imported by journey-walk.test.ts, and a store or clock left active
|
|
9
|
+
* would leak into whichever suite the sweep runs next. */
|
|
10
|
+
export declare function inMemoryWorld(label: string): {
|
|
11
|
+
root: () => string;
|
|
12
|
+
restore: () => void;
|
|
13
|
+
};
|
|
14
|
+
export type Failure = {
|
|
15
|
+
step: string;
|
|
16
|
+
beat: string;
|
|
17
|
+
detail: string;
|
|
18
|
+
};
|
|
19
|
+
/** What a caller that scores a walk observes around each step: the step, its resolved path and body,
|
|
20
|
+
* and (after) the answer and the failures the step produced. */
|
|
21
|
+
export type StepHooks = {
|
|
22
|
+
before?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, body: unknown) => void;
|
|
23
|
+
after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer: {
|
|
24
|
+
status: number;
|
|
25
|
+
body: unknown;
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
}, failures: Failure[]) => void;
|
|
28
|
+
};
|
|
29
|
+
/** A pack served for a walk the way a World runs it (its server.ts on a port, the World's root and scenario its options):
|
|
30
|
+
* the walk's requests go to it over HTTP, the vendor's host in their Host header, instead of to the fetch in process;
|
|
31
|
+
* its sockets are WebSockets to that port, and a line connection (`stream`) is a TCP connection to its protocol port. */
|
|
32
|
+
export type ServedTwin = (root: string, options: {
|
|
33
|
+
scenarioPath?: string;
|
|
34
|
+
}) => Promise<{
|
|
35
|
+
fetch: (request: Request) => Promise<Response>;
|
|
36
|
+
stop: () => void;
|
|
37
|
+
socket?: (url: string, headers: Record<string, string>) => WebSocket;
|
|
38
|
+
stream?: TwinStream;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function walk(journey: Journey, root: string, collect: Failure[], opts: {
|
|
41
|
+
beats: boolean;
|
|
42
|
+
verbose?: boolean;
|
|
43
|
+
hooks?: StepHooks;
|
|
44
|
+
captured?: Captured;
|
|
45
|
+
served?: ServedTwin;
|
|
46
|
+
}): Promise<Captured>;
|
|
47
|
+
export declare function withPinnedWorldDraws<T>(fn: () => Promise<T>): Promise<T>;
|