@volter/twin-standard 1.0.148 → 1.0.150
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 +12 -3
- package/client-sdks/bun.lock +0 -32
- package/client-sdks/package.json +0 -2
- package/dist/client-sdks/bun.lock +0 -32
- package/dist/client-sdks/package.json +0 -2
- package/dist/src/assessment.d.ts +6 -4
- package/dist/src/assessment.js +2 -1
- package/dist/src/cli.js +21 -6
- package/dist/src/clients/harness.d.ts +3 -1
- package/dist/src/clients/harness.js +4 -2
- package/dist/src/clients/jira.d.ts +2 -0
- package/dist/src/clients/jira.js +71 -0
- package/dist/src/clients/polar.d.ts +2 -0
- package/dist/src/clients/polar.js +73 -0
- package/dist/src/clients/resend.js +38 -0
- package/dist/src/clients/run.js +1 -1
- package/dist/src/clients/sentry.js +1 -26
- package/dist/src/clients/smtp.d.ts +2 -0
- package/dist/src/clients/smtp.js +54 -0
- package/dist/src/clients/stripe.js +1 -1
- package/dist/src/clients/volteridentity.js +4 -3
- package/dist/src/clients/x.d.ts +2 -0
- package/dist/src/clients/x.js +64 -0
- package/dist/src/conformance.d.ts +4 -3
- package/dist/src/conformance.js +293 -8
- package/dist/src/consumer.js +5 -1
- package/dist/src/create-pack.d.ts +5 -0
- package/dist/src/create-pack.js +38 -4
- package/dist/src/derive.js +21 -13
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +47 -4
- package/dist/src/grade.d.ts +1 -0
- package/dist/src/grade.js +1 -1
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pack-facts.js +11 -2
- package/dist/src/pack-files.js +2 -1
- package/dist/src/protocol-3.js +40 -6
- package/dist/src/spec-ir-client.js +3 -2
- package/dist/src/spec-ir-commands.d.ts +73 -0
- package/dist/src/spec-ir-commands.js +63 -0
- package/dist/src/spec-ir.d.ts +2 -0
- package/dist/src/spec-ir.js +43 -0
- package/dist/src/tools/judge-brief.d.ts +1 -0
- package/dist/src/tools/judge-brief.js +67 -0
- package/dist/src/tools/life-coverage-walk.d.ts +2 -0
- package/dist/src/tools/life-coverage-walk.js +220 -0
- package/dist/src/tools/life-coverage.d.ts +2 -0
- package/dist/src/tools/life-coverage.js +271 -0
- package/dist/src/tools/packs-root.d.ts +9 -0
- package/dist/src/tools/packs-root.js +17 -0
- package/dist/src/tools/run-tool.d.ts +1 -0
- package/dist/src/tools/run-tool.js +18 -0
- package/dist/src/tools/score-pack.d.ts +2 -0
- package/dist/src/tools/score-pack.js +264 -0
- package/dist/src/tools/vendor-examples.d.ts +2 -0
- package/dist/src/tools/vendor-examples.js +1493 -0
- package/dist/src/types.d.ts +2 -0
- package/dist/src/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +6 -0
- package/dist/src/walk/behavior-journey.js +108 -8
- package/dist/src/walk/journey-kit.d.ts +23 -1
- package/dist/src/walk/journey-kit.js +7 -2
- package/dist/src/walk/rendered-scalar.d.ts +3 -0
- package/dist/src/walk/rendered-scalar.js +23 -0
- package/dist/src/walk/sse-example.js +45 -0
- package/package.json +2 -2
- package/src/assessment.ts +2 -1
- package/src/cli.ts +15 -2
- package/src/clients/harness.ts +4 -2
- package/src/clients/jira.ts +73 -0
- package/src/clients/polar.ts +73 -0
- package/src/clients/resend.ts +32 -0
- package/src/clients/run.ts +2 -2
- package/src/clients/sentry.ts +1 -25
- package/src/clients/smtp.ts +49 -0
- package/src/clients/stripe.ts +1 -1
- package/src/clients/volteridentity.ts +4 -3
- package/src/clients/x.ts +66 -0
- package/src/conformance.ts +226 -10
- package/src/consumer.ts +5 -1
- package/src/create-pack.ts +35 -5
- package/src/derive.ts +21 -14
- package/src/gate.ts +40 -6
- package/src/grade.ts +2 -2
- package/src/pack-facts.ts +10 -3
- package/src/pack-files.ts +2 -1
- package/src/protocol-3.ts +32 -6
- package/src/spec-ir-client.ts +3 -2
- package/src/spec-ir-commands.ts +81 -0
- package/src/spec-ir.ts +31 -0
- package/src/tools/judge-brief.ts +63 -0
- package/src/tools/life-coverage-walk.ts +173 -0
- package/src/tools/life-coverage.ts +237 -0
- package/src/tools/packs-root.ts +21 -0
- package/src/tools/run-tool.ts +10 -0
- package/src/tools/score-pack.ts +244 -0
- package/src/tools/vendor-examples.ts +1250 -0
- package/src/types.ts +2 -0
- package/src/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +65 -4
- package/src/walk/journey-kit.ts +21 -3
- package/src/walk/rendered-scalar.ts +17 -0
- package/src/walk/sse-example.ts +37 -0
package/dist/src/gate.js
CHANGED
|
@@ -42,6 +42,48 @@ export function unitsOf(repo) {
|
|
|
42
42
|
* routes by hand: only the fetch reaches a lane. A directory with no manifest and no lane is no Protocol 3 pack. */
|
|
43
43
|
const ROOT_ENTRIES = new Set(['README.md', 'package.json', 'LICENSE', 'CHANGELOG.md', 'tsconfig.json', 'tsconfig.build.json', 'gate.ts', 'census.json', 'vendor', 'journeys', 'spec', 'generated', 'src', 'node_modules']);
|
|
44
44
|
const ROOT_SRC = new Set(['manifest.ts', 'fetch.ts', 'index.ts', 'cli.ts', 'server.ts']);
|
|
45
|
+
/** The copied root seed is data, never an alternate executable seed runner. */
|
|
46
|
+
function rootSeedFailures(dir) {
|
|
47
|
+
const path = join(dir, 'src', 'semantics', 'seed.ts');
|
|
48
|
+
if (!existsSync(path))
|
|
49
|
+
return [];
|
|
50
|
+
const text = readFileSync(path, 'utf8');
|
|
51
|
+
if (ts.transpileModule(text, { fileName: path, reportDiagnostics: true }).diagnostics?.some(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error))
|
|
52
|
+
return ['handler-contract: root seed has invalid TypeScript syntax'];
|
|
53
|
+
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
|
|
54
|
+
const literal = (node) => {
|
|
55
|
+
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node) || [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind))
|
|
56
|
+
return true;
|
|
57
|
+
if (ts.isPrefixUnaryExpression(node))
|
|
58
|
+
return node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand);
|
|
59
|
+
if (ts.isArrayLiteralExpression(node))
|
|
60
|
+
return node.elements.every(element => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && literal(element));
|
|
61
|
+
return ts.isObjectLiteralExpression(node) && node.properties.every(property => ts.isPropertyAssignment(property)
|
|
62
|
+
&& !ts.isComputedPropertyName(property.name) && literal(property.initializer));
|
|
63
|
+
};
|
|
64
|
+
let imports = 0, seeds = 0;
|
|
65
|
+
for (const statement of source.statements) {
|
|
66
|
+
if (ts.isImportDeclaration(statement)) {
|
|
67
|
+
const clause = statement.importClause, bindings = clause?.namedBindings;
|
|
68
|
+
if (!clause?.isTypeOnly || clause.name || !bindings || !ts.isNamedImports(bindings)
|
|
69
|
+
|| bindings.elements.length !== 1 || bindings.elements[0].name.text !== 'SeedCall' || bindings.elements[0].propertyName
|
|
70
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== '@volter/world-core')
|
|
71
|
+
return ['handler-contract: root seed imports only the SeedCall type from world-core'];
|
|
72
|
+
imports++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!ts.isVariableStatement(statement) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
76
|
+
|| !(statement.declarationList.flags & ts.NodeFlags.Const) || statement.declarationList.declarations.length !== 1)
|
|
77
|
+
return ['handler-contract: root seed exports only const seed: SeedCall[] literal data'];
|
|
78
|
+
const declaration = statement.declarationList.declarations[0];
|
|
79
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'seed' || !declaration.type || !ts.isArrayTypeNode(declaration.type)
|
|
80
|
+
|| !ts.isTypeReferenceNode(declaration.type.elementType) || declaration.type.elementType.typeName.getText(source) !== 'SeedCall' || declaration.type.elementType.typeArguments?.length
|
|
81
|
+
|| !declaration.initializer || !ts.isArrayLiteralExpression(declaration.initializer) || !literal(declaration.initializer))
|
|
82
|
+
return ['handler-contract: root seed must be literal SeedCall[] data'];
|
|
83
|
+
seeds++;
|
|
84
|
+
}
|
|
85
|
+
return imports === 1 && seeds === 1 ? [] : ['handler-contract: root seed requires one SeedCall type import and one seed export'];
|
|
86
|
+
}
|
|
45
87
|
/** What the repository ignores (a build's dist/, a World's state) is no file of the pack, as protocol-3's layout reads it. */
|
|
46
88
|
function ignored(dir, names) {
|
|
47
89
|
if (!names.length)
|
|
@@ -57,15 +99,16 @@ export function laneRootFailures(dir) {
|
|
|
57
99
|
const skip = ignored(dir, stray);
|
|
58
100
|
const outside = [
|
|
59
101
|
...stray.filter((n) => !skip.has(n)),
|
|
60
|
-
...(existsSync(join(dir, 'spec')) ? readdirSync(join(dir, 'spec')).filter((n) => !['sources.json', 'SOURCE.md', 'README.md'].includes(n)).map((n) => `spec/${n}`) : []),
|
|
61
|
-
...(existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((n) => !ROOT_SRC.has(n) && !n.endsWith('.d.ts') && n !== 'semantics').map((n) => `src/${n}`) : []),
|
|
102
|
+
...(existsSync(join(dir, 'spec')) ? readdirSync(join(dir, 'spec')).filter((n) => !isPackArtifact(n) && !['sources.json', 'SOURCE.md', 'README.md'].includes(n)).map((n) => `spec/${n}`) : []),
|
|
103
|
+
...(existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((n) => !isPackArtifact(n) && !ROOT_SRC.has(n) && !n.endsWith('.d.ts') && n !== 'semantics').map((n) => `src/${n}`) : []),
|
|
62
104
|
// what the lanes share, and nothing else (architecture, "Pack layout": a vendor whose every API is a lane)
|
|
63
|
-
...(existsSync(join(dir, 'src', 'semantics')) ? readdirSync(join(dir, 'src', 'semantics')).filter((n) => n
|
|
105
|
+
...(existsSync(join(dir, 'src', 'semantics')) ? readdirSync(join(dir, 'src', 'semantics')).filter((n) => !isPackArtifact(n) && !['shared.ts', 'seed.ts'].includes(n)).map((n) => `src/semantics/${n}`) : []),
|
|
64
106
|
];
|
|
65
107
|
const failures = outside.length ? [`layout: ${outside.length} outside the vendor's root (its lanes hold the rest): ${outside.slice(0, 8).join(', ')}${outside.length > 8 ? ` and ${outside.length - 8} more` : ''}`] : [];
|
|
66
108
|
// the vendor's front is declared and the kernel serves it: a manifest, and a fetch that is createVendorFetch over it
|
|
67
109
|
const src = (n) => (existsSync(join(dir, 'src', n)) ? readFileSync(join(dir, 'src', n), 'utf8') : '');
|
|
68
110
|
failures.push(...unpublishedUnitFiles(dir).map((f) => `layout: ${f}`));
|
|
111
|
+
failures.push(...rootSeedFailures(dir));
|
|
69
112
|
if (!src('manifest.ts'))
|
|
70
113
|
failures.push("layout: no src/manifest.ts: a vendor of lanes declares its lanes' routes, discovery and descriptor");
|
|
71
114
|
if (!/\bcreateVendorFetch\(/.test(src('fetch.ts')))
|
|
@@ -109,7 +152,7 @@ export async function p3Gate(units, opts = {}) {
|
|
|
109
152
|
}
|
|
110
153
|
const grades = await grade(unit, { static: true, sections: ['form'], ...opts });
|
|
111
154
|
const form = Object.values(grades).flatMap((g) => g.criteria).filter((c) => !c.pass);
|
|
112
|
-
out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`) });
|
|
155
|
+
out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`), cacheable: Object.values(grades).every(g => g.criteria.every(c => c.cacheable !== false)) });
|
|
113
156
|
}
|
|
114
157
|
return out;
|
|
115
158
|
});
|
package/dist/src/grade.d.ts
CHANGED
package/dist/src/grade.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function grade(dir, opts = {}) {
|
|
|
26
26
|
result = await c.check(unit);
|
|
27
27
|
}
|
|
28
28
|
catch (e) {
|
|
29
|
-
result = { pass: false, reason: `the check threw: ${e.message.slice(0, 160)}` };
|
|
29
|
+
result = { pass: false, cacheable: false, reason: `the check threw: ${e.message.slice(0, 160)}` };
|
|
30
30
|
}
|
|
31
31
|
opts.onCriterion?.(name, c.id, result);
|
|
32
32
|
criteria.push({ id: c.id, section: c.section, asks: c.asks, ...result });
|
package/dist/src/pack-facts.d.ts
CHANGED
|
@@ -76,9 +76,17 @@ export type PackFacts = {
|
|
|
76
76
|
};
|
|
77
77
|
/** The managed-infrastructure kind this pack is the World's backing for (architecture.md, "The descriptor"). */
|
|
78
78
|
managedService?: {
|
|
79
|
-
kind: 'redis' | 'mongodb';
|
|
79
|
+
kind: 'redis' | 'mongodb' | 'clickhouse';
|
|
80
80
|
scheme: string;
|
|
81
81
|
note: string;
|
|
82
|
+
configurationArg?: '--configuration';
|
|
83
|
+
originalInputs?: {
|
|
84
|
+
version: 1;
|
|
85
|
+
manifestArg: '--original-inputs';
|
|
86
|
+
rootArg: '--original-root';
|
|
87
|
+
digestArg: '--original-inputs-sha256';
|
|
88
|
+
paths: string[];
|
|
89
|
+
};
|
|
82
90
|
};
|
|
83
91
|
/** The World's managed database the runtime binds the twin to (see TwinPack.managedDatabase). */
|
|
84
92
|
managedDatabase?: {
|
package/dist/src/pack-facts.js
CHANGED
|
@@ -184,12 +184,21 @@ export async function compilePackFacts(dirs) {
|
|
|
184
184
|
}
|
|
185
185
|
if (pack.managedService !== undefined) {
|
|
186
186
|
const ms = pack.managedService;
|
|
187
|
-
if (ms.kind !== 'redis' && ms.kind !== 'mongodb')
|
|
188
|
-
throw new Error(`pack-facts: ${vendor} declares managedService.kind "${String(ms.kind)}"; the managed-infrastructure kinds a twin backs are redis and
|
|
187
|
+
if (ms.kind !== 'redis' && ms.kind !== 'mongodb' && ms.kind !== 'clickhouse')
|
|
188
|
+
throw new Error(`pack-facts: ${vendor} declares managedService.kind "${String(ms.kind)}"; the managed-infrastructure kinds a twin backs are redis, mongodb and clickhouse`);
|
|
189
189
|
if (typeof ms.scheme !== 'string' || !/^[a-z][a-z0-9+.-]*$/.test(ms.scheme))
|
|
190
190
|
throw new Error(`pack-facts: ${vendor} declares managedService.scheme "${String(ms.scheme)}"; it is the published URL's scheme`);
|
|
191
191
|
if (typeof ms.note !== 'string' || ms.note.trim() === '')
|
|
192
192
|
throw new Error(`pack-facts: ${vendor} declares managedService with no note`);
|
|
193
|
+
if (ms.configurationArg !== undefined && ms.configurationArg !== '--configuration')
|
|
194
|
+
throw new Error(`pack-facts: ${vendor} has an unsupported managed operator-configuration flag`);
|
|
195
|
+
if (ms.originalInputs !== undefined) {
|
|
196
|
+
const input = ms.originalInputs;
|
|
197
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || input.version !== 1 || input.manifestArg !== '--original-inputs' || input.rootArg !== '--original-root' || input.digestArg !== '--original-inputs-sha256'
|
|
198
|
+
|| !Array.isArray(input.paths) || !input.paths.length || new Set(input.paths).size !== input.paths.length
|
|
199
|
+
|| input.paths.some(path => typeof path !== 'string' || !path.startsWith('/') || path === '/' || path.slice(1).split('/').some(part => !part || part === '.' || part === '..' || /[\\\0\r\n]/.test(part))))
|
|
200
|
+
throw new Error(`pack-facts: ${vendor} has an invalid original-input consumer`);
|
|
201
|
+
}
|
|
193
202
|
if (pack.bin === undefined)
|
|
194
203
|
throw new Error(`pack-facts: ${vendor} declares managedService with no bin; the managed infrastructure starts the pack by its bin`);
|
|
195
204
|
const other = Object.entries(packs).find(([, f]) => f.managedService?.kind === ms.kind);
|
package/dist/src/pack-files.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Release outputs and runtime state, relative to the pack directory being listed. */
|
|
2
2
|
export function isPackArtifact(path) {
|
|
3
|
-
return /^(dist|node_modules|\.volter)(\/|$)/.test(path) || path === 'generated/pack-facts.json'
|
|
3
|
+
return /^(dist|node_modules|\.volter)(\/|$)/.test(path) || path === 'generated/pack-facts.json'
|
|
4
|
+
|| path.split('/').includes('.metadata_never_index');
|
|
4
5
|
}
|
package/dist/src/protocol-3.js
CHANGED
|
@@ -101,7 +101,7 @@ function filesUnder(dir, base = dir) {
|
|
|
101
101
|
if (run.status !== 0 || run.error) {
|
|
102
102
|
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 ?? ''))}`);
|
|
103
103
|
}
|
|
104
|
-
files = [...new Set(String(run.stdout).split('\0').filter(
|
|
104
|
+
files = [...new Set(String(run.stdout).split('\0').filter(f => f && !f.split('/').includes('.metadata_never_index')))];
|
|
105
105
|
listing?.set(top, files);
|
|
106
106
|
}
|
|
107
107
|
const under = rel(top, dir);
|
|
@@ -114,6 +114,9 @@ function allFilesUnder(dir, base = dir, excludeArtifacts = false) {
|
|
|
114
114
|
return [];
|
|
115
115
|
const out = [];
|
|
116
116
|
for (const name of readdirSync(dir)) {
|
|
117
|
+
// macOS directory bookkeeping is not a pack source or release output.
|
|
118
|
+
if (name === '.metadata_never_index')
|
|
119
|
+
continue;
|
|
117
120
|
const path = join(dir, name);
|
|
118
121
|
if (excludeArtifacts && isPackArtifact(rel(base, path)))
|
|
119
122
|
continue;
|
|
@@ -304,11 +307,31 @@ const criteria = [
|
|
|
304
307
|
}
|
|
305
308
|
// the pack's own path and the scratch directory are this checkout's, not the pack's: the verdict names neither, so
|
|
306
309
|
// a standing written from one checkout reads the same from any other
|
|
310
|
+
if (run.status === null || run.signal !== null || run.error)
|
|
311
|
+
return { pass: false, cacheable: false,
|
|
312
|
+
reason: `derive-pack interrupted: status=${run.status} signal=${run.signal} error=${run.error?.message ?? ''}` };
|
|
307
313
|
if (run.status !== 0)
|
|
308
|
-
return fail(`derive-pack refused: ${String(run.stderr || run.stdout).split(unit.dir).join('.').split(tmp).join('<out>').trim().split('\n').slice(-2).join(' ').slice(0, 200)}`);
|
|
314
|
+
return { ...fail(`derive-pack refused: ${String(run.stderr || run.stdout).split(unit.dir).join('.').split(tmp).join('<out>').trim().split('\n').slice(-2).join(' ').slice(0, 200)}`), cacheable: run.status === 2 };
|
|
309
315
|
return changed.length ? fail(`derive-pack writes differently: ${changed.slice(0, 5).join(', ')}${changed.length > 5 ? ` and ${changed.length - 5} more` : ''}`) : pass(`${filesUnder(gen).length} files reproduced`);
|
|
310
316
|
},
|
|
311
317
|
},
|
|
318
|
+
{
|
|
319
|
+
id: 'fixed-reproducible', section: 'form',
|
|
320
|
+
asks: 'create-pack regenerates the fixed files (fetch, and a pack\'s index, server and cli) byte for byte',
|
|
321
|
+
async check(unit) {
|
|
322
|
+
// loaded when graded: create-pack reads this module's handler names, so a static import would be a cycle
|
|
323
|
+
const { fixedFiles } = await import("./create-pack.js");
|
|
324
|
+
let want;
|
|
325
|
+
try {
|
|
326
|
+
want = await fixedFiles(unit.dir);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
return fail(`create-pack cannot write the fixed files: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200));
|
|
330
|
+
}
|
|
331
|
+
const differ = Object.entries(want).filter(([rel, text]) => !existsSync(join(unit.dir, rel)) || readFileSync(join(unit.dir, rel), 'utf8') !== text).map(([rel]) => rel);
|
|
332
|
+
return differ.length ? fail(`create-pack --index writes differently: ${differ.join(', ')} (run it, never edit a fixed file)`) : pass(`${Object.keys(want).length} fixed files reproduced`);
|
|
333
|
+
},
|
|
334
|
+
},
|
|
312
335
|
{
|
|
313
336
|
id: 'layout', section: 'form',
|
|
314
337
|
asks: "the unit's files exactly Pack layout's tree: fixed names at src/'s root, semantics/ its fixed files and one per spec family, screens/ one per declared screen, journeys/ its closed set",
|
|
@@ -534,8 +557,8 @@ const criteria = [
|
|
|
534
557
|
// an error message's "from '<name>'", is none)
|
|
535
558
|
let text = readFileSync(join(src, f), 'utf8');
|
|
536
559
|
if (f === 'fetch.ts') {
|
|
537
|
-
// The generated fetch supplies the kernel wire with the pack-local parser
|
|
538
|
-
// Exempt only
|
|
560
|
+
// The generated fetch supplies the kernel wire with the pack-local library: graphql's parser, a stream's BSON.
|
|
561
|
+
// Exempt only those exact generated parts; other static or dynamic imports still count.
|
|
539
562
|
const source = ts.createSourceFile(f, text, ts.ScriptTarget.Latest, true);
|
|
540
563
|
const loaders = [];
|
|
541
564
|
const visit = (node) => {
|
|
@@ -550,6 +573,17 @@ const criteria = [
|
|
|
550
573
|
factory.arguments[0] === graphql.parent)
|
|
551
574
|
loaders.push({ start: node.getStart(source), end: node.end });
|
|
552
575
|
}
|
|
576
|
+
// The generated BSON loader of a stream's pack (architecture, the MongoDB wire): exactly
|
|
577
|
+
// `export const loadBson = (): Promise<unknown> => import('bson');`, BSON supplied as graphql is.
|
|
578
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
579
|
+
node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text === 'bson') {
|
|
580
|
+
const arrow = node.parent, declaration = arrow.parent, list = declaration?.parent, statement = list?.parent;
|
|
581
|
+
if (ts.isArrowFunction(arrow) && arrow.parameters.length === 0 && arrow.body === node &&
|
|
582
|
+
declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.name.text === 'loadBson' &&
|
|
583
|
+
statement && ts.isVariableStatement(statement) && statement.parent === source &&
|
|
584
|
+
statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
|
|
585
|
+
loaders.push({ start: node.getStart(source), end: node.end });
|
|
586
|
+
}
|
|
553
587
|
ts.forEachChild(node, visit);
|
|
554
588
|
};
|
|
555
589
|
visit(source);
|
|
@@ -641,7 +675,7 @@ const criteria = [
|
|
|
641
675
|
manifest = (await import(__rewriteRelativeImportExtension(manifestPath))).manifest;
|
|
642
676
|
}
|
|
643
677
|
catch (e) {
|
|
644
|
-
return fail(`manifest.ts does not load: ${e.message.slice(0, 120)}`);
|
|
678
|
+
return { ...fail(`manifest.ts does not load: ${e.message.slice(0, 120)}`), cacheable: false };
|
|
645
679
|
}
|
|
646
680
|
const resources = Object.entries(manifest?.resources ?? {});
|
|
647
681
|
const ruled = resources.filter(([, r]) => (r.state && Object.keys(r.state).length) || (Array.isArray(r.notState) && r.notState.length));
|
|
@@ -654,7 +688,7 @@ const criteria = [
|
|
|
654
688
|
states = (await import(__rewriteRelativeImportExtension(statesPath))).states;
|
|
655
689
|
}
|
|
656
690
|
catch (e) {
|
|
657
|
-
return fail(`semantics/states.ts does not load: ${e.message.slice(0, 120)}`);
|
|
691
|
+
return { ...fail(`semantics/states.ts does not load: ${e.message.slice(0, 120)}`), cacheable: false };
|
|
658
692
|
}
|
|
659
693
|
if (!states)
|
|
660
694
|
return fail('semantics/states.ts exports no `states`');
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// A vendor that publishes no spec of its API but ships its own client: the API as that client calls it. Tinybird
|
|
2
2
|
// publishes no OpenAPI of its platform API (its reference pages omit endpoints its own CLI calls: /v0/tags,
|
|
3
3
|
// /v0/sql_tables), and its Python client (`tinybird/client.py`, class TinyB, in tinybird-cli on PyPI) makes every
|
|
4
|
-
// request through `_req(url, method=..., data=...)`. Each such call, read from the client's syntax tree
|
|
5
|
-
// `spec/
|
|
4
|
+
// request through `_req(url, method=..., data=...)`. Each such call, read from the client's syntax tree, is vendored as
|
|
5
|
+
// data in `spec/client-ops.json` (the program that read it is not in the pack: a pack keeps no converter in its
|
|
6
|
+
// `spec/`, Creating a pack, step 2): its method, its URL as the client
|
|
6
7
|
// builds it (a label is the Python expression the client interpolates), the query keys it sends (literal, or the keys
|
|
7
8
|
// of the dict it urlencodes) and its body's keys, each typed by the client's own annotation of the value (`null`
|
|
8
9
|
// where the client does not say). A spec patch corrects the table as it corrects a JSON spec (derive-pack): what the
|
|
@@ -35,4 +35,77 @@ type RedisCommandFile = Record<string, {
|
|
|
35
35
|
}>;
|
|
36
36
|
/** The command table of the vendored files (file name → its parsed JSON), in command order. */
|
|
37
37
|
export declare function fromRedisCommands(files: Record<string, RedisCommandFile>, version: string): CommandTableSpec;
|
|
38
|
+
export type FieldCondition = {
|
|
39
|
+
form: 'always' | 'given' | 'wire-version' | 'other';
|
|
40
|
+
text?: string;
|
|
41
|
+
};
|
|
42
|
+
/** A field of a command document: where the client sets it and under what condition. `by` names a helper that sets
|
|
43
|
+
* it; `from` the builder it was read in, where that is not the command's own (a base class, a subclass). */
|
|
44
|
+
export type CommandField = {
|
|
45
|
+
name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
optional?: boolean;
|
|
48
|
+
sentWhen: FieldCondition;
|
|
49
|
+
source: {
|
|
50
|
+
file: string;
|
|
51
|
+
line: number;
|
|
52
|
+
};
|
|
53
|
+
by?: string;
|
|
54
|
+
from?: string;
|
|
55
|
+
};
|
|
56
|
+
/** A command that is a document. `statements`: for a field that is a list of statements, the fields of one. */
|
|
57
|
+
export type DocumentCommandDef = {
|
|
58
|
+
id: string;
|
|
59
|
+
fields: CommandField[];
|
|
60
|
+
statements?: Record<string, CommandField[]>;
|
|
61
|
+
};
|
|
62
|
+
export type DocumentCommandTableSpec = {
|
|
63
|
+
format: 'command-table';
|
|
64
|
+
language: 'document';
|
|
65
|
+
version: string;
|
|
66
|
+
source: {
|
|
67
|
+
package: string;
|
|
68
|
+
commit: string;
|
|
69
|
+
licence: string;
|
|
70
|
+
};
|
|
71
|
+
commands: DocumentCommandDef[];
|
|
72
|
+
envelope: CommandField[];
|
|
73
|
+
};
|
|
74
|
+
type BuilderField = CommandField & {
|
|
75
|
+
typeFrom?: string;
|
|
76
|
+
value?: string;
|
|
77
|
+
also?: unknown[];
|
|
78
|
+
spreadUnder?: string;
|
|
79
|
+
};
|
|
80
|
+
type Builder = {
|
|
81
|
+
builder: string;
|
|
82
|
+
kind: 'command' | 'statement';
|
|
83
|
+
file: string;
|
|
84
|
+
line: number;
|
|
85
|
+
command: string | null;
|
|
86
|
+
class?: string;
|
|
87
|
+
extends?: string;
|
|
88
|
+
addsTo?: string;
|
|
89
|
+
fields: BuilderField[];
|
|
90
|
+
};
|
|
91
|
+
export type DriverCommandBuilders = {
|
|
92
|
+
format: 'driver-command-builders';
|
|
93
|
+
driver: {
|
|
94
|
+
package: string;
|
|
95
|
+
version: string;
|
|
96
|
+
commit: string;
|
|
97
|
+
licence: string;
|
|
98
|
+
};
|
|
99
|
+
builders: Builder[];
|
|
100
|
+
envelope: Array<{
|
|
101
|
+
layer: string;
|
|
102
|
+
file: string;
|
|
103
|
+
line: number;
|
|
104
|
+
fields: BuilderField[];
|
|
105
|
+
}>;
|
|
106
|
+
};
|
|
107
|
+
/** The command table of a client's command builders: each command with its own fields, the fields its subclasses
|
|
108
|
+
* add, the fields a base class adds to the commands that descend from it, and the fields of its statements. A builder this cannot place is
|
|
109
|
+
* an error naming it; a table with a silent hole is never returned. */
|
|
110
|
+
export declare function fromDriverCommandBuilders(doc: DriverCommandBuilders): DocumentCommandTableSpec;
|
|
38
111
|
export {};
|
|
@@ -36,3 +36,66 @@ export function fromRedisCommands(files, version) {
|
|
|
36
36
|
commands.sort((a, b) => a.id.localeCompare(b.id));
|
|
37
37
|
return { format: 'command-table', version, commands };
|
|
38
38
|
}
|
|
39
|
+
const fieldOf = (f, from) => ({
|
|
40
|
+
name: f.name, type: f.type, ...(f.optional ? { optional: true } : {}), sentWhen: f.sentWhen, source: f.source,
|
|
41
|
+
...(f.by ? { by: f.by } : {}), ...(from ? { from } : {}),
|
|
42
|
+
});
|
|
43
|
+
/** The command table of a client's command builders: each command with its own fields, the fields its subclasses
|
|
44
|
+
* add, the fields a base class adds to the commands that descend from it, and the fields of its statements. A builder this cannot place is
|
|
45
|
+
* an error naming it; a table with a silent hole is never returned. */
|
|
46
|
+
export function fromDriverCommandBuilders(doc) {
|
|
47
|
+
const named = doc.builders.filter((b) => b.kind === 'command' && b.command !== null);
|
|
48
|
+
const classOf = new Map(named.filter((b) => b.class).map((b) => [b.class, b.command]));
|
|
49
|
+
const commands = new Map();
|
|
50
|
+
const add = (id, fields, from) => {
|
|
51
|
+
const held = commands.get(id) ?? { id, fields: [] };
|
|
52
|
+
for (const f of fields)
|
|
53
|
+
if (!held.fields.some((h) => h.name === f.name))
|
|
54
|
+
held.fields.push(fieldOf(f, from));
|
|
55
|
+
commands.set(id, held);
|
|
56
|
+
};
|
|
57
|
+
for (const b of named)
|
|
58
|
+
add(b.command, b.fields);
|
|
59
|
+
// what a subclass adds to its parent's document, and what the base of every command adds to all of them
|
|
60
|
+
const everyCommand = [];
|
|
61
|
+
for (const b of doc.builders.filter((b) => b.kind === 'command' && b.command === null)) {
|
|
62
|
+
if (b.addsTo === undefined)
|
|
63
|
+
throw new Error(`command-builders: ${b.builder} (${b.file}:${b.line}) names no command and adds to none`);
|
|
64
|
+
const parent = classOf.get(b.addsTo);
|
|
65
|
+
if (parent !== undefined)
|
|
66
|
+
add(parent, b.fields, b.builder);
|
|
67
|
+
else if (b.class === b.addsTo)
|
|
68
|
+
everyCommand.push(b);
|
|
69
|
+
else
|
|
70
|
+
throw new Error(`command-builders: ${b.builder} (${b.file}:${b.line}) adds to ${b.addsTo}, which builds no named command`);
|
|
71
|
+
}
|
|
72
|
+
// the base's fields go to the commands whose class descends from it, and to no other (a getMore is not one)
|
|
73
|
+
const parentOf = new Map(doc.builders.filter((b) => b.class && b.extends).map((b) => [b.class, b.extends]));
|
|
74
|
+
const descends = (cls, base) => { for (let at = cls, hops = 0; at !== undefined && hops < 16; at = parentOf.get(at), hops += 1)
|
|
75
|
+
if (at === base)
|
|
76
|
+
return true; return false; };
|
|
77
|
+
for (const b of everyCommand)
|
|
78
|
+
for (const n of named)
|
|
79
|
+
if (descends(n.extends, b.class))
|
|
80
|
+
add(n.command, b.fields, b.builder);
|
|
81
|
+
// a statement builder's fields belong to the command of its file that carries the statement list
|
|
82
|
+
for (const b of doc.builders.filter((b) => b.kind === 'statement')) {
|
|
83
|
+
const owner = named.find((n) => n.file === b.file && n.fields.some((f) => f.value === 'this.statements'));
|
|
84
|
+
const list = owner?.fields.find((f) => f.value === 'this.statements');
|
|
85
|
+
if (!owner || !list)
|
|
86
|
+
throw new Error(`command-builders: ${b.builder} (${b.file}:${b.line}) builds a statement and no command of its file carries a statement list`);
|
|
87
|
+
(commands.get(owner.command).statements ??= {})[list.name] = b.fields.map((f) => fieldOf(f));
|
|
88
|
+
}
|
|
89
|
+
const envelope = [];
|
|
90
|
+
for (const layer of doc.envelope)
|
|
91
|
+
for (const f of layer.fields)
|
|
92
|
+
if (!envelope.some((h) => h.name === f.name))
|
|
93
|
+
envelope.push(fieldOf(f, layer.layer));
|
|
94
|
+
if (commands.size === 0)
|
|
95
|
+
throw new Error('command-builders: no command was read');
|
|
96
|
+
return {
|
|
97
|
+
format: 'command-table', language: 'document', version: doc.driver.version,
|
|
98
|
+
source: { package: doc.driver.package, commit: doc.driver.commit, licence: doc.driver.licence },
|
|
99
|
+
commands: [...commands.values()].sort((a, b) => a.id.localeCompare(b.id)), envelope,
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/src/spec-ir.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type IrOperation = {
|
|
|
37
37
|
bodyEncoding: 'json' | 'form' | 'multipart' | 'xml' | 'none';
|
|
38
38
|
/** A form-encoded operation's number and boolean fields, by bracket path (formScalarsOf). */
|
|
39
39
|
scalars?: Record<string, 'integer' | 'number' | 'boolean'>;
|
|
40
|
+
/** A form-encoded operation's string fields that take only listed values, by bracket path (formEnumsOf). */
|
|
41
|
+
enums?: Record<string, string[]>;
|
|
40
42
|
successStatus: number;
|
|
41
43
|
/** Binary media types declared by each response status, independent of the twin’s response headers. */
|
|
42
44
|
responseMedia?: Record<string, string[]>;
|
package/dist/src/spec-ir.js
CHANGED
|
@@ -295,6 +295,47 @@ function formScalarsOf(doc, op, pathItem, encoding) {
|
|
|
295
295
|
visit(p, [name], 0);
|
|
296
296
|
return Object.keys(out).length ? Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b))) : undefined;
|
|
297
297
|
}
|
|
298
|
+
/** A form-encoded operation's string fields whose schema lists the values they take (`status`: active, inactive,
|
|
299
|
+
* canceled), by the bracket path formScalarsOf uses; a field another alternative leaves free (a string without an enum,
|
|
300
|
+
* or a number) takes any value and is not listed. */
|
|
301
|
+
function formEnumsOf(doc, op, pathItem, encoding) {
|
|
302
|
+
if (encoding !== 'form')
|
|
303
|
+
return undefined;
|
|
304
|
+
const out = {};
|
|
305
|
+
const visit = (schema, at, depth) => {
|
|
306
|
+
const s = deref(doc, schema);
|
|
307
|
+
if (!s || typeof s !== 'object' || depth > 12)
|
|
308
|
+
return;
|
|
309
|
+
const alts = [...(Array.isArray(s.anyOf) ? s.anyOf : []), ...(Array.isArray(s.oneOf) ? s.oneOf : [])].map((a) => deref(doc, a));
|
|
310
|
+
if (alts.length) {
|
|
311
|
+
const free = alts.some((a) => (a?.type === 'string' && !Array.isArray(a.enum)) || a?.type === 'integer' || a?.type === 'number' || a?.type === 'boolean');
|
|
312
|
+
const listed = alts.filter((a) => a?.type === 'string' && Array.isArray(a.enum)).flatMap((a) => a.enum.map(String));
|
|
313
|
+
if (!free && listed.length && at.length)
|
|
314
|
+
out[at.join('.')] = [...new Set(listed)];
|
|
315
|
+
for (const a of alts)
|
|
316
|
+
if (a?.type === 'object' || a?.type === 'array' || a?.properties)
|
|
317
|
+
visit(a, at, depth + 1);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (s.type === 'string' && Array.isArray(s.enum) && at.length) {
|
|
321
|
+
out[at.join('.')] = s.enum.map(String);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (s.type === 'array') {
|
|
325
|
+
visit(s.items, [...at, '[]'], depth + 1);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const [name, p] of Object.entries(propsOf(doc, s)))
|
|
329
|
+
visit(p, [...at, name], depth + 1);
|
|
330
|
+
};
|
|
331
|
+
for (const p of [...(pathItem.parameters ?? []), ...(op.parameters ?? [])].map((q) => deref(doc, q)))
|
|
332
|
+
if (p?.in === 'query')
|
|
333
|
+
visit(p.schema, [String(p.name)], 0);
|
|
334
|
+
const body = deref(doc, deref(doc, op.requestBody)?.content?.['application/x-www-form-urlencoded']?.schema);
|
|
335
|
+
for (const [name, p] of Object.entries(propsOf(doc, body)))
|
|
336
|
+
visit(p, [name], 0);
|
|
337
|
+
return Object.keys(out).length ? Object.fromEntries(Object.entries(out).sort(([x], [y]) => x.localeCompare(y))) : undefined;
|
|
338
|
+
}
|
|
298
339
|
/** The class of one operation, from its method, its path and what it answers. An operation this cannot place is `action`, never CRUD. */
|
|
299
340
|
/** A path and its discriminator as one key: `/responses/{id}` and `/responses/{id}?beta=true` are different reads. */
|
|
300
341
|
// a null value (the key present with any value) keys as the bare name
|
|
@@ -445,6 +486,7 @@ export function fromOpenAPI(spec) {
|
|
|
445
486
|
const credentials = [...new Set(security.flatMap((req) => { const names = Object.keys(req ?? {}); return names.length ? names : ['anonymous']; }))].sort();
|
|
446
487
|
const body = bodyOf(doc, op);
|
|
447
488
|
const scalars = formScalarsOf(doc, op, item, body.bodyEncoding);
|
|
489
|
+
const enums = formEnumsOf(doc, op, item, body.bodyEncoding);
|
|
448
490
|
const ownServer = String((op.servers ?? item.servers)?.[0]?.url ?? '');
|
|
449
491
|
const ownBase = /^https?:\/\//.test(ownServer) ? new URL(ownServer).pathname.replace(/\/+$/, '') : undefined;
|
|
450
492
|
raw.push({
|
|
@@ -456,6 +498,7 @@ export function fromOpenAPI(spec) {
|
|
|
456
498
|
...(ownBase !== undefined && ownBase !== docBase ? { basePath: ownBase } : {}),
|
|
457
499
|
...body,
|
|
458
500
|
...(scalars ? { scalars } : {}),
|
|
501
|
+
...(enums ? { enums } : {}),
|
|
459
502
|
successStatus: success.status,
|
|
460
503
|
...(Object.keys(responseMedia).length ? { responseMedia, ...(Object.keys(defaultResponseMedia).length ? { defaultResponseMedia } : {}) } : {}),
|
|
461
504
|
...(success.empty ? { emptySuccess: true } : {}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// A JUDGE'S BRIEF (pack-done's (f); architecture, "What a judge rejects"): what a judge reads and writes for one vendor's
|
|
2
|
+
// outline, life and published examples, printed for a judge in another session (`codex exec`, `claude -p`, any agent
|
|
3
|
+
// runtime). The verdict names its judge and the authors; the grade refuses one whose judge is an author
|
|
4
|
+
// (packages/twin-standard/src/verdict.ts). One judging per pack: the verdict stands through later changes.
|
|
5
|
+
//
|
|
6
|
+
// twin-standard judge-brief <vendor> --authors <author name>[,<agent>...] (from the pack repository's root; twin-world's
|
|
7
|
+
// bun scripts/judge-brief.ts runs the same)
|
|
8
|
+
import { attributionName } from "../verdict.js";
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { packsRoot } from "./packs-root.js";
|
|
13
|
+
const PACKS = packsRoot();
|
|
14
|
+
/** The architecture a judge's list is read from: the twin-world checkout this standard is in, else its published copy. */
|
|
15
|
+
const ARCHITECTURE = join(import.meta.dir, '..', '..', '..', '..', 'docs', 'contributing', 'architecture.md');
|
|
16
|
+
const PUBLISHED_ARCHITECTURE = 'https://world-docs.volter.ai/docs/contributing/architecture.md';
|
|
17
|
+
const vendor = process.argv[2];
|
|
18
|
+
const authorsAt = process.argv.indexOf('--authors');
|
|
19
|
+
const authors = authorsAt > 0 ? (process.argv[authorsAt + 1] ?? '').split(',').map((a) => a.trim()).filter(Boolean).map((name) => attributionName(name)) : [];
|
|
20
|
+
if (!vendor || vendor.startsWith('-') || !authors.length) {
|
|
21
|
+
console.error('usage: twin-standard judge-brief <vendor> --authors <author name>[,<agent>...] (from the pack repository\'s root)');
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const journeys = join(PACKS, vendor, 'journeys');
|
|
25
|
+
if (!existsSync(journeys)) {
|
|
26
|
+
console.error(`judge-brief: ${vendor} has no journeys/ in ${PACKS}`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
// the list of what a judge rejects, as architecture holds it
|
|
30
|
+
const architecture = existsSync(ARCHITECTURE) ? readFileSync(ARCHITECTURE, 'utf8') : await fetch(PUBLISHED_ARCHITECTURE).then((r) => (r.ok ? r.text() : ''), () => '');
|
|
31
|
+
const listAt = architecture.indexOf('#### What a judge rejects\n');
|
|
32
|
+
if (listAt < 0) {
|
|
33
|
+
console.error(`judge-brief: the architecture (${existsSync(ARCHITECTURE) ? ARCHITECTURE : PUBLISHED_ARCHITECTURE}) has no "#### What a judge rejects" section`);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
const listFrom = architecture.indexOf('\n- ', listAt);
|
|
37
|
+
const listTo = architecture.indexOf('\n\n', listFrom + 1);
|
|
38
|
+
const rejects = architecture.slice(listFrom + 1, listTo < 0 ? undefined : listTo).trim();
|
|
39
|
+
const sha = (file) => createHash('sha256').update(readFileSync(file)).digest('hex');
|
|
40
|
+
const judged = [
|
|
41
|
+
{ file: 'customer-life.outline.md', verdict: 'customer-life.outline.verdict.json', what: "the life's outline: the customer's story in acts" },
|
|
42
|
+
{ file: 'customer-life.json', verdict: 'customer-life.verdict.json', what: 'the life: every step an application or person takes, its expectations' },
|
|
43
|
+
{ file: 'vendor-examples.json', verdict: 'vendor-examples.verdict.json', what: "the published examples: the vendor's own documented requests and answers, replayed" },
|
|
44
|
+
].filter((j) => existsSync(join(journeys, j.file)));
|
|
45
|
+
console.log(`You are the judge of the ${vendor} twin's journeys. You did not write them, and you judge them in a session other than
|
|
46
|
+
the run that wrote them. Judge them against the vendor and against the list below; do not edit any judged file, and run no
|
|
47
|
+
test. Read what you need: the pack's spec (${vendor}/spec/, SOURCE.md naming every page read), its demand
|
|
48
|
+
(${vendor}/journeys/demand.json: what the applications call), its decision table (journeys/decisions.json), and the
|
|
49
|
+
vendor's own documentation the spec cites.
|
|
50
|
+
|
|
51
|
+
What you judge (in ${vendor}/journeys/), each with the sha256 your verdict names:
|
|
52
|
+
${judged.map((j) => `- ${j.file} (${j.what}): sha256 ${sha(join(journeys, j.file))}`).join('\n')}
|
|
53
|
+
|
|
54
|
+
What a judge rejects (docs/contributing/architecture.md, "What a judge rejects"):
|
|
55
|
+
${rejects}
|
|
56
|
+
|
|
57
|
+
For each file write its verdict beside it, as JSON:
|
|
58
|
+
${judged.map((j) => `- ${j.verdict}`).join('\n')}
|
|
59
|
+
{
|
|
60
|
+
"judge": { "agent": "<what you are: your runtime and model>", "name": "<your author name or reviewer role>" },
|
|
61
|
+
"authors": ${JSON.stringify(authors)},
|
|
62
|
+
"sha256": "<the judged file's sha256 above>",
|
|
63
|
+
"judgedAt": "<today, YYYY-MM-DD>",
|
|
64
|
+
"plausible": <true only when no ruling stands>,
|
|
65
|
+
"rulings": [ { "id": "<short name>", "steps": [<step indexes, or acts>], "barItem": "<the list's item>", "reason": "<what is wrong, citing the step and the vendor's page>", "correction": "<what the author changes>" } ]
|
|
66
|
+
}
|
|
67
|
+
A correction naming a wait gives it as a life writes one: one number and one unit (\`915m\`, \`3d\`). A verdict with any ruling is not plausible. The authors apply every ruling; this is the one judging, and the verdict stands through their changes.`);
|