@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
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
// read for. `--sweep` prints, without changing done, every actor field (creator, user, *_by, ...) no `equals` pins and every
|
|
36
36
|
// list no expect closes: advice for fixing a ruling as a class (its hits include lists a pinned text already fixes).
|
|
37
37
|
//
|
|
38
|
-
// Usage: twin-standard examples <vendor> [--sweep] (from the pack repository's root
|
|
39
|
-
// bun scripts/vendor-examples.ts <vendor> runs the same)
|
|
38
|
+
// Usage: twin-standard examples <vendor> [--sweep] (from the pack repository's root)
|
|
40
39
|
import { parseExactJson, exactInteger, isExactIntegerValue } from '@volter/world-core/exact-json';
|
|
41
40
|
import { hasOwnSurface } from '../lanes.ts';
|
|
42
41
|
import { archiveSource } from '../archive-source.ts';
|
|
@@ -48,7 +47,7 @@ import { basename, join, resolve } from 'node:path';
|
|
|
48
47
|
import { gunzipSync } from 'node:zlib';
|
|
49
48
|
import { inMemoryWorld, walk, type Failure, usePacks } from '../walk/behavior-journey.ts';
|
|
50
49
|
import { vendorFetch, vendorStream } from '../walk/vendor-fetch.ts';
|
|
51
|
-
import { at as selectorAt, captureAt, expandFills, isResp, isGit,
|
|
50
|
+
import { at as selectorAt, captureAt, expandFills, isResp, isGit, isSocket, isWait, type Journey, type JourneyStep, type LineStep } from '../walk/journey-kit.ts';
|
|
52
51
|
import { lanesOf, shapeChecker } from '../walk/shape.ts';
|
|
53
52
|
import { applySpecPatch, discoveryToOpenAPI, mergeDocuments, readSpecDocuments, slug, type SpecPatch } from '../spec.ts';
|
|
54
53
|
import { packsRoot } from './packs-root.ts';
|
|
@@ -63,7 +62,7 @@ usePacks(PACKS);
|
|
|
63
62
|
* still be the sample's type. `schema` is the served spec's schema of the answer, which says where null is allowed. */
|
|
64
63
|
/** `placeholder`: strings of the published input matching it stand for ids the World mints (openai's `vs_abc123`), so the
|
|
65
64
|
* step sends a string there, not that one. */
|
|
66
|
-
type Example = { names?: string[]; wire?: 'client'; patchRemoved?: boolean; id: string; operation: string; lane?: string; status?: number; output: unknown; input?: unknown; partial?: boolean; placeholder?: RegExp; target?: string; restXml?:
|
|
65
|
+
type Example = { names?: string[]; wire?: 'client'; patchRemoved?: boolean; id: string; operation: string; lane?: string; status?: number; output: unknown; input?: unknown; partial?: boolean; placeholder?: RegExp; target?: string; restXml?: RestXml; lines?: string[]; sample?: boolean; schema?: SpecSchema; request?: { method: string; url: string; headers?: Record<string, string> }; authorizationAbsent?: boolean };
|
|
67
66
|
/** A served spec's schema, its references resolved lazily (`resolveSchema`). */
|
|
68
67
|
type SpecSchema = { $ref?: string; nullable?: boolean; properties?: Record<string, SpecSchema>; items?: SpecSchema; allOf?: SpecSchema[]; anyOf?: SpecSchema[]; oneOf?: SpecSchema[]; [k: string]: unknown };
|
|
69
68
|
let specSchemas: Record<string, SpecSchema> = {};
|
|
@@ -71,6 +70,7 @@ let specSchemas: Record<string, SpecSchema> = {};
|
|
|
71
70
|
const schemasByOperation: Record<string, Record<string, SpecSchema>> = {};
|
|
72
71
|
/** Each operation's request-body schema and its JSON answers' schemas by status, as the served spec gives them. */
|
|
73
72
|
const operationSchemas: Record<string, { request?: SpecSchema; responses: Record<string, SpecSchema> }> = {};
|
|
73
|
+
const operationKey = (operation: string, lane: string | undefined): string => lane ? `${lane}:${operation}` : operation;
|
|
74
74
|
|
|
75
75
|
/** A page's placeholder id in an answer-map key is the id sent at the corresponding published request position.
|
|
76
76
|
* Only declared placeholders bind, and an ambiguous repeated placeholder cannot stand for a guessed map key. */
|
|
@@ -94,7 +94,7 @@ function answerKeys(output: unknown, published: unknown, sent: unknown, placehol
|
|
|
94
94
|
return mapped(output);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/** The schemas the twin serves: the vendor's, with the pack's spec patches (spec/patches.json) applied as
|
|
97
|
+
/** The schemas the twin serves: the vendor's, with the pack's spec patches (spec/patches.json) applied as the derivation
|
|
98
98
|
* applies them. A sample is checked against what the twin is derived from; the published examples themselves stay the
|
|
99
99
|
* vendor's bytes. */
|
|
100
100
|
function served<T extends object>(pkg: string, doc: T): T {
|
|
@@ -107,7 +107,9 @@ function served<T extends object>(pkg: string, doc: T): T {
|
|
|
107
107
|
|
|
108
108
|
// ── a restXml service's examples (S3's): the input where each member's binding puts it, the answer read by its shape ──
|
|
109
109
|
type Json = any;
|
|
110
|
-
|
|
110
|
+
type SmithyModel = { shapes: Record<string, Json> };
|
|
111
|
+
type RestXml = { id: string; model: SmithyModel };
|
|
112
|
+
const restXmlService = (model: SmithyModel): boolean => Object.values(model.shapes).some((shape) => shape.type === 'service' && shape.traits?.['aws.protocols#restXml'] !== undefined);
|
|
111
113
|
/** A translated placeholder-key map is a complete map even when its surrounding documented request is partial. */
|
|
112
114
|
function requestMapKeysHold(published: unknown, translated: unknown, sent: unknown, placeholder?: RegExp): boolean {
|
|
113
115
|
if (!placeholder || !published || typeof published !== 'object') return true;
|
|
@@ -122,10 +124,10 @@ function requestMapKeysHold(published: unknown, translated: unknown, sent: unkno
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
const PRELUDE: Record<string, string> = { Boolean: 'boolean', PrimitiveBoolean: 'boolean', Integer: 'integer', PrimitiveInteger: 'integer', Long: 'long', PrimitiveLong: 'long', Float: 'float', Double: 'double' };
|
|
125
|
-
const scalarType = (target: string): string =>
|
|
126
|
-
const scalarOf = (target: string, v: unknown): unknown => {
|
|
127
|
+
const scalarType = (model: SmithyModel, target: string): string => model.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
|
|
128
|
+
const scalarOf = (model: SmithyModel, target: string, v: unknown): unknown => {
|
|
127
129
|
if (typeof v !== 'string') return v;
|
|
128
|
-
const t = scalarType(target);
|
|
130
|
+
const t = scalarType(model, target);
|
|
129
131
|
if (t === 'boolean') return v === 'true';
|
|
130
132
|
if (['integer', 'long', 'short', 'byte'].includes(t)) return /^[+-]?\d+$/.test(v) ? exactInteger(v) : v;
|
|
131
133
|
return ['float', 'double'].includes(t) && v !== '' && !Number.isNaN(Number(v)) ? Number(v) : v;
|
|
@@ -134,39 +136,39 @@ const arrayOf = (v: unknown): unknown[] => (v === undefined || v === '' ? [] : A
|
|
|
134
136
|
|
|
135
137
|
/** An answer's XML members (xml-view) read by the model's shape: each member under its xmlName, a flattened list the
|
|
136
138
|
* repeated element, a wrapped list its container's member elements, scalars as their shape's type. */
|
|
137
|
-
function typedXml(target: string, v: unknown): unknown {
|
|
138
|
-
const shape =
|
|
139
|
-
if (!shape || shape.type !== 'structure') return scalarOf(target, v);
|
|
139
|
+
function typedXml(model: SmithyModel, target: string, v: unknown): unknown {
|
|
140
|
+
const shape = model.shapes[target];
|
|
141
|
+
if (!shape || shape.type !== 'structure') return scalarOf(model, target, v);
|
|
140
142
|
if (!v || typeof v !== 'object') return v === '' ? {} : v;
|
|
141
143
|
const obj = v as Record<string, unknown>;
|
|
142
144
|
const out: Record<string, unknown> = {};
|
|
143
145
|
for (const [name, m] of Object.entries((shape.members ?? {}) as Record<string, Json>)) {
|
|
144
146
|
if (Object.keys(m.traits ?? {}).some((t) => /^smithy\.api#http(Header|PrefixHeaders|ResponseCode)$/.test(t))) continue;
|
|
145
147
|
const el = String(m.traits?.['smithy.api#xmlName'] ?? name);
|
|
146
|
-
const ts =
|
|
148
|
+
const ts = model.shapes[m.target];
|
|
147
149
|
const raw = obj[el];
|
|
148
150
|
if (ts?.type === 'list') {
|
|
149
|
-
if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) { if (raw !== undefined) out[name] = arrayOf(raw).map((i) => typedXml(ts.member.target, i)); continue; }
|
|
151
|
+
if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) { if (raw !== undefined) out[name] = arrayOf(raw).map((i) => typedXml(model, ts.member.target, i)); continue; }
|
|
150
152
|
if (raw === undefined) continue;
|
|
151
153
|
const memberEl = String(ts.member.traits?.['smithy.api#xmlName'] ?? 'member');
|
|
152
|
-
out[name] = arrayOf(raw === '' ? undefined : (raw as Record<string, unknown>)[memberEl]).map((i) => typedXml(ts.member.target, i));
|
|
153
|
-
} else if (raw !== undefined) out[name] = typedXml(m.target, raw);
|
|
154
|
+
out[name] = arrayOf(raw === '' ? undefined : (raw as Record<string, unknown>)[memberEl]).map((i) => typedXml(model, ts.member.target, i));
|
|
155
|
+
} else if (raw !== undefined) out[name] = typedXml(model, m.target, raw);
|
|
154
156
|
}
|
|
155
157
|
return out;
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
/** A restXml operation's success answer as its output structure: the body's members by shape (or its payload
|
|
159
161
|
* member's), and the members its headers carry. */
|
|
160
|
-
function restXmlAnswer(opId:
|
|
161
|
-
const out =
|
|
162
|
+
function restXmlAnswer({ id: opId, model }: RestXml, body: unknown, headers: Record<string, string> = {}): unknown {
|
|
163
|
+
const out = model.shapes[model.shapes[opId]?.output?.target ?? ''];
|
|
162
164
|
if (!out) return body;
|
|
163
165
|
const members = Object.entries((out.members ?? {}) as Record<string, Json>);
|
|
164
166
|
const payload = members.find(([, m]) => m.traits?.['smithy.api#httpPayload'] !== undefined);
|
|
165
|
-
const answer = (payload ? { [payload[0]]: typedXml(payload[1].target, body) } : typedXml(
|
|
167
|
+
const answer = (payload ? { [payload[0]]: typedXml(model, payload[1].target, body) } : typedXml(model, model.shapes[opId].output.target, body)) as Record<string, unknown>;
|
|
166
168
|
const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
167
169
|
for (const [name, m] of members) {
|
|
168
170
|
const h = m.traits?.['smithy.api#httpHeader'];
|
|
169
|
-
if (h !== undefined && lower[String(h).toLowerCase()] !== undefined) answer[name] = scalarOf(m.target, lower[String(h).toLowerCase()]);
|
|
171
|
+
if (h !== undefined && lower[String(h).toLowerCase()] !== undefined) answer[name] = scalarOf(model, m.target, lower[String(h).toLowerCase()]);
|
|
170
172
|
// a map bound to a header prefix (S3's Metadata, `x-amz-meta-*`): every header under it, by the rest of its name
|
|
171
173
|
const prefix = m.traits?.['smithy.api#httpPrefixHeaders'];
|
|
172
174
|
if (prefix !== undefined) answer[name] = Object.fromEntries(Object.entries(lower).filter(([k]) => k.startsWith(String(prefix).toLowerCase())).map(([k, v]) => [k.slice(String(prefix).length), v]));
|
|
@@ -176,8 +178,8 @@ function restXmlAnswer(opId: string, body: unknown, headers: Record<string, stri
|
|
|
176
178
|
|
|
177
179
|
/** Where a restXml example's input is not what the step sends: each member the example gives, at its binding (a path
|
|
178
180
|
* label, a query key, a header; a payload or body member is the step's body and is not compared). */
|
|
179
|
-
function restXmlInputProblems(opId:
|
|
180
|
-
const shape =
|
|
181
|
+
function restXmlInputProblems({ id: opId, model }: RestXml, input: Record<string, unknown>, path: string, headers: Record<string, string>, placeholder: RegExp, rawPath?: string): string[] {
|
|
182
|
+
const shape = model.shapes[model.shapes[opId]?.input?.target ?? ''];
|
|
181
183
|
const url = new URL(path, 'http://twin.local');
|
|
182
184
|
// the step as written, its captures unfilled: a value it takes from a capture is the World's own id (an S3 upload's
|
|
183
185
|
// UploadId), which stands for the example's literal as a spec's ids stand for the World's (architecture, "Published examples")
|
|
@@ -215,11 +217,13 @@ function lenientJson(text: string): string {
|
|
|
215
217
|
* response's `example`/`examples` (a `$ref` to `components.examples` followed), a Swagger 2 response's `examples`, Stripe's
|
|
216
218
|
* fixtures (one sample per resource, beside its spec), and OpenAI's `x-oaiMeta.examples` (its `response` is JSON text; one that is not JSON, a stream or an abridged answer, is
|
|
217
219
|
* kept as text and compared with the published framing and payload). */
|
|
218
|
-
function published(pkg: string): Example[] {
|
|
220
|
+
function published(pkg: string, lane: string | undefined): Example[] {
|
|
219
221
|
// Both the vendor API and its lanes publish examples; a root spec must not hide a lane. A lane's example names its
|
|
220
222
|
// lane, the operation being the lane's own (its owner is `<lane>:<id>` in the vendor's owners)
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
+
const smithy = ['smithy.json.gz', 'smithy.json'].map((name) => join(pkg, 'spec', name)).find(existsSync);
|
|
224
|
+
const model = smithy ? parseExactJson((smithy.endsWith('.gz') ? gunzipSync(readFileSync(smithy)) : readFileSync(smithy)).toString('utf8')) as SmithyModel : undefined;
|
|
225
|
+
const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg, lane, model) : []), ...fromDocs(pkg, lane, model)].map((e) => ({ ...e, lane }));
|
|
226
|
+
return [...own, ...lanesOf(pkg).flatMap((dir) => published(dir, lane ? `${lane}:${basename(dir)}` : basename(dir)))];
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
/** The examples a vendor's documentation pages publish beside its spec (a schedule page's expressions, each with what
|
|
@@ -231,7 +235,7 @@ function published(pkg: string): Example[] {
|
|
|
231
235
|
* reference writes every property, and every scalar as a string, `"verified": "true"` beside a schema saying boolean)
|
|
232
236
|
* shows what the answer can hold, not one request's answer: its example is a sample against the served schema, and a
|
|
233
237
|
* string where the schema says number, integer or boolean is read as that type, in the answer and the request body. */
|
|
234
|
-
function fromDocs(pkg: string): Example[] {
|
|
238
|
+
function fromDocs(pkg: string, lane: string | undefined, model: SmithyModel | undefined): Example[] {
|
|
235
239
|
const file = join(pkg, 'spec', 'doc-examples.json');
|
|
236
240
|
if (!existsSync(file)) return [];
|
|
237
241
|
const docs = parseExactJson(readFileSync(file, 'utf8')) as Array<{ wire?: 'client'; title: string; source: string; quote?: string; operation: string; input?: unknown; output?: unknown; means: string; status?: number; rendered?: boolean; placeholders?: { names: string[]; quote: string }; request?: { method: string; url: string; headers?: Record<string, string> } }>;
|
|
@@ -261,11 +265,10 @@ function fromDocs(pkg: string): Example[] {
|
|
|
261
265
|
// a page that names its own variables for the reader to fill ("Make sure to fill in the … BRANCH_NAME … variables")
|
|
262
266
|
// lists them with that sentence quoted: each stands for any value, and the published text stays as written
|
|
263
267
|
for (const d of docs) if (d.placeholders && (!d.placeholders.names?.length || !d.placeholders.quote?.trim())) throw new Error(`${file}: "${d.title}" names placeholders without the page's words for them (placeholders.quote)`);
|
|
264
|
-
const smithy = ['smithy.json.gz', 'smithy.json'].some((f) => existsSync(join(pkg, 'spec', f)));
|
|
265
268
|
// a page's example is a fragment (an expression, not the request that carries it): the body holds it
|
|
266
269
|
return docs.map((d) => {
|
|
267
270
|
if (!d.rendered) return fromPage(d, d.output, d.input);
|
|
268
|
-
const schemas = operationSchemas[d.operation];
|
|
271
|
+
const schemas = operationSchemas[operationKey(d.operation, lane)];
|
|
269
272
|
const schema = schemas?.responses[String(d.status ?? 200)];
|
|
270
273
|
if (!schema) throw new Error(`${file}: "${d.title}" is rendered from a schema the served spec does not give (${d.operation}, ${d.status ?? 200})`);
|
|
271
274
|
// the renderer writes any string it cannot fill as the word "string": in a rendered input it stands for any string
|
|
@@ -276,7 +279,13 @@ function fromDocs(pkg: string): Example[] {
|
|
|
276
279
|
function fromPage(d: (typeof docs)[number], output: unknown, input: unknown): Example {
|
|
277
280
|
const named = d.placeholders?.names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
278
281
|
const placeholder = named ? new RegExp(`${placeholderIds.source}|^(?:${named})$`, placeholderIds.flags) : placeholderIds;
|
|
279
|
-
|
|
282
|
+
let restXml: RestXml | undefined;
|
|
283
|
+
if (model && restXmlService(model)) {
|
|
284
|
+
const id = Object.keys(model.shapes).find((key) => key.endsWith(`#${d.operation}`));
|
|
285
|
+
if (!id) throw new Error(`${file}: "${d.title}" names no operation in its XML service (${d.operation})`);
|
|
286
|
+
restXml = { id, model };
|
|
287
|
+
}
|
|
288
|
+
return { names: d.placeholders?.names, ...(d.wire ? { wire: d.wire } : {}), id: `${d.operation}#doc ${d.title}`, operation: d.operation, output, ...(d.status ? { status: d.status } : {}), ...(input !== undefined ? { input, partial: true, placeholder } : {}), ...(model ? (restXml ? { restXml } : { target: d.operation }) : {}), ...(d.request ? { request: d.request } : {}), ...(authorizationAbsent.has(d) ? { authorizationAbsent: true } : {}) };
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
291
|
|
|
@@ -351,11 +360,23 @@ function decodedExample(media: string, value: unknown): unknown {
|
|
|
351
360
|
try { return parseExactJson(value); } catch { return value; }
|
|
352
361
|
}
|
|
353
362
|
|
|
354
|
-
function fromSpec(pkg: string): Example[] {
|
|
363
|
+
function fromSpec(pkg: string, lane: string | undefined, model: SmithyModel | undefined): Example[] {
|
|
355
364
|
const rfc = existsSync(join(pkg, 'spec')) ? readdirSync(join(pkg, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
|
|
356
365
|
if (rfc) return scenarios(readFileSync(join(pkg, 'spec', rfc), 'utf8'));
|
|
357
|
-
|
|
358
|
-
|
|
366
|
+
const out: Example[] = [];
|
|
367
|
+
if (model) {
|
|
368
|
+
const restXml = restXmlService(model);
|
|
369
|
+
for (const [id, shape] of Object.entries(model.shapes)) {
|
|
370
|
+
const examples = shape.traits?.['smithy.api#examples'] as Array<{ title: string; input?: unknown; output?: unknown; error?: { content?: unknown } }> | undefined;
|
|
371
|
+
const operation = id.split('#')[1];
|
|
372
|
+
// an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
|
|
373
|
+
// restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
|
|
374
|
+
for (const e of examples ?? []) out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: { id, model } } : { target: operation }) });
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
// a spec vendored as published or gzipped, as the derivation reads it
|
|
379
|
+
const spec = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'discovery.json.gz', 'discovery.json'].map((f) => join(pkg, 'spec', f)).find((p) => existsSync(p));
|
|
359
380
|
// a Protocol Buffers service publishes no examples in its .proto files: its examples are its documentation's alone
|
|
360
381
|
if (!spec && existsSync(join(pkg, 'spec', 'proto'))) return [];
|
|
361
382
|
// a command table (Redis's src/commands/*.json) publishes no examples either: its examples are its documentation's
|
|
@@ -366,23 +387,9 @@ function fromSpec(pkg: string): Example[] {
|
|
|
366
387
|
const documents = spec ? undefined : readSpecDocuments(join(pkg, 'spec'));
|
|
367
388
|
if (!spec && !documents) throw new Error(`${pkg}: no published-example reader for this spec yet`);
|
|
368
389
|
const text = spec ? (spec.endsWith('.gz') ? gunzipSync(readFileSync(spec)) : readFileSync(spec)).toString('utf8') : '';
|
|
369
|
-
const out: Example[] = [];
|
|
370
|
-
if (spec && /smithy\.json(\.gz)?$/.test(spec)) {
|
|
371
|
-
const model = parseExactJson(text) as { shapes: Record<string, { type?: string; traits?: Record<string, unknown> }> };
|
|
372
|
-
const restXml = Object.values(model.shapes).some((sh) => sh.type === 'service' && sh.traits?.['aws.protocols#restXml'] !== undefined);
|
|
373
|
-
if (restXml) xmlModel = model as { shapes: Record<string, Json> };
|
|
374
|
-
for (const [id, shape] of Object.entries(model.shapes)) {
|
|
375
|
-
const examples = shape.traits?.['smithy.api#examples'] as Array<{ title: string; input?: unknown; output?: unknown; error?: { content?: unknown } }> | undefined;
|
|
376
|
-
const operation = id.split('#')[1];
|
|
377
|
-
// an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
|
|
378
|
-
// restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
|
|
379
|
-
for (const e of examples ?? []) out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: id } : { target: operation }) });
|
|
380
|
-
}
|
|
381
|
-
return out;
|
|
382
|
-
}
|
|
383
390
|
type Op = { operationId?: string; responses?: Record<string, Record<string, unknown>>; 'x-oaiMeta'?: { examples?: unknown } };
|
|
384
391
|
type Doc = { paths: Record<string, Record<string, Op>>; components?: { examples?: Record<string, { value?: unknown }>; schemas?: Record<string, SpecSchema> }; definitions?: Record<string, SpecSchema> };
|
|
385
|
-
// a Google API Discovery document publishes no examples; it is read as the OpenAPI document
|
|
392
|
+
// a Google API Discovery document publishes no examples; it is read as the OpenAPI document the derivation reads it as,
|
|
386
393
|
// its patches applied to the Discovery document first (spec-ir-discovery.ts)
|
|
387
394
|
const discovery = spec !== undefined && /discovery\.json(\.gz)?$/.test(spec);
|
|
388
395
|
const docs = (documents ? Object.values(documents.documents) : [discovery ? discoveryToOpenAPI(parseExactJson(text)) : /\.yaml(\.gz)?$/.test(spec!) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_key, value) => typeof value === 'bigint' ? exactInteger(String(value)) : value)) : parseExactJson(text)]) as Doc[];
|
|
@@ -399,8 +406,9 @@ function fromSpec(pkg: string): Example[] {
|
|
|
399
406
|
if (!op || typeof op !== 'object' || !('responses' in op)) continue;
|
|
400
407
|
// Swagger 2: the request is the `in: body` parameter's schema, an answer its response's `schema`
|
|
401
408
|
const bodyParam = ((op as { parameters?: Array<{ in?: string; schema?: SpecSchema }> }).parameters ?? []).find((x) => x.in === 'body');
|
|
402
|
-
|
|
403
|
-
|
|
409
|
+
const key = operationKey(idOf(op, method, path), lane);
|
|
410
|
+
schemasByOperation[key] = specSchemas;
|
|
411
|
+
operationSchemas[key] = {
|
|
404
412
|
request: json((op as { requestBody?: { content?: unknown } }).requestBody?.content) ?? bodyParam?.schema,
|
|
405
413
|
responses: Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, r]) => { const sch = json(r.content) ?? (r as { schema?: SpecSchema }).schema; return sch ? [[code, sch]] : []; })),
|
|
406
414
|
};
|
|
@@ -829,17 +837,17 @@ function outputSchemaMismatches(schema: SpecSchema | undefined, value: unknown,
|
|
|
829
837
|
* recorded with the per-example classification. Request echoes and generated output are absent. */
|
|
830
838
|
const storedCollectionFields: Record<string, Record<string, string[]>> = {
|
|
831
839
|
'ai-gateway': { listModels: ['data'] },
|
|
832
|
-
aws: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
|
|
833
|
-
cloudflare: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
|
|
840
|
+
aws: { 's3:ListBuckets': ['Buckets'], 's3:ListMultipartUploads': ['Uploads'], 's3:ListObjects': ['Contents', 'CommonPrefixes'], 's3:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
|
|
841
|
+
cloudflare: { 'r2:ListBuckets': ['Buckets'], 'r2:ListMultipartUploads': ['Uploads'], 'r2:ListObjects': ['Contents', 'CommonPrefixes'], 'r2:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
|
|
834
842
|
discord: { oauth2_token: ['guild.roles'], get_my_oauth2_authorization: ['scopes'] },
|
|
835
843
|
github: { 'repos/get-branch': ['commit.parents'], 'repos/get-commit': ['parents', 'files'], 'repos/get-content': ['', 'entries'], 'repos/compare-commits': ['commits', 'files'], 'git/get-tree': ['tree'], 'git/list-matching-refs': [''], 'repos/get-repo-rulesets': [''] },
|
|
836
|
-
hackernews: { getItem: ['kids'], getUser: ['submitted'], getUpdates: ['items', 'profiles'], getTopStories: [''], getNewStories: [''], getBestStories: [''], getAskStories: [''], getShowStories: [''], getJobStories: [''] },
|
|
844
|
+
hackernews: { 'api:getItem': ['kids'], 'api:getUser': ['submitted'], 'api:getUpdates': ['items', 'profiles'], 'api:getTopStories': [''], 'api:getNewStories': [''], 'api:getBestStories': [''], 'api:getAskStories': [''], 'api:getShowStories': [''], 'api:getJobStories': [''] },
|
|
837
845
|
linkedin: { findOrganizationAclsByRoleAssignee: ['elements'] },
|
|
838
846
|
'npm-registry': { 'libnpmsearch.searchStream': ['objects', 'objects.[].package.maintainers'] },
|
|
839
847
|
openrouter: { getCurrentKey: ['data.allowed_data_regions'], getModels: ['data'] },
|
|
840
848
|
redis: { SCAN: ['[].1'] },
|
|
841
849
|
resend: { 'domains/get': ['records'], 'emails/list-receiving': ['data', 'data.[].attachments'] },
|
|
842
|
-
sentry: { listOrganizations: ['', '[].features'] },
|
|
850
|
+
sentry: { 'api:listOrganizations': ['', '[].features'] },
|
|
843
851
|
slack: { conversations_rename: ['channel.previous_names'], conversations_invite: ['channel.previous_names'], conversations_members: ['members'], conversations_list: ['channels', 'channels.[].previous_names'], users_conversations: ['channels', 'channels.[].previous_names'], conversations_info: ['channel.previous_names'], conversations_replies: ['messages'], conversations_history: ['messages'], usergroups_list: ['usergroups'] },
|
|
844
852
|
tinybird: { datasources: ['datasources'], jobs: ['jobs'], pipes: ['pipes'], tokens: ['tokens'], token_get: ['scopes'], token_refresh: ['scopes'], pipe_set_endpoint: ['nodes'] },
|
|
845
853
|
};
|
|
@@ -931,7 +939,7 @@ if (!vendor || vendor.startsWith('-')) { console.error('usage: twin-standard exa
|
|
|
931
939
|
const PKG = join(PACKS, vendor);
|
|
932
940
|
const file = join(PKG, 'journeys', 'vendor-examples.json');
|
|
933
941
|
await loadPlaceholders(PKG);
|
|
934
|
-
const examples = published(PKG);
|
|
942
|
+
const examples = published(PKG, undefined);
|
|
935
943
|
const byId = new Map(examples.map((e) => [e.id, e]));
|
|
936
944
|
|
|
937
945
|
if (byId.size !== examples.length) throw new Error(`${vendor}: ${examples.length - byId.size} published examples share an id`);
|
|
@@ -948,9 +956,11 @@ const shape = await shapeChecker(PKG);
|
|
|
948
956
|
// (a page of an operation the spec lacks can match another's route)
|
|
949
957
|
for (const e of examples) {
|
|
950
958
|
if (e.patchRemoved || !e.request || !shape) continue;
|
|
951
|
-
const
|
|
959
|
+
const hit = shape.operationOf(e.request.method, new URL(e.request.url, `http://${journey.host ?? 'twin.local'}`).href, e.request.headers);
|
|
960
|
+
const routed = hit ? operationKey(hit.op.id, hit.lane) : undefined;
|
|
952
961
|
if (e.operation === 'gap' && routed === undefined) continue;
|
|
953
|
-
|
|
962
|
+
const expected = operationKey(e.operation, e.lane);
|
|
963
|
+
if (routed !== expected) throw new Error(`spec/doc-examples.json: "${e.id}" sends ${e.request.method} ${e.request.url}, which the spec routes to ${routed ?? 'no operation'}, not ${expected}`);
|
|
954
964
|
}
|
|
955
965
|
const sweeping = process.argv.includes('--sweep');
|
|
956
966
|
const sweepHits: string[] = [];
|
|
@@ -1065,17 +1075,19 @@ await walk(journey, world.root(), failures, {
|
|
|
1065
1075
|
},
|
|
1066
1076
|
after: (step, _path, answer, stepFailures) => {
|
|
1067
1077
|
if (isGit(step) || isWait(step) || isSocket(step)) return;
|
|
1068
|
-
|
|
1078
|
+
const request = answer.request;
|
|
1079
|
+
const hit = request ? shape?.operationOf(request.method, request.url, request.headers) : undefined;
|
|
1080
|
+
const operation = hit?.op;
|
|
1081
|
+
if (request) {
|
|
1069
1082
|
const st = step as JourneyStep;
|
|
1070
|
-
const method =
|
|
1083
|
+
const method = request.method;
|
|
1071
1084
|
// a read by what the operation does (an RPC wire POSTs its reads), else by its verb
|
|
1072
|
-
const
|
|
1073
|
-
const opClass = op?.class;
|
|
1085
|
+
const opClass = operation?.class;
|
|
1074
1086
|
const isRead = opClass ? ['retrieve', 'list', 'computed'].includes(opClass) : method === 'GET';
|
|
1075
|
-
if (isRead) reads.push({ path: bare(
|
|
1087
|
+
if (isRead) reads.push({ path: bare(request.url), body: JSON.stringify(answer.body ?? '') });
|
|
1076
1088
|
else if (st.example && answer.status < 300 && answer.body && typeof answer.body === 'object' && !Array.isArray(answer.body) && Object.keys(answer.body).length <= 3) {
|
|
1077
1089
|
const body = answer.body as Record<string, unknown>;
|
|
1078
|
-
const segs = bare(
|
|
1090
|
+
const segs = bare(request.url).split('/');
|
|
1079
1091
|
// an action on a subject (`/emails/{id}/cancel`) touches the subject; a create touches the new id under the collection;
|
|
1080
1092
|
// a write whose subject is neither minted nor in its path (an RPC call naming it in its body) is left to the judge
|
|
1081
1093
|
const minted = typeof body.id === 'string' ? body.id : '';
|
|
@@ -1083,8 +1095,7 @@ await walk(journey, world.root(), failures, {
|
|
|
1083
1095
|
if (minted || idInPath) {
|
|
1084
1096
|
const base = minted && !segs.includes(minted) ? `${segs.join('/')}/${minted}` : /^[a-z_-]+$/.test(segs.at(-1) ?? '') && segs.length > 2 ? segs.slice(0, -1).join('/') : segs.join('/');
|
|
1085
1097
|
// only an effect the vendor gives a door to read: a GET of the subject or of its collection (a moderation has neither)
|
|
1086
|
-
const
|
|
1087
|
-
const gets = (at: string): boolean => shape?.operationOf('GET', at, hdrs)?.op.method.toUpperCase() === 'GET';
|
|
1098
|
+
const gets = (at: string): boolean => shape?.operationOf('GET', new URL(at, request.url).href, request.headers)?.op.method.toUpperCase() === 'GET';
|
|
1088
1099
|
const door = !shape || gets(base) || gets(base.split('/').slice(0, -1).join('/'));
|
|
1089
1100
|
if (door) thinWrites.push({ step: st.name, base, id: base.split('/').at(-1) ?? '', from: reads.length, deletes: method === 'DELETE' || opClass === 'delete' });
|
|
1090
1101
|
}
|
|
@@ -1104,38 +1115,40 @@ await walk(journey, world.root(), failures, {
|
|
|
1104
1115
|
if (!s.example) return;
|
|
1105
1116
|
const ex = byId.get(s.example);
|
|
1106
1117
|
if (!ex) { unknown.push(s.example); return; }
|
|
1107
|
-
|
|
1118
|
+
const key = operationKey(ex.operation, ex.lane);
|
|
1119
|
+
specSchemas = schemasByOperation[key] ?? {};
|
|
1108
1120
|
replayed.add(s.example);
|
|
1109
|
-
if (ex.operation === 'gap' && ex.request) {
|
|
1110
|
-
|
|
1111
|
-
if (s.method !== ex.request.method || sent.href !== ex.request.url) failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
|
|
1121
|
+
if (ex.operation === 'gap' && ex.request && request) {
|
|
1122
|
+
if (request.method !== ex.request.method || request.url !== ex.request.url) failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
|
|
1112
1123
|
}
|
|
1113
|
-
if (ex.authorizationAbsent &&
|
|
1124
|
+
if (ex.authorizationAbsent && request && new Headers(request.headers).has('authorization')) failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the recording has no Authorization header, but the replay sends one' });
|
|
1114
1125
|
// the step calls the example's operation (the spec's, as the walk's SHAPE check reads the request)
|
|
1115
|
-
if (
|
|
1116
|
-
const called =
|
|
1117
|
-
if (called && called !==
|
|
1126
|
+
if (request && shape) {
|
|
1127
|
+
const called = hit ? operationKey(hit.op.id, hit.lane) : undefined;
|
|
1128
|
+
if (called && called !== key) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${key}` });
|
|
1118
1129
|
}
|
|
1119
1130
|
// the example is sent as published: its input as the body, to its operation
|
|
1120
1131
|
// a published field the step leaves out (`unsent`: a rendered value naming an object the World does not hold) must
|
|
1121
1132
|
// be one the operation's request schema does not require
|
|
1122
1133
|
for (const [path, why] of Object.entries(s.unsent ?? {})) {
|
|
1123
1134
|
if (!why) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} gives no reason` });
|
|
1124
|
-
if (requiredAt(operationSchemas[
|
|
1135
|
+
if (requiredAt(operationSchemas[key]?.request, path)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} is required by the operation's request schema` });
|
|
1125
1136
|
}
|
|
1126
1137
|
const publishedBody = exampleBody({ ...s, body: ex.input, form: undefined, bodyBase64: undefined }, ex.input);
|
|
1127
1138
|
const inputType = Object.entries(ex.request?.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1]
|
|
1128
|
-
??
|
|
1139
|
+
?? (request ? new Headers(request.headers).get('content-type') ?? undefined : undefined);
|
|
1129
1140
|
const publishedInput = s.unsent && publishedBody !== undefined ? withoutPaths(publishedBody, Object.keys(s.unsent)) : publishedBody;
|
|
1130
1141
|
// the body the step sends, every check below reading it alike: no body (a GET) is the empty input a page publishes for it
|
|
1131
1142
|
const body = isResp(s) ? s.resp : exampleBody(s, publishedInput) ?? {};
|
|
1132
1143
|
// A placeholder used as a request-map key is bound by its scalar request position, just as an answer key is.
|
|
1133
1144
|
const input = answerKeys(publishedInput, publishedInput, body, ex.placeholder, true);
|
|
1134
|
-
if (ex.restXml)
|
|
1145
|
+
if (ex.restXml) {
|
|
1146
|
+
if (request) for (const p of restXmlInputProblems(ex.restXml, (input ?? {}) as Record<string, unknown>, shape?.pathOf(request.method, request.url, request.headers) ?? request.url, request.headers, placeholderIds, (s as { path?: string }).path)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
|
|
1147
|
+
}
|
|
1135
1148
|
else if (input !== undefined && (!requestMapKeysHold(publishedInput, input, body, ex.placeholder) || (ex.partial ? !holds(body, input, ex.placeholder, undefined, ex.names, false, inputType) : !same(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, false, inputType)))) failures.push({ step: s.name, beat: 'EXAMPLE', detail: typeof input === 'string' ? comparedText(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, inputType)?.problem ?? "the body is not the example's published input" : "the body is not the example's published input" });
|
|
1136
1149
|
// a scenario's turn is sent as the RFC writes it: its client lines (none for the greeting, a connect)
|
|
1137
1150
|
if (ex.lines && JSON.stringify([s.send ?? []].flat()) !== JSON.stringify(ex.lines)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `sends ${JSON.stringify(s.send ?? null)}, not the scenario's ${JSON.stringify(ex.lines)}` });
|
|
1138
|
-
const target =
|
|
1151
|
+
const target = request ? new Headers(request.headers).get('x-amz-target') : undefined;
|
|
1139
1152
|
if (ex.target && !target?.endsWith(`.${ex.target}`)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `x-amz-target is ${target}, not the example's operation ${ex.target}` });
|
|
1140
1153
|
if (ex.status && answer.status !== ex.status && !s.exampleDiffers?.status) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `answered ${answer.status}, the example ${ex.status}` });
|
|
1141
1154
|
if (stepFailures.length) return;
|
|
@@ -1166,9 +1179,8 @@ await walk(journey, world.root(), failures, {
|
|
|
1166
1179
|
const published = isResp(s) && publishedValue !== undefined ? (Array.isArray(s.resp?.[0]) ? publishedValue : [publishedValue]) : publishedValue;
|
|
1167
1180
|
const responseType = answer.headers?.['content-type'];
|
|
1168
1181
|
const literal = isResp(s) || (published !== null && typeof published !== 'object');
|
|
1169
|
-
const valueSchema = ex.schema ?? operationSchemas[
|
|
1170
|
-
comparisonCollections = [...(storedCollectionFields[vendor]?.[
|
|
1171
|
-
const operation = shape?.operationOf((s.method ?? 'GET').toUpperCase(), _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) })?.op;
|
|
1182
|
+
const valueSchema = ex.schema ?? operationSchemas[key]?.responses[String(answer.status)];
|
|
1183
|
+
comparisonCollections = [...(storedCollectionFields[vendor]?.[key] ?? [])];
|
|
1172
1184
|
// A derived resource list names its holder; never treat every array under a read as a list.
|
|
1173
1185
|
if (operation?.class === 'list' && operation.answers?.list) {
|
|
1174
1186
|
const holder = operation.answers.key ? (published as Record<string, unknown>)?.[operation.answers.key] : published;
|
|
@@ -1209,7 +1221,7 @@ await walk(journey, world.root(), failures, {
|
|
|
1209
1221
|
// an example of an operation the twin does not serve is breadth's gap (score-pack), counted here and not replayed
|
|
1210
1222
|
type Owned = { owners?: () => Record<string, string> } | undefined;
|
|
1211
1223
|
const owners = ((await vendorFetch(PACKS, vendor))?.(world.root()) as Owned)?.owners?.() ?? ((await vendorStream(PACKS, vendor))?.(world.root()) as Owned)?.owners?.() ?? {};
|
|
1212
|
-
const ownerOf = (e: Example): string | undefined => e.wire === 'client' ? 'gap' : owners[e.
|
|
1224
|
+
const ownerOf = (e: Example): string | undefined => e.wire === 'client' ? 'gap' : owners[operationKey(e.operation, e.lane)];
|
|
1213
1225
|
const patchRemoved = examples.filter((e) => e.patchRemoved);
|
|
1214
1226
|
const unserved = examples.filter((e) => !e.patchRemoved && ownerOf(e) === 'gap');
|
|
1215
1227
|
const recorded = Object.keys(journey.notReplayed ?? {});
|
package/src/verdict.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A JUDGE'S INDEPENDENCE (the grade's outline-judged and life-judged, the published examples' verdict): a verdict rules
|
|
2
2
|
// only when it names its judge (`judge.agent`) and the authors of what it judges (`authors`), and the judge is none of
|
|
3
3
|
// them. The dispatcher stamps author names consistently; a judge started independently
|
|
4
|
-
// (
|
|
4
|
+
// (`twin-standard judge-brief` prints its brief) names its reviewer role (`judge.name`). One judging per pack: the
|
|
5
5
|
// verdict stands, and later changes to what it judged keep it (architecture, "Writing a life", Judge).
|
|
6
6
|
|
|
7
7
|
/** Committed attribution names people or author roles, never execution identities. */
|