@volter/twin-standard 1.0.148 → 1.0.149
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/assessment.d.ts +1 -0
- package/dist/src/conformance.d.ts +0 -2
- package/dist/src/conformance.js +37 -2
- package/dist/src/create-pack.js +7 -1
- 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 +10 -4
- 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/types.d.ts +2 -0
- package/dist/src/walk/behavior-journey.js +100 -2
- package/dist/src/walk/journey-kit.d.ts +21 -1
- package/dist/src/walk/journey-kit.js +1 -0
- package/package.json +2 -2
- package/src/conformance.ts +34 -2
- package/src/create-pack.ts +8 -2
- 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 +8 -4
- package/src/spec-ir-client.ts +3 -2
- package/src/spec-ir-commands.ts +81 -0
- package/src/types.ts +2 -0
- package/src/walk/behavior-journey.ts +59 -2
- package/src/walk/journey-kit.ts +14 -1
package/dist/src/assessment.d.ts
CHANGED
|
@@ -11,8 +11,6 @@ export type ConformanceResult = {
|
|
|
11
11
|
export declare const UNIT_FILES: readonly ["src/manifest.ts", "spec/SOURCE.md", "journeys/customer-life.json", "journeys/decisions.json"];
|
|
12
12
|
/** The published package must carry every conformance input its units hold, including lanes. */
|
|
13
13
|
export declare function unpublishedUnitFiles(unit: string): string[];
|
|
14
|
-
/** Each of UNIT_FILES a unit lacks where conformance reads it: the manifest of every unit, the life at its vendor's root
|
|
15
|
-
* (lifeServed), the spec's record and the decision table of a unit with a spec (decided()). */
|
|
16
14
|
export declare function unitFilesMissing(unit: string): string[];
|
|
17
15
|
type Owners = Record<string, string>;
|
|
18
16
|
type Decision = {
|
package/dist/src/conformance.js
CHANGED
|
@@ -56,6 +56,41 @@ export function unpublishedUnitFiles(unit) {
|
|
|
56
56
|
}
|
|
57
57
|
/** Each of UNIT_FILES a unit lacks where conformance reads it: the manifest of every unit, the life at its vendor's root
|
|
58
58
|
* (lifeServed), the spec's record and the decision table of a unit with a spec (decided()). */
|
|
59
|
+
const trackedJourneySources = new Map();
|
|
60
|
+
/** Table ownership follows one index snapshot per checkout/run. */
|
|
61
|
+
function hasJourneySource(unit) {
|
|
62
|
+
const dir = join(unit, 'journeys');
|
|
63
|
+
let checkout = resolve(unit);
|
|
64
|
+
while (!existsSync(join(checkout, '.git'))) {
|
|
65
|
+
const parent = dirname(checkout);
|
|
66
|
+
if (parent === checkout) {
|
|
67
|
+
// An unpacked published package has no Git index; its delivered files
|
|
68
|
+
// are its source. Markers and empty directories are not table inputs.
|
|
69
|
+
if (!existsSync(dir))
|
|
70
|
+
return false;
|
|
71
|
+
const holdsFile = (at) => readdirSync(at, { withFileTypes: true }).some(entry => entry.name !== '.metadata_never_index'
|
|
72
|
+
&& (entry.isFile() || (entry.isDirectory() && holdsFile(join(at, entry.name)))));
|
|
73
|
+
return holdsFile(dir);
|
|
74
|
+
}
|
|
75
|
+
checkout = parent;
|
|
76
|
+
}
|
|
77
|
+
const env = { ...process.env };
|
|
78
|
+
for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_PREFIX', 'GIT_INDEX_FILE'])
|
|
79
|
+
delete env[key];
|
|
80
|
+
if (process.env.VOLTER_PACK_HOOK_INDEX)
|
|
81
|
+
env.GIT_INDEX_FILE = process.env.VOLTER_PACK_HOOK_INDEX;
|
|
82
|
+
const key = `${checkout}\0${env.GIT_INDEX_FILE ?? ''}`;
|
|
83
|
+
let source = trackedJourneySources.get(key);
|
|
84
|
+
if (!source) {
|
|
85
|
+
const files = spawnSync('git', ['-C', checkout, 'ls-files', '-z', '--cached', '--', ':(glob)**/journeys/**'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024 });
|
|
86
|
+
if (files.status !== 0 || files.error)
|
|
87
|
+
throw new Error(`cannot read tracked journey source: ${String(files.stderr || files.error?.message)}`);
|
|
88
|
+
source = files.stdout.split('\0').filter(path => path && !path.split('/').includes('.metadata_never_index'));
|
|
89
|
+
trackedJourneySources.set(key, source);
|
|
90
|
+
}
|
|
91
|
+
const prefix = relative(checkout, dir).replaceAll('\\', '/') + '/';
|
|
92
|
+
return source.some(path => path.startsWith(prefix));
|
|
93
|
+
}
|
|
59
94
|
export function unitFilesMissing(unit) {
|
|
60
95
|
const vendor = existsSync(join(unit, 'package.json')) ? unit : dirname(unit);
|
|
61
96
|
const spec = hasOwnSurface(unit);
|
|
@@ -63,7 +98,7 @@ export function unitFilesMissing(unit) {
|
|
|
63
98
|
'src/manifest.ts': unit,
|
|
64
99
|
'spec/SOURCE.md': spec ? unit : undefined,
|
|
65
100
|
'journeys/customer-life.json': vendor,
|
|
66
|
-
'journeys/decisions.json': spec ? (
|
|
101
|
+
'journeys/decisions.json': spec ? (hasJourneySource(unit) ? unit : vendor) : undefined,
|
|
67
102
|
};
|
|
68
103
|
return UNIT_FILES.filter((f) => at[f] !== undefined && !existsSync(join(at[f], f))).map((f) => `${relative(unit, join(at[f], f))} is missing (conformance reads it)`);
|
|
69
104
|
}
|
|
@@ -119,7 +154,7 @@ async function decided(pack) {
|
|
|
119
154
|
for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
|
|
120
155
|
const name = nameOf(pack, unit);
|
|
121
156
|
const key = unit === pack ? '' : basename(unit);
|
|
122
|
-
const journeys =
|
|
157
|
+
const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
|
|
123
158
|
const owners = byUnit ? byUnit[key] ?? {} : (await makeOf(unit))?.().owners?.();
|
|
124
159
|
if (!owners) {
|
|
125
160
|
out.push(`${name}: its fetch (${fetchName(unit)}) names no owners() to check the decision table against`);
|
package/dist/src/create-pack.js
CHANGED
|
@@ -617,7 +617,13 @@ export async function createPack(args) {
|
|
|
617
617
|
// a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form,
|
|
618
618
|
// the one form a unit takes after --init (a server.ts of a wire of the pack's own, SMTP's, is never written over)
|
|
619
619
|
if (!lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'))) {
|
|
620
|
-
|
|
620
|
+
const wire = existsSync(join(dir, 'src', 'stream.ts'));
|
|
621
|
+
// a raw-tcp pack exporting one create<Name>TwinStream is taken to serve that stream on its main port, and is admitted
|
|
622
|
+
// by its TCP answer alone (world-runtime host.ts servesByteStream); this one serves gRPC there, its stream on a side port
|
|
623
|
+
if (wire && manifest?.descriptor?.transport === 'raw-tcp') {
|
|
624
|
+
throw new Error(`create-pack: ${vendor} serves gRPC on its main port and its own wire (./stream.ts) on a side port, so its descriptor's transport is not raw-tcp: raw-tcp with a stream export says the main port is that stream (world-runtime host.ts servesByteStream).`);
|
|
625
|
+
}
|
|
626
|
+
writeFileSync(join(dir, 'src', 'server.ts'), serverFile(vendor, true, wire));
|
|
621
627
|
console.log(`create-pack: wrote ${join(dir, 'src', 'server.ts')}`);
|
|
622
628
|
}
|
|
623
629
|
else if (manifest?.live?.length) {
|
package/dist/src/derive.js
CHANGED
|
@@ -21,7 +21,7 @@ import { gunzipSync } from 'node:zlib';
|
|
|
21
21
|
import { CRUD_CLASSES, fromOpenAPI, fromSmithy, fromSwagger2, slug } from "./spec-ir.js";
|
|
22
22
|
import { fromCommandReplies } from "./spec-ir-lines.js";
|
|
23
23
|
import { fromProto, protoSchema, readProtoFiles } from "./spec-ir-proto.js";
|
|
24
|
-
import { fromRedisCommands } from "./spec-ir-commands.js";
|
|
24
|
+
import { fromDriverCommandBuilders, fromRedisCommands } from "./spec-ir-commands.js";
|
|
25
25
|
import { fromClientCalls } from "./spec-ir-client.js";
|
|
26
26
|
import { discoveryMethods, fromDiscovery, isDiscovery } from "./spec-ir-discovery.js";
|
|
27
27
|
import { handlerCitations, quotedPage, sourceHeld, urlsIn } from "./check-sources.js";
|
|
@@ -34,22 +34,28 @@ if (!vendor) {
|
|
|
34
34
|
console.error('usage: twin-standard derive <pack dir> [--out <dir>]');
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
|
+
// A source-declared refusal is deterministic over the pinned bytes. Unexpected
|
|
38
|
+
// loader, filesystem and process failures keep status 1 and must be retried.
|
|
39
|
+
const SOURCE_REFUSAL = 2;
|
|
37
40
|
const PKG = unitAt(vendor).dir;
|
|
38
41
|
// an OpenAPI document, an AWS Smithy JSON model (smithy.json), the RFC a line protocol is (rfcNNNN.txt), or the
|
|
39
42
|
// Protocol Buffers files an RPC service is published as (spec/proto/, every file the service imports), or the calls a
|
|
40
43
|
// vendor's own client makes where the vendor publishes no spec (client-ops.json, spec-ir-client.ts), or one API's several
|
|
41
44
|
// OpenAPI documents (spec/openapi/, read as their union: spec-documents.ts), or a Google API Discovery document
|
|
42
|
-
// (discovery.json, spec-ir-discovery.ts)
|
|
45
|
+
// (discovery.json, spec-ir-discovery.ts), or the command documents a vendor's own client builds where the vendor
|
|
46
|
+
// publishes no table a pack may derive from (command-builders.json, spec-ir-commands.ts)
|
|
43
47
|
const rfc = existsSync(join(PKG, 'spec')) ? readdirSync(join(PKG, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
|
|
44
|
-
const specPath = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'smithy.json.gz', 'smithy.json', 'discovery.json.gz', 'discovery.json', ...(rfc ? [rfc] : []), 'openapi', 'proto', 'commands', 'client-ops.json'].map((f) => join(PKG, 'spec', f)).find((p) => existsSync(p));
|
|
48
|
+
const specPath = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'smithy.json.gz', 'smithy.json', 'discovery.json.gz', 'discovery.json', ...(rfc ? [rfc] : []), 'openapi', 'proto', 'commands', 'command-builders.json', 'client-ops.json'].map((f) => join(PKG, 'spec', f)).find((p) => existsSync(p));
|
|
45
49
|
if (!specPath) {
|
|
46
|
-
console.error(`derive-pack: no spec at ${join(PKG, 'spec')}/openapi.{json,yaml}[.gz], smithy.json[.gz], rfcNNNN.txt, proto/, commands
|
|
47
|
-
process.exit(
|
|
50
|
+
console.error(`derive-pack: no spec at ${join(PKG, 'spec')}/openapi.{json,yaml}[.gz], smithy.json[.gz], rfcNNNN.txt, proto/, commands/, command-builders.json or client-ops.json`);
|
|
51
|
+
process.exit(SOURCE_REFUSAL);
|
|
48
52
|
}
|
|
49
53
|
const rpcService = basename(specPath) === 'proto';
|
|
50
54
|
const severalDocuments = basename(specPath) === 'openapi';
|
|
51
55
|
// a command language's table, one vendored file per command (Redis's src/commands/*.json), with its version in SOURCE.md
|
|
52
56
|
const commandTable = basename(specPath) === 'commands';
|
|
57
|
+
// a language whose commands are documents: the table a pack's extractor read from the vendor's client
|
|
58
|
+
const commandBuilders = basename(specPath) === 'command-builders.json';
|
|
53
59
|
const protoTree = (dir, at = '') => Object.fromEntries(readdirSync(join(dir, at), { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? Object.entries(protoTree(dir, join(at, e.name))) : e.name.endsWith('.proto') ? [[join(at, e.name), readFileSync(join(dir, at, e.name), 'utf8')]] : [])));
|
|
54
60
|
const bytes = rpcService || commandTable || severalDocuments ? Buffer.from('') : readFileSync(specPath);
|
|
55
61
|
const text = (specPath.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
@@ -164,7 +170,7 @@ if (existsSync(patchPath)) {
|
|
|
164
170
|
for (const p of parseExactJson(readFileSync(patchPath, 'utf8')).patches) {
|
|
165
171
|
if (!p.why?.trim()) {
|
|
166
172
|
console.error(`${vendor}: spec patch ${p.op} ${p.path} gives no why`);
|
|
167
|
-
process.exit(
|
|
173
|
+
process.exit(SOURCE_REFUSAL);
|
|
168
174
|
}
|
|
169
175
|
{
|
|
170
176
|
const s = p.source;
|
|
@@ -183,7 +189,7 @@ if (severalDocuments) {
|
|
|
183
189
|
const { doc: merged, contradictions } = mergeDocuments(doc);
|
|
184
190
|
if (contradictions.length) {
|
|
185
191
|
console.error(`${vendor}: the documents contradict each other; a patch (citing what shows which holds) rules each:\n ${contradictions.join('\n ')}`);
|
|
186
|
-
process.exit(
|
|
192
|
+
process.exit(SOURCE_REFUSAL);
|
|
187
193
|
}
|
|
188
194
|
doc = merged;
|
|
189
195
|
}
|
|
@@ -193,7 +199,9 @@ const out = outAt >= 0 && process.argv[outAt + 1] ? process.argv[outAt + 1] : jo
|
|
|
193
199
|
mkdirSync(out, { recursive: true });
|
|
194
200
|
// A line protocol's surface is its commands and the replies each may answer; its manifest's transitions
|
|
195
201
|
// name commands and cite the RFC.
|
|
196
|
-
if (
|
|
202
|
+
if (commandBuilders)
|
|
203
|
+
doc = fromDriverCommandBuilders(doc);
|
|
204
|
+
if (lineProtocol || commandTable || commandBuilders) {
|
|
197
205
|
const spec = doc;
|
|
198
206
|
writeFileSync(join(out, 'surface.gen.json'), `${JSON.stringify({ generatedBy: 'scripts/derive-pack.ts', spec: basename(specPath), ...spec })}\n`);
|
|
199
207
|
const manifestPath = join(PKG, 'src', 'manifest.ts');
|
|
@@ -213,7 +221,7 @@ if (lineProtocol || commandTable) {
|
|
|
213
221
|
}
|
|
214
222
|
if (evidence.length) {
|
|
215
223
|
console.error(`${vendor}: ${evidence.length} citation(s) do not hold:\n ${[...new Set(evidence)].join('\n ')}`);
|
|
216
|
-
process.exit(
|
|
224
|
+
process.exit(SOURCE_REFUSAL);
|
|
217
225
|
}
|
|
218
226
|
console.log(`${vendor}: spec ${spec.version}, ${spec.commands.length} commands${'anyCommand' in spec ? `; any command may answer ${spec.anyCommand.join(', ')}` : ''}`);
|
|
219
227
|
process.exit(0);
|
|
@@ -322,7 +330,7 @@ if (existsSync(manifestPath)) {
|
|
|
322
330
|
}
|
|
323
331
|
if (evidence.length) {
|
|
324
332
|
console.error(`${vendor}: ${evidence.length} citation(s) do not hold:\n ${[...new Set(evidence)].join('\n ')}`);
|
|
325
|
-
process.exit(
|
|
333
|
+
process.exit(SOURCE_REFUSAL);
|
|
326
334
|
}
|
|
327
335
|
// every state candidate of a resource the pack models is ruled on: a state field, or named not one
|
|
328
336
|
const unruled = ir.resources.flatMap((r) => {
|
|
@@ -331,7 +339,7 @@ if (existsSync(manifestPath)) {
|
|
|
331
339
|
});
|
|
332
340
|
if (unruled.length) {
|
|
333
341
|
console.error(`${vendor}: the manifest does not rule on ${unruled.length} state candidate(s): ${unruled.join(', ')} (declare each a state field or list it in notState)`);
|
|
334
|
-
process.exit(
|
|
342
|
+
process.exit(SOURCE_REFUSAL);
|
|
335
343
|
}
|
|
336
344
|
// a resource's refresh scope ("The real-system adapters", What a pack declares) names the list that enumerates it or
|
|
337
345
|
// its retrieve, or says why the vendor offers no read-back; whether every stored resource declares one is
|
|
@@ -421,14 +429,14 @@ if (existsSync(manifestPath)) {
|
|
|
421
429
|
});
|
|
422
430
|
if (scopes.length) {
|
|
423
431
|
console.error(`${vendor}: ${scopes.length} refresh scope(s) do not hold:\n ${scopes.join('\n ')}`);
|
|
424
|
-
process.exit(
|
|
432
|
+
process.exit(SOURCE_REFUSAL);
|
|
425
433
|
}
|
|
426
434
|
// the architecture's mechanical rules over the pack's own code (./p3-rules.ts): a line breaking one records why, or
|
|
427
435
|
// the build stops
|
|
428
436
|
const hits = p3RuleHits(PKG);
|
|
429
437
|
if (hits.length) {
|
|
430
438
|
console.error(`${vendor}: ${hits.length} line(s) break a Protocol 3 rule (fix it, or record why on the line: // p3: <reason>):\n ${hits.map((h) => `${h.file}:${h.line}: ${h.rule}\n ${h.text}`).join('\n ')}`);
|
|
431
|
-
process.exit(
|
|
439
|
+
process.exit(SOURCE_REFUSAL);
|
|
432
440
|
}
|
|
433
441
|
}
|
|
434
442
|
const counts = new Map();
|
package/dist/src/gate.d.ts
CHANGED
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,8 +307,11 @@ 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
|
},
|
|
@@ -641,7 +647,7 @@ const criteria = [
|
|
|
641
647
|
manifest = (await import(__rewriteRelativeImportExtension(manifestPath))).manifest;
|
|
642
648
|
}
|
|
643
649
|
catch (e) {
|
|
644
|
-
return fail(`manifest.ts does not load: ${e.message.slice(0, 120)}`);
|
|
650
|
+
return { ...fail(`manifest.ts does not load: ${e.message.slice(0, 120)}`), cacheable: false };
|
|
645
651
|
}
|
|
646
652
|
const resources = Object.entries(manifest?.resources ?? {});
|
|
647
653
|
const ruled = resources.filter(([, r]) => (r.state && Object.keys(r.state).length) || (Array.isArray(r.notState) && r.notState.length));
|
|
@@ -654,7 +660,7 @@ const criteria = [
|
|
|
654
660
|
states = (await import(__rewriteRelativeImportExtension(statesPath))).states;
|
|
655
661
|
}
|
|
656
662
|
catch (e) {
|
|
657
|
-
return fail(`semantics/states.ts does not load: ${e.message.slice(0, 120)}`);
|
|
663
|
+
return { ...fail(`semantics/states.ts does not load: ${e.message.slice(0, 120)}`), cacheable: false };
|
|
658
664
|
}
|
|
659
665
|
if (!states)
|
|
660
666
|
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/types.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface ScoreReport {
|
|
|
53
53
|
}
|
|
54
54
|
export interface CriterionResult {
|
|
55
55
|
pass: boolean;
|
|
56
|
+
/** An interrupted check is not a verdict about unchanged source. */
|
|
57
|
+
cacheable?: boolean;
|
|
56
58
|
/** why it failed, or what it measured when it passed */
|
|
57
59
|
reason: string;
|
|
58
60
|
}
|