@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/src/published.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
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, type ConformanceResult } from './conformance.ts';
|
|
8
10
|
import { p3Gate } from './gate.ts';
|
|
9
11
|
|
|
10
12
|
export type PublishedGate = { spec: string; failures: Array<{ unit: string; failures: string[] }> };
|
|
@@ -12,8 +14,9 @@ export type PublishedGate = { spec: string; failures: Array<{ unit: string; fail
|
|
|
12
14
|
/** The vendor a package twins: its name without its scope and the `twin-` prefix (`@volter/twin-supabase` is supabase). */
|
|
13
15
|
export const vendorOfPackage = (name: string): string => name.replace(/^@[^/]+\//, '').replace(/^twin-/, '');
|
|
14
16
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
+
/** `spec` (`<name>@<version>`) as the registry serves it, unpacked under its vendor's name in a scratch directory, handed
|
|
18
|
+
* to `use`; throws when it cannot be fetched or unpacked. */
|
|
19
|
+
async function withPublished<T>(spec: string, opts: { registry?: string }, use: (dir: string, tmp: string) => Promise<T>): Promise<T> {
|
|
17
20
|
const tmp = mkdtempSync(join(tmpdir(), 'twin-standard-'));
|
|
18
21
|
try {
|
|
19
22
|
const pack = spawnSync('npm', ['pack', spec, '--pack-destination', tmp, '--silent', ...(opts.registry ? ['--registry', opts.registry] : [])], { encoding: 'utf8' });
|
|
@@ -24,14 +27,59 @@ export async function gatePublished(spec: string, opts: { registry?: string } =
|
|
|
24
27
|
const name = (JSON.parse(readFileSync(join(tmp, 'package', 'package.json'), 'utf8')) as { name: string }).name;
|
|
25
28
|
const dir = join(tmp, vendorOfPackage(name));
|
|
26
29
|
renameSync(join(tmp, 'package'), dir);
|
|
27
|
-
|
|
28
|
-
const results = await p3Gate([dir, ...lanes]);
|
|
29
|
-
const unit = (u: string): string => u.slice(tmp.length + 1);
|
|
30
|
-
return {
|
|
31
|
-
spec,
|
|
32
|
-
failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })),
|
|
33
|
-
};
|
|
30
|
+
return await use(dir, tmp);
|
|
34
31
|
} finally {
|
|
35
32
|
rmSync(tmp, { recursive: true, force: true });
|
|
36
33
|
}
|
|
37
34
|
}
|
|
35
|
+
|
|
36
|
+
/** A platform package a published pack is served with (`@volter/world-core`, `@volter/world-ui`): the one beside this
|
|
37
|
+
* standard (twin-world's, where the catalog clones it), else one the caller installed. Found by its package's entry,
|
|
38
|
+
* walked up to its root (an `exports` that names no `./package.json` is refused by Node). */
|
|
39
|
+
function platformDir(name: string): string | undefined {
|
|
40
|
+
for (const from of [import.meta.url, join(process.cwd(), 'package.json')]) {
|
|
41
|
+
let at: string;
|
|
42
|
+
try { at = dirname(createRequire(from).resolve(name)); } catch { continue; }
|
|
43
|
+
for (; at !== dirname(at); at = dirname(at)) {
|
|
44
|
+
const pkg = join(at, 'package.json');
|
|
45
|
+
if (existsSync(pkg) && (JSON.parse(readFileSync(pkg, 'utf8')) as { name?: string }).name === name) return at;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The conformance of `spec` as the registry serves it: the published tarball with its own dependencies installed,
|
|
52
|
+
* served and read against the kernel (kernelDir). */
|
|
53
|
+
export async function conformPublished(spec: string, opts: { registry?: string } = {}): Promise<ConformanceResult[]> {
|
|
54
|
+
const kernel = platformDir('@volter/world-core');
|
|
55
|
+
if (!kernel) throw new Error('no @volter/world-core to serve the pack with: install it beside the standard');
|
|
56
|
+
return withPublished(spec, opts, async (dir) => {
|
|
57
|
+
// what the pack depends on: the platform's packages (`@volter/*`) the same as the kernel, every other (react for its
|
|
58
|
+
// screens, a database client) installed as an application installing the pack gets it
|
|
59
|
+
const deps = (JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { dependencies?: Record<string, string> }).dependencies ?? {};
|
|
60
|
+
const platform = Object.keys(deps).filter((d) => d.startsWith('@volter/'));
|
|
61
|
+
const others = Object.entries(deps).filter(([d]) => !d.startsWith('@volter/')).map(([d, v]) => `${d}@${v}`);
|
|
62
|
+
if (others.length) {
|
|
63
|
+
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' });
|
|
64
|
+
if (install.status !== 0) throw new Error(`${spec}: its dependencies do not install (${String(install.stderr).trim().split('\n').at(-1) ?? ''})`);
|
|
65
|
+
}
|
|
66
|
+
mkdirSync(join(dir, 'node_modules', '@volter'), { recursive: true });
|
|
67
|
+
for (const name of ['@volter/world-core', ...platform]) {
|
|
68
|
+
const at = name === '@volter/world-core' ? kernel : platformDir(name);
|
|
69
|
+
if (!at) throw new Error(`${spec}: depends on ${name}, which is not beside the standard`);
|
|
70
|
+
rmSync(join(dir, 'node_modules', name), { recursive: true, force: true });
|
|
71
|
+
symlinkSync(at, join(dir, 'node_modules', name));
|
|
72
|
+
}
|
|
73
|
+
return conformance(dir);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
|
|
78
|
+
export async function gatePublished(spec: string, opts: { registry?: string } = {}): Promise<PublishedGate> {
|
|
79
|
+
return withPublished(spec, opts, async (dir, tmp) => {
|
|
80
|
+
const lanes = readdirSync(dir).filter((l) => existsSync(join(dir, l, 'src', 'manifest.ts')) && !existsSync(join(dir, l, 'package.json'))).map((l) => join(dir, l));
|
|
81
|
+
const results = await p3Gate([dir, ...lanes]);
|
|
82
|
+
const unit = (u: string): string => u.slice(tmp.length + 1);
|
|
83
|
+
return { spec, failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })) };
|
|
84
|
+
});
|
|
85
|
+
}
|
package/src/spec-documents.ts
CHANGED
|
@@ -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
|
|
@@ -21,7 +22,7 @@ export function readSpecDocuments(specDir: string): SpecDocuments | undefined {
|
|
|
21
22
|
for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
|
|
22
23
|
const bytes = readFileSync(join(dir, f));
|
|
23
24
|
const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
24
|
-
documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = /\.ya?ml(\.gz)?$/.test(f) ? parseYaml(text) :
|
|
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);
|
|
25
26
|
}
|
|
26
27
|
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}`);
|
|
27
28
|
return { documents };
|
package/src/spec-ir-client.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// The calls at one method and path are one operation, their query and body keys together. The client says nothing
|
|
12
12
|
// of what an answer holds, so an operation answers no resource here: its answers are the vendor's pages' examples.
|
|
13
13
|
|
|
14
|
-
import type { IrOperation, IrParam, SpecIR } from './spec-ir.ts';
|
|
14
|
+
import type { IrOperation, IrParam, IrResource, SpecIR } from './spec-ir.ts';
|
|
15
15
|
|
|
16
16
|
export type ClientCall = {
|
|
17
17
|
/** the client method that makes the call, and its line */
|
|
@@ -19,10 +19,13 @@ export type ClientCall = {
|
|
|
19
19
|
line: number;
|
|
20
20
|
method: string;
|
|
21
21
|
url: string;
|
|
22
|
+
/** Response declarations supplied by cited reference-page patches, absent from extracted calls. */
|
|
23
|
+
answers?: IrOperation['answers'];
|
|
24
|
+
resource?: string;
|
|
22
25
|
query?: Record<string, string | null>;
|
|
23
26
|
body?: { encoding: 'json' | 'form' | 'raw'; fields?: Record<string, string | null>; passes?: string };
|
|
24
27
|
};
|
|
25
|
-
export type ClientOpsSpec = { format: 'client'; version: string; calls: ClientCall[] };
|
|
28
|
+
export type ClientOpsSpec = { format: 'client'; version: string; calls: ClientCall[]; resources?: IrResource[] };
|
|
26
29
|
|
|
27
30
|
/** A label's name from the expression the client interpolates: `quote(datasource_name, safe='')` is
|
|
28
31
|
* `datasource_name`, `workspace['id']` is `workspace_id`. */
|
|
@@ -51,6 +54,12 @@ export function fromClientCalls(spec: ClientOpsSpec): SpecIR {
|
|
|
51
54
|
else byRoute.set(key, { first: call, path, labels, calls: [call] });
|
|
52
55
|
}
|
|
53
56
|
const operations: IrOperation[] = [...byRoute.values()].map(({ first, path, labels, calls }) => {
|
|
57
|
+
const declared = <K extends 'answers' | 'resource'>(key: K): ClientCall[K] => {
|
|
58
|
+
const values = calls.map((c) => c[key]).filter((v) => v !== undefined);
|
|
59
|
+
if (values.some((v) => JSON.stringify(v) !== JSON.stringify(values[0]))) throw new Error(`client response declarations conflict at ${first.method} ${path}: ${key}`);
|
|
60
|
+
return values[0];
|
|
61
|
+
};
|
|
62
|
+
const answers = declared('answers'); const resource = declared('resource') ?? answers?.resource;
|
|
54
63
|
const query = new Map<string, IrParam>();
|
|
55
64
|
const body = new Map<string, IrParam>();
|
|
56
65
|
const encodings = new Set<string>();
|
|
@@ -72,7 +81,9 @@ export function fromClientCalls(spec: ClientOpsSpec): SpecIR {
|
|
|
72
81
|
body: [...body.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
73
82
|
bodyEncoding,
|
|
74
83
|
successStatus: 200,
|
|
75
|
-
class: 'action',
|
|
84
|
+
class: first.method.toUpperCase() === 'GET' && answers ? (answers.list ? 'list' : 'retrieve') : 'action',
|
|
85
|
+
...(answers ? { answers } : {}),
|
|
86
|
+
...(resource ? { resource } : {}),
|
|
76
87
|
};
|
|
77
88
|
});
|
|
78
89
|
// two routes whose first calls share a client method (one method calling twice) keep distinct ids
|
|
@@ -82,5 +93,5 @@ export function fromClientCalls(spec: ClientOpsSpec): SpecIR {
|
|
|
82
93
|
seen.set(o.id, n);
|
|
83
94
|
if (n > 1) o.id = `${o.id}_${n}`;
|
|
84
95
|
}
|
|
85
|
-
return { format: 'client', version: spec.version, resources: [], operations };
|
|
96
|
+
return { format: 'client', version: spec.version, resources: spec.resources ?? [], operations };
|
|
86
97
|
}
|
package/src/spec-ir.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isExactIntegerValue } from '@volter/world-core/exact-json';
|
|
1
2
|
// THE SPEC IR — one representation of a vendor's published surface, beneath every spec format, that
|
|
2
3
|
// the derived-pack generator reads (docs/contributing/architecture.md, "Protocol 3"). A front end
|
|
3
4
|
// turns a spec into the IR; the generator only ever sees the IR. `fromOpenAPI` is the first front end, and `fromSmithy`
|
|
@@ -8,16 +9,19 @@
|
|
|
8
9
|
type Json = any;
|
|
9
10
|
|
|
10
11
|
export type OperationClass = 'create' | 'retrieve' | 'list' | 'update' | 'delete' | 'action' | 'computed' | 'non-resource';
|
|
12
|
+
/** Binary media whose resource shape is the response bytes, rather than JSON metadata. */
|
|
13
|
+
export const binaryMedia = (media: string): boolean => /^(?:application\/(?:octet-stream|pdf|zip|gzip)|(?:image|audio|video)\/[^/]+)$/.test(media.toLowerCase());
|
|
14
|
+
|
|
11
15
|
export const CRUD_CLASSES: ReadonlySet<OperationClass> = new Set(['create', 'retrieve', 'list', 'update', 'delete']);
|
|
12
16
|
|
|
13
17
|
/** A parameter or body field: its name, type and whether it is required, and the numeric bounds the spec states
|
|
14
18
|
* (`minimum`, `maximum`), which a strict body (the manifest's `body.strict`, `ctx.fields`) refuses outside of. */
|
|
15
|
-
export type IrParam = { name: string; type: string; required: boolean; minimum?: number; maximum?: number };
|
|
19
|
+
export type IrParam = { name: string; type: string; required: boolean; minimum?: number | string; maximum?: number | string };
|
|
16
20
|
|
|
17
21
|
/** A schema's stated numeric bounds, when it states any. */
|
|
18
|
-
function boundsOf(doc: Json, schema: Json): { minimum?: number; maximum?: number } {
|
|
22
|
+
function boundsOf(doc: Json, schema: Json): { minimum?: number | string; maximum?: number | string } {
|
|
19
23
|
const s = deref(doc, schema);
|
|
20
|
-
return { ...(typeof s?.minimum === 'number' ? { minimum: s.minimum } : {}), ...(typeof s?.maximum === 'number' ? { maximum: s.maximum } : {}) };
|
|
24
|
+
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 } : {}) };
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export type IrOperation = {
|
|
@@ -46,6 +50,9 @@ export type IrOperation = {
|
|
|
46
50
|
/** A form-encoded operation's number and boolean fields, by bracket path (formScalarsOf). */
|
|
47
51
|
scalars?: Record<string, 'integer' | 'number' | 'boolean'>;
|
|
48
52
|
successStatus: number;
|
|
53
|
+
/** Binary media types declared by each response status, independent of the twin’s response headers. */
|
|
54
|
+
responseMedia?: Record<string, string[]>;
|
|
55
|
+
defaultResponseMedia?: Record<string, string>;
|
|
49
56
|
/** the success is documented with no content at all (a 200 with nothing in it) */
|
|
50
57
|
emptySuccess?: true;
|
|
51
58
|
/** The success answer may be a server-sent event stream (`text/event-stream`). */
|
|
@@ -245,6 +252,17 @@ function successOf(doc: Json, op: Json, resources: Map<string, string>): { statu
|
|
|
245
252
|
const items = s?.type === 'array' ? s.items : s?.properties?.data?.type === 'array' ? s.properties.data.items : undefined;
|
|
246
253
|
const listed = items ? resourcesOfSchema(doc, items, resources) : [];
|
|
247
254
|
if (listed.length) return { status, answers: answer(listed, true) };
|
|
255
|
+
// a single object under `data` (X's create: a thin data object beside partial errors); a `data` that is an array, named
|
|
256
|
+
// by reference or inline, is a list's envelope, read below
|
|
257
|
+
if (s?.properties?.data && deref(doc, s.properties.data)?.type !== 'array') {
|
|
258
|
+
const payload = s.properties.data;
|
|
259
|
+
const name = refName(payload);
|
|
260
|
+
const found = resourcesOfSchema(doc, payload, resources);
|
|
261
|
+
if (found.length) return { status, answers: answer(found, false, 'data') };
|
|
262
|
+
if (name && propsOf(doc, payload).id) return { status, answers: answer([name], false, 'data') };
|
|
263
|
+
// A data payload without a modeled identity cannot make the accompanying errors the resource.
|
|
264
|
+
return { status };
|
|
265
|
+
}
|
|
248
266
|
// An envelope that holds exactly one resource, or one list of them, under a named property, beside
|
|
249
267
|
// at most a few scalars (`ok`, a cursor). An object with an `id` of its own, or with other objects
|
|
250
268
|
// in it, is not an envelope: it is something that embeds another (a Pages site, a branch).
|
|
@@ -281,7 +299,16 @@ function paramsOf(doc: Json, op: Json, pathItem: Json): { pathParams: string[];
|
|
|
281
299
|
}
|
|
282
300
|
|
|
283
301
|
function bodyOf(doc: Json, op: Json): { body: IrParam[]; bodyEncoding: IrOperation['bodyEncoding'] } {
|
|
284
|
-
const
|
|
302
|
+
const request = deref(doc, op.requestBody);
|
|
303
|
+
const content = request?.content ?? {};
|
|
304
|
+
// A whole binary body is the payload itself, not a JSON object with no fields. Keep the existing blob contract so
|
|
305
|
+
// the kernel leaves its bytes opaque even when a caller labels them application/json.
|
|
306
|
+
const representations = Object.values(content);
|
|
307
|
+
const binary = representations.length > 0 && representations.every((media: Json) => {
|
|
308
|
+
const schema = deref(doc, media?.schema);
|
|
309
|
+
return schema?.type === 'string' && schema.format === 'binary';
|
|
310
|
+
});
|
|
311
|
+
if (binary) return { body: [{ name: 'body', type: 'blob', required: request?.required === true }], bodyEncoding: 'none' };
|
|
285
312
|
const mediaOf = { json: 'application/json', form: 'application/x-www-form-urlencoded', multipart: 'multipart/form-data' } as const;
|
|
286
313
|
const encoding = content[mediaOf.json] ? 'json' : content[mediaOf.form] ? 'form' : content[mediaOf.multipart] ? 'multipart' : 'none';
|
|
287
314
|
if (encoding === 'none') return { body: [], bodyEncoding: 'none' };
|
|
@@ -412,7 +439,7 @@ export function fromOpenAPI(spec: Json): SpecIR {
|
|
|
412
439
|
const d = deref(doc, schema);
|
|
413
440
|
if (d?.type === 'array') return named(d.items);
|
|
414
441
|
const props = propsOf(doc, schema);
|
|
415
|
-
if (props.data
|
|
442
|
+
if (props.data) return deref(doc, props.data)?.type === 'array' ? named(deref(doc, props.data).items) : named(props.data);
|
|
416
443
|
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);
|
|
417
444
|
return held.length === 1 ? held[0]! : [];
|
|
418
445
|
};
|
|
@@ -434,6 +461,16 @@ export function fromOpenAPI(spec: Json): SpecIR {
|
|
|
434
461
|
const op = (item as Json)[method];
|
|
435
462
|
if (!op) continue;
|
|
436
463
|
const success = successOf(doc, op, resourceBySchema);
|
|
464
|
+
const responseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
|
|
465
|
+
const media = Object.keys(deref(doc, response)?.content ?? {}).filter(binaryMedia).sort();
|
|
466
|
+
return media.length ? [[code, media]] : [];
|
|
467
|
+
}));
|
|
468
|
+
const defaultResponseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
|
|
469
|
+
if (!responseMedia[code]) return [];
|
|
470
|
+
const media = Object.keys(deref(doc, response)?.content ?? {});
|
|
471
|
+
const defaultMedia = media.includes('application/json') ? 'application/json' : media.length === 1 ? media[0] : undefined;
|
|
472
|
+
return defaultMedia ? [[code, defaultMedia]] : [];
|
|
473
|
+
}));
|
|
437
474
|
const streams = Object.entries(op.responses ?? {}).some(([code, r]) => /^2\d\d$/.test(code) && deref(doc, r)?.content?.['text/event-stream']);
|
|
438
475
|
const security = (op.security ?? doc.security ?? []) as Json[];
|
|
439
476
|
const scopes = [...new Set(security.flatMap((req: Json) => Object.values(req ?? {}).flat() as string[]))].sort();
|
|
@@ -452,6 +489,7 @@ export function fromOpenAPI(spec: Json): SpecIR {
|
|
|
452
489
|
...body,
|
|
453
490
|
...(scalars ? { scalars } : {}),
|
|
454
491
|
successStatus: success.status,
|
|
492
|
+
...(Object.keys(responseMedia).length ? { responseMedia, ...(Object.keys(defaultResponseMedia).length ? { defaultResponseMedia } : {}) } : {}),
|
|
455
493
|
...(success.empty ? { emptySuccess: true as const } : {}),
|
|
456
494
|
...(streams ? { streams: true } : {}),
|
|
457
495
|
...(scopes.length ? { scopes } : {}),
|
package/src/verdict.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
7
|
+
/** Committed attribution names people or author roles, never execution identities. */
|
|
8
|
+
export function attributionName(name: string | undefined): string | undefined {
|
|
9
|
+
if (name === undefined) 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)) throw new Error('attribution must use an author name, not an execution identifier');
|
|
11
|
+
return name;
|
|
12
|
+
}
|
|
13
|
+
export type Verdict = { judge?: { agent?: string; name?: string; session?: string; review?: string }; authors?: string[] };
|
|
14
|
+
|
|
15
|
+
/** Why the verdict is no verdict, or undefined when it rules. `author` is the name the judged file itself names as
|
|
16
|
+
* its author (a life's `author`). */
|
|
17
|
+
export function notIndependent(verdict: Verdict, author?: string): string | undefined {
|
|
18
|
+
const agent = attributionName(verdict.judge?.agent);
|
|
19
|
+
verdict.authors?.forEach(attributionName);
|
|
20
|
+
attributionName(author);
|
|
21
|
+
attributionName(verdict.judge?.session);
|
|
22
|
+
attributionName(verdict.judge?.review);
|
|
23
|
+
const session = attributionName(verdict.judge?.name ?? verdict.judge?.session);
|
|
24
|
+
if (/self-judged/i.test(session ?? '') || (!!author && session === author)) return 'self-judged (not a verdict)';
|
|
25
|
+
if (!agent || !verdict.authors?.length) return 'unattributed (the dispatcher has not stamped judge.agent and authors)';
|
|
26
|
+
if (verdict.authors.includes(agent)) return `the judge is among the authors (${agent})`;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|