@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/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`);
|
|
@@ -321,6 +356,190 @@ async function standalone(pack) {
|
|
|
321
356
|
}
|
|
322
357
|
return out;
|
|
323
358
|
}
|
|
359
|
+
/** The request an application's first call of an operation sends when it knows nothing yet: every path parameter, every
|
|
360
|
+
* required query parameter and every header the operation is routed by holding a placeholder, its discriminator's
|
|
361
|
+
* values, and an empty body in the operation's encoding. What it measures is the twin's answer to a call it did not
|
|
362
|
+
* script: a refusal in the vendor's form, or a result in the vendor's shape, and never a crash, a hang or a row. */
|
|
363
|
+
function sweepRequest(op, basePath, host, credential) {
|
|
364
|
+
const fill = (name, type) => (type === 'integer' || type === 'number' ? '1' : type === 'boolean' ? 'true' : `conformance-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`);
|
|
365
|
+
const path = `${op.basePath ?? basePath}${op.path}`.replace(/\{([^}+]+)\+?\}/g, (_m, name) => fill(name));
|
|
366
|
+
const url = new URL(`https://${host}${path}`);
|
|
367
|
+
for (const q of op.query ?? [])
|
|
368
|
+
if (q.required)
|
|
369
|
+
url.searchParams.set(q.name, fill(q.name, q.type));
|
|
370
|
+
for (const [name, value] of Object.entries(op.discriminator ?? {}))
|
|
371
|
+
url.searchParams.set(name, value ?? fill(name));
|
|
372
|
+
const headers = new Headers(credential);
|
|
373
|
+
for (const [name, value] of Object.entries(op.headers ?? {}))
|
|
374
|
+
headers.set(name, value ?? fill(name));
|
|
375
|
+
const method = op.method.toUpperCase();
|
|
376
|
+
let body;
|
|
377
|
+
if (op.bodyEncoding === 'form') {
|
|
378
|
+
headers.set('content-type', 'application/x-www-form-urlencoded');
|
|
379
|
+
body = '';
|
|
380
|
+
}
|
|
381
|
+
else if (op.bodyEncoding === 'xml') {
|
|
382
|
+
headers.set('content-type', 'application/xml');
|
|
383
|
+
body = '';
|
|
384
|
+
}
|
|
385
|
+
else if (op.bodyEncoding === 'json' || op.bodyEncoding === 'multipart') {
|
|
386
|
+
headers.set('content-type', 'application/json');
|
|
387
|
+
body = '{}';
|
|
388
|
+
}
|
|
389
|
+
return new Request(url, { method, headers, ...(body !== undefined && !['GET', 'HEAD'].includes(method) ? { body } : {}) });
|
|
390
|
+
}
|
|
391
|
+
/** Every operation the pack serves, called over HTTP as an application calls it, against its server.ts served as a World
|
|
392
|
+
* runs it: once plainly and once with a scenario loaded (an empty one, as a World that scripts nothing loads it). Each
|
|
393
|
+
* answers within 10 s, never with a 500 the spec does not declare, never with a field of the twin's own (`_`
|
|
394
|
+
* bookkeeping, the kernel's `updatedAt`) and, answered successfully, in the shape the spec gives the operation; and every
|
|
395
|
+
* refresh list a fresh account can read (one with no parameter in its path) answers it successfully, so a World rooted
|
|
396
|
+
* at this twin reads its resources back. Escaped before: the scenario hang (a served pack given a scenario answered
|
|
397
|
+
* nothing), leaked kernel fields in answers no life step read, and refresh scopes on operations the pack answered with
|
|
398
|
+
* the gap. The life walks what a customer does; this calls what the pack declares it serves. */
|
|
399
|
+
async function surfaceSwept(pack, scratch) {
|
|
400
|
+
const out = [];
|
|
401
|
+
const descriptor = (await manifestOf(pack)).descriptor;
|
|
402
|
+
const fetchOf = (await import(__rewriteRelativeImportExtension(join(pack, 'src', 'fetch.ts'))))[fetchName(pack)];
|
|
403
|
+
const root = fetchOf?.();
|
|
404
|
+
const byUnit = root?.ownersByUnit?.() ?? { '': root?.owners?.() ?? {} };
|
|
405
|
+
const { shapeChecker } = await import("./walk/shape.js");
|
|
406
|
+
const { leaked } = await import("./walk/behavior-journey.js");
|
|
407
|
+
const shape = await shapeChecker(pack);
|
|
408
|
+
const units = [];
|
|
409
|
+
const vendorFields = new Set();
|
|
410
|
+
for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
|
|
411
|
+
const surface = readJson(join(unit, 'src', 'generated', 'surface.gen.json'));
|
|
412
|
+
for (const r of surface?.resources ?? [])
|
|
413
|
+
for (const f of r.fields)
|
|
414
|
+
vendorFields.add(f.name);
|
|
415
|
+
// a line protocol and a command table ride in no path of their own: the life's line steps and the examples read them
|
|
416
|
+
if (!surface?.operations || ['command-reply', 'command-table'].includes(String(surface.format)))
|
|
417
|
+
continue;
|
|
418
|
+
const m = (await manifestOf(unit));
|
|
419
|
+
const host = m.hosts?.find((h) => h.host)?.host ?? descriptor?.hosts?.find((h) => h.host)?.host;
|
|
420
|
+
if (!host) {
|
|
421
|
+
out.push(`${nameOf(pack, unit)}: no host is named (its manifest's hosts or the descriptor's) for an application to call it at`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const owners = byUnit[unit === pack ? '' : basename(unit)] ?? {};
|
|
425
|
+
const refreshLists = Object.entries(m.resources ?? {}).flatMap(([resource, decl]) => (decl.refresh?.list ? [{ resource, id: decl.refresh.list }] : []));
|
|
426
|
+
units.push({ name: nameOf(pack, unit), host, basePath: surface.basePath ?? '', ops: surface.operations.filter((o) => owners[o.id] !== undefined && owners[o.id] !== 'gap'), refreshLists, ...(m.auth ? { auth: m.auth } : {}) });
|
|
427
|
+
}
|
|
428
|
+
if (!units.length)
|
|
429
|
+
return { failures: out, note: 'no unit serves an operation on a path of its own' };
|
|
430
|
+
const tally = { calls: 0, ok: 0, refused: 0 };
|
|
431
|
+
const env = globalThis.process.env;
|
|
432
|
+
const scenarioPath = join(scratch, 'surface-scenario.json');
|
|
433
|
+
writeFileSync(scenarioPath, '{"handlers":[]}');
|
|
434
|
+
for (const mode of ['plain', 'scenario']) {
|
|
435
|
+
const before = env.VOLTER_WORLD_BOOT_ID;
|
|
436
|
+
env.VOLTER_WORLD_BOOT_ID = 'conformance';
|
|
437
|
+
let server;
|
|
438
|
+
try {
|
|
439
|
+
server = await serve(pack, { root: join(scratch, `surface-${mode}`), ...(mode === 'scenario' ? { scenarioPath } : {}) });
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
if (before === undefined)
|
|
443
|
+
delete env.VOLTER_WORLD_BOOT_ID;
|
|
444
|
+
else
|
|
445
|
+
env.VOLTER_WORLD_BOOT_ID = before;
|
|
446
|
+
}
|
|
447
|
+
const http = servedHttpFetch(server.inspectPort ?? server.port);
|
|
448
|
+
const send = async (request) => {
|
|
449
|
+
await server.managedDatabaseUrl?.();
|
|
450
|
+
const abort = new AbortController();
|
|
451
|
+
try {
|
|
452
|
+
return await within(10_000, http(request, { signal: abort.signal }));
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
return error;
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
abort.abort();
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
try {
|
|
462
|
+
// the account an application holds: the key the credential door issues, sent as the vendor reads its credential
|
|
463
|
+
let issued;
|
|
464
|
+
if (descriptor?.credentialDoor) {
|
|
465
|
+
const door = descriptor.credentialDoor;
|
|
466
|
+
const answer = await send(new Request(`https://${descriptor.hosts?.find((h) => h.host)?.host ?? units[0].host}${door.path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(door.body) }));
|
|
467
|
+
const fields = answer instanceof Response ? (await answer.json().catch(() => ({}))) : {};
|
|
468
|
+
issued = Object.values(door.fill).map((f) => fields[f]).find((v) => typeof v === 'string');
|
|
469
|
+
}
|
|
470
|
+
const said = mode === 'scenario' ? ' with a scenario loaded' : '';
|
|
471
|
+
for (const u of units) {
|
|
472
|
+
// a unit whose manifest declares no `auth` checks its key in its own front: the key goes as most vendors read
|
|
473
|
+
// one, a bearer token, and a refusal of it is no finding of the refresh below
|
|
474
|
+
const auth = u.auth ?? { header: 'authorization', scheme: 'Bearer' };
|
|
475
|
+
const credential = issued ? { [auth.header]: auth.scheme ? `${auth.scheme} ${issued}` : issued } : {};
|
|
476
|
+
const answered = new Map();
|
|
477
|
+
for (const op of u.ops) {
|
|
478
|
+
const request = sweepRequest(op, u.basePath, u.host, credential);
|
|
479
|
+
const where = `${u.name}: ${op.id} (${request.method} ${new URL(request.url).pathname})`;
|
|
480
|
+
const res = await send(request);
|
|
481
|
+
if (res === 'timeout') {
|
|
482
|
+
out.push(`${where} answered nothing in 10 s${said}`);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (res instanceof Error) {
|
|
486
|
+
out.push(`${where}: the connection failed${said} (${res.message})`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
answered.set(op.id, res.status);
|
|
490
|
+
if (mode === 'plain') {
|
|
491
|
+
tally.calls++;
|
|
492
|
+
if (res.status < 300)
|
|
493
|
+
tally.ok++;
|
|
494
|
+
else
|
|
495
|
+
tally.refused++;
|
|
496
|
+
}
|
|
497
|
+
const text = await res.text();
|
|
498
|
+
if (res.status === 500 && !op.responseMedia?.['500'] && !op.responseMedia?.['5XX']) {
|
|
499
|
+
out.push(`${where} answered 500${said}, which the spec does not declare: ${text.slice(0, 160)}`);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (mode === 'scenario')
|
|
503
|
+
continue;
|
|
504
|
+
const type = res.headers.get('content-type') ?? '';
|
|
505
|
+
let body;
|
|
506
|
+
if (/json/.test(type)) {
|
|
507
|
+
try {
|
|
508
|
+
body = JSON.parse(text);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
body = undefined;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const key of leaked(body, vendorFields, '', !op.answers || res.status >= 300))
|
|
515
|
+
out.push(`${where} answers the twin's own field ${key}`);
|
|
516
|
+
if (res.status >= 200 && res.status < 300 && shape) {
|
|
517
|
+
const hit = shape.operationOf(request.method, request.url, Object.fromEntries(request.headers));
|
|
518
|
+
if (hit)
|
|
519
|
+
for (const detail of shape.failures(hit.op, body, type, request.headers.get('accept') ?? undefined, res.status, { method: request.method, responseText: text }))
|
|
520
|
+
out.push(`${where}: ${detail}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (mode === 'plain')
|
|
524
|
+
for (const r of u.refreshLists) {
|
|
525
|
+
const op = u.ops.find((o) => o.id === r.id);
|
|
526
|
+
if (!op || op.path.includes('{') || (op.basePath ?? '').includes('{'))
|
|
527
|
+
continue;
|
|
528
|
+
const status = answered.get(r.id);
|
|
529
|
+
if (status !== undefined && (status === 401 || status === 403) && (!u.auth || !issued))
|
|
530
|
+
continue;
|
|
531
|
+
if (status !== undefined && (status < 200 || status >= 300))
|
|
532
|
+
out.push(`${u.name}: ${r.resource}'s refresh lists by ${r.id}, which answered a fresh account ${status}: a World rooted at this twin reads nothing back`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
await server.stop();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// one defect repeated over a surface reads as one: the first 40 findings, and how many more
|
|
541
|
+
return { failures: out.length > 40 ? [...out.slice(0, 40), `… ${out.length - 40} more`] : out, note: `${tally.calls} served operations called, twice each: ${tally.ok} answered successfully, ${tally.refused} refused, unscripted` };
|
|
542
|
+
}
|
|
324
543
|
/** A line protocol's connection over TCP, in the walk's stream shape. A line drew its replies when bytes arrived and then
|
|
325
544
|
* 50 ms passed with no more (a multi-line reply arrives in pieces), or when 500 ms passed with none (a line that draws no
|
|
326
545
|
* reply: a message's body before its end); 10 s bounds a server that keeps writing. The windows are timing, not protocol:
|
|
@@ -394,7 +613,9 @@ async function lifeServed(pack) {
|
|
|
394
613
|
socket: (url, headers) => new WebSocket(`ws://127.0.0.1:${port}${url}`, { headers }),
|
|
395
614
|
// a line connection opened as an application's client opens it: TCP to the protocol listener (the primary port)
|
|
396
615
|
stream: lineConnection(server.port),
|
|
397
|
-
|
|
616
|
+
// bounded: Bun's server.stop(true) never settles once a WebSocket peer has closed under it (discord's life, Bun
|
|
617
|
+
// 1.3.11: the walk ended and the stop spun at full CPU for 45 minutes), so the check waits 5 s and goes on
|
|
618
|
+
stop: async () => { await within(5000, Promise.resolve(server.stop())); },
|
|
398
619
|
};
|
|
399
620
|
};
|
|
400
621
|
const world = inMemoryWorld(`conform-${basename(pack)}`);
|
|
@@ -430,7 +651,9 @@ async function clientCases(pack, scratch) {
|
|
|
430
651
|
const descriptor = (await manifestOf(pack)).descriptor;
|
|
431
652
|
// the first host that names one: a descriptor may lead with a suffix (a region's hosts under one domain), which no
|
|
432
653
|
// request addresses; the case's client reaches whatever host its issued credentials name
|
|
433
|
-
|
|
654
|
+
// a raw protocol's pack (hostsNone: SMTP, whose relay is whatever host the operator names) is reached by its TCP
|
|
655
|
+
// connection alone; its client names a stand-in host the connection never resolves
|
|
656
|
+
const host = descriptor?.hosts?.find((h) => h.host)?.host ?? (descriptor?.hostsNone ? 'relay.world.test' : undefined);
|
|
434
657
|
if (!host)
|
|
435
658
|
return { failures: ['the descriptor names no host for its client to address'] };
|
|
436
659
|
const scenarioPath = join(scratch, 'client-scenario.json');
|
|
@@ -453,13 +676,31 @@ async function clientCases(pack, scratch) {
|
|
|
453
676
|
const issued = {};
|
|
454
677
|
const door = descriptor?.credentialDoor;
|
|
455
678
|
if (door) {
|
|
456
|
-
|
|
679
|
+
// a door that answers nothing is the pack's failure, never conform's hang (a pack served with a scenario that answered
|
|
680
|
+
// nothing once held this request open for good)
|
|
681
|
+
const abort = new AbortController();
|
|
682
|
+
const asked = await within(10_000, fetch(`http://127.0.0.1:${server.port}${door.path}`, { method: 'POST', headers: { host, 'content-type': 'application/json' }, body: JSON.stringify(door.body), signal: abort.signal }).then((r) => r.json().catch(() => ({}))));
|
|
683
|
+
abort.abort();
|
|
684
|
+
if (asked === 'timeout')
|
|
685
|
+
return { failures: [`the credential door (POST ${door.path}) answered nothing in 10 s, served with a scenario loaded`] };
|
|
686
|
+
const answer = asked;
|
|
457
687
|
for (const [name, field] of Object.entries(door.fill))
|
|
458
688
|
if (answer[field] !== undefined)
|
|
459
689
|
issued[name] = String(answer[field]);
|
|
460
690
|
}
|
|
461
691
|
const job = join(scratch, 'client-job.json');
|
|
462
|
-
|
|
692
|
+
// a release carries its facts (generated/pack-facts.json, `twin-standard facts`); a source checkout does not, so its
|
|
693
|
+
// facts are compiled here, as that command compiles them, for the client the case runs
|
|
694
|
+
let packFacts = join(pack, 'generated', 'pack-facts.json');
|
|
695
|
+
if (!existsSync(packFacts)) {
|
|
696
|
+
const { compilePackFacts, renderPackFacts } = await import("./pack-facts.js");
|
|
697
|
+
const compiled = await compilePackFacts([{ vendor, dir: pack }]);
|
|
698
|
+
if (compiled.undeclared.length)
|
|
699
|
+
throw new Error('Pack descriptor is missing');
|
|
700
|
+
packFacts = join(scratch, 'pack-facts.json');
|
|
701
|
+
writeFileSync(packFacts, renderPackFacts(compiled));
|
|
702
|
+
}
|
|
703
|
+
writeFileSync(job, JSON.stringify({ vendor, port: server.port, host, env: issued, scenarioPath, packFacts, ...(descriptor?.hostsNone ? { raw: true } : {}) }));
|
|
463
704
|
// Node, as an application runs its SDK (asynchronous: this process serves the pack meanwhile)
|
|
464
705
|
const out = await new Promise((resolve) => {
|
|
465
706
|
const child = spawn('node', ['--experimental-strip-types', '--no-warnings', join(clients, `run${extension}`), job], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -479,8 +720,46 @@ async function clientCases(pack, scratch) {
|
|
|
479
720
|
await server.stop();
|
|
480
721
|
}
|
|
481
722
|
}
|
|
723
|
+
/** The gate over every unit of the pack: the form section of the grade, which holds the rules an author's code keeps
|
|
724
|
+
* (p3-rules.ts: ids from ctx.mint, the twin's URL from ctx.publicBase, the kernel's crypto, bytes in ctx.blobs, secrets
|
|
725
|
+
* drawn from ctx.secret) and the pack layout, handler, kernel and manifest contracts. A contributor's conform is the
|
|
726
|
+
* whole of what a machine decides of the pack, as the catalog's admission reads it. */
|
|
727
|
+
async function formOf(pack) {
|
|
728
|
+
const { p3Gate } = await import("./gate.js");
|
|
729
|
+
return (await p3Gate(unitsIn(pack))).flatMap((r) => r.failures.map((f) => `${nameOf(pack, r.unit)}: ${f}`));
|
|
730
|
+
}
|
|
731
|
+
/** The pack's records hold what a reviewer reads them for: each demand row cites the application's line it was read at
|
|
732
|
+
* (`<path>:<line>`, or `<package>@<version>:<path>:<line>` for a call a dependency makes), the spec's record names a
|
|
733
|
+
* vendored file that is there, the life names the session that wrote it (what makes a judge's verdict independent),
|
|
734
|
+
* and the package says what it serves in a README. Rules every builder brief carried by hand, decided here. */
|
|
735
|
+
function records(pack) {
|
|
736
|
+
const out = [];
|
|
737
|
+
if (!existsSync(join(pack, 'README.md')))
|
|
738
|
+
out.push('README.md is missing: the package says what it serves, for whom, its doors and what it leaves out');
|
|
739
|
+
const life = readJson(join(pack, 'journeys', 'customer-life.json'));
|
|
740
|
+
if (life && (typeof life.author !== 'string' || !life.author.trim()))
|
|
741
|
+
out.push('journeys/customer-life.json names no author (the session that wrote it), so no verdict on it can be shown independent');
|
|
742
|
+
for (const unit of unitsIn(pack)) {
|
|
743
|
+
const name = nameOf(pack, unit);
|
|
744
|
+
const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
|
|
745
|
+
const demand = unit === pack || hasJourneySource(unit) ? readJson(join(journeys, 'demand.json')) : undefined;
|
|
746
|
+
for (const app of demand?.applications ?? [])
|
|
747
|
+
for (const row of app.backend ?? []) {
|
|
748
|
+
if (typeof row.evidence !== 'string' || !/:\d+/.test(row.evidence))
|
|
749
|
+
out.push(`${name}: ${app.name ?? 'an application'}'s call of ${row.operation ?? '(no operation)'} cites no line it was read at (evidence "<path>:<line>")`);
|
|
750
|
+
}
|
|
751
|
+
if (!hasOwnSurface(unit))
|
|
752
|
+
continue;
|
|
753
|
+
const source = existsSync(join(unit, 'spec', 'SOURCE.md')) ? readFileSync(join(unit, 'spec', 'SOURCE.md'), 'utf8') : '';
|
|
754
|
+
const file = /\*\*File:\*\*[^`\n]*`([^`]+)`/.exec(source)?.[1];
|
|
755
|
+
if (file && !existsSync(join(unit, 'spec', file)))
|
|
756
|
+
out.push(`${name}: spec/SOURCE.md names ${file} as the vendored spec, and spec/ holds no such file`);
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
}
|
|
482
760
|
/** Every check over the pack at `dir`, in order. */
|
|
483
|
-
|
|
761
|
+
/** `onCheck` hears each check's id as it starts, so a caller can show where a long run stands. */
|
|
762
|
+
export async function conformance(dir, options = {}) {
|
|
484
763
|
const pack = resolve(dir);
|
|
485
764
|
if (!existsSync(join(pack, 'package.json')))
|
|
486
765
|
throw new Error(`${dir} is no pack (no package.json): conformance takes a pack's directory`);
|
|
@@ -490,8 +769,10 @@ export async function conformance(dir) {
|
|
|
490
769
|
const here = process.cwd();
|
|
491
770
|
process.chdir(scratch);
|
|
492
771
|
const run = async (id, asks, check) => {
|
|
772
|
+
options.onCheck?.(id);
|
|
493
773
|
try {
|
|
494
|
-
|
|
774
|
+
const r = await check();
|
|
775
|
+
return Array.isArray(r) ? { id, asks, failures: r } : { id, asks, failures: r.failures, note: r.note };
|
|
495
776
|
}
|
|
496
777
|
catch (error) {
|
|
497
778
|
return { id, asks, failures: [`the check could not run: ${error.message.split('\n')[0]}`] };
|
|
@@ -506,6 +787,7 @@ export async function conformance(dir) {
|
|
|
506
787
|
await run('registered', 'a vendor-backed unit, lane or not, reaches the registered pack\'s state system', () => registered(pack, runtime)),
|
|
507
788
|
await run('allowance', 'a rate budget above the fallback rests on the vendor\'s documented allowance, cited in its manifest', () => allowance(pack, runtime)),
|
|
508
789
|
await (async () => {
|
|
790
|
+
options.onCheck?.('client');
|
|
509
791
|
const asks = "driven by the vendor's own client (its official SDK), served as a World runs it, the vendor's documented behaviour holds";
|
|
510
792
|
try {
|
|
511
793
|
const r = await clientCases(pack, scratch);
|
|
@@ -517,6 +799,9 @@ export async function conformance(dir) {
|
|
|
517
799
|
})(),
|
|
518
800
|
await run('life', 'the life walked over HTTP against the pack served as a World runs it, with its scenario: every check held', () => lifeServed(pack)),
|
|
519
801
|
await run('standalone', 'served by its server.ts as a World runs it, the pack answers HTTP and the boot probe, and each declared socket upgrades', () => standalone(pack)),
|
|
802
|
+
await run('surface', 'every served operation called over HTTP as an application calls it, plainly and with a scenario loaded: answered within 10 s, no undeclared 500, no field of the twin\'s own, a success in the spec\'s shape, and every refresh list a fresh account reads answering', () => surfaceSwept(pack, scratch)),
|
|
803
|
+
await run('records', 'each demand row cites the line it was read at, the spec\'s record names a vendored file that is there, the life names its author, the package has its README', () => records(pack)),
|
|
804
|
+
await run('form', 'every unit passes the form section of the grade (the gate): the layout, the handler and kernel contracts, the manifest as data, the rules an author\'s code holds', () => formOf(pack)),
|
|
520
805
|
];
|
|
521
806
|
}
|
|
522
807
|
finally {
|
package/dist/src/consumer.js
CHANGED
|
@@ -201,7 +201,11 @@ export async function runConsumerWorkflow(packDir, options) {
|
|
|
201
201
|
throw new Error('Resume changed recorded application state before readback');
|
|
202
202
|
await run('retained-readback', ['run', '--', ...recipe.retention.readback]);
|
|
203
203
|
const after = JSON.parse(await run('log-after-readback', ['log', '--json']));
|
|
204
|
-
|
|
204
|
+
// the readback adds no write of the application's: what it adds may only be the vendor's own moves (a key's
|
|
205
|
+
// last-used time, recorded by the vendor as a request uses it: actor `system`, ctx.asVendor), never an API call's
|
|
206
|
+
const added = after.filter((entry) => !before.some((prior) => prior.id === entry.id));
|
|
207
|
+
const kept = before.every((prior) => after.some((entry) => JSON.stringify(entry) === JSON.stringify(prior)));
|
|
208
|
+
if (!kept || added.some((entry) => entry.actor?.kind !== 'system'))
|
|
205
209
|
throw new Error('Retained readback added mutations; it must only inspect existing state');
|
|
206
210
|
retainedReadback = true;
|
|
207
211
|
}
|
|
@@ -39,4 +39,9 @@ export declare function initFiles(vendor: string): Record<string, string>;
|
|
|
39
39
|
* surface of its own under `<lane>/src`, keeping the vendor's one store and sharing its `src/semantics/shared.ts`, and the
|
|
40
40
|
* pack's server routing each lane's host to it. */
|
|
41
41
|
export declare function laneInitFiles(vendor: string, lanes: string[]): Record<string, string>;
|
|
42
|
+
/** A unit's fixed files as the manifest and the spec make them now (architecture, "Creating a pack"): a lane's fetch, and
|
|
43
|
+
* a pack's fetch, index, server and cli. A pack with a wire of its own (`src/stream.ts`, SMTP's and Redis's byte-stream
|
|
44
|
+
* door) writes its server and cli itself: they carry its listener beside the fetch, so they are not fixed. `--index`
|
|
45
|
+
* writes these and the grade's `fixed-reproducible` compares them byte for byte. */
|
|
46
|
+
export declare function fixedFiles(dir: string): Promise<Record<string, string>>;
|
|
42
47
|
export declare function createPack(args: string[]): Promise<void>;
|
package/dist/src/create-pack.js
CHANGED
|
@@ -160,6 +160,9 @@ export function packFetchFile(dir, name) {
|
|
|
160
160
|
const hasLive = existsSync(join(semantics, 'live.ts'));
|
|
161
161
|
// a GraphQL API beside the REST one: its resolvers and the vendor's schema (graphql-wire.ts)
|
|
162
162
|
const hasGraphql = existsSync(join(semantics, 'graphql.ts')) && existsSync(join(dir, 'src', 'generated', 'graphql-sdl.gen.json'));
|
|
163
|
+
// a stream whose documents are BSON (the MongoDB wire): the pack's own bson library, resolved from the pack as graphql's is
|
|
164
|
+
const packageJson = existsSync(join(dir, 'package.json')) ? parseExactJson(readFileSync(join(dir, 'package.json'), 'utf8')) : {};
|
|
165
|
+
const hasBson = existsSync(join(dir, 'src', 'stream.ts')) && packageJson.dependencies?.bson !== undefined;
|
|
163
166
|
const screensDir = join(dir, 'src', 'screens');
|
|
164
167
|
const screens = existsSync(screensDir) ? readdirSync(screensDir).filter((n) => /\.tsx?$/.test(n) && !/\.(test|uitest)\.tsx?$/.test(n) && !/^shared\.tsx?$/.test(n)).sort() : [];
|
|
165
168
|
const id = (file) => file.replace(/\.tsx?$/, '');
|
|
@@ -205,6 +208,7 @@ export function packFetchFile(dir, name) {
|
|
|
205
208
|
...screens.map((f) => `import { screen as ${identifier(`screen_${id(f)}`)} } from './screens/${f}';`),
|
|
206
209
|
...laneImports,
|
|
207
210
|
'',
|
|
211
|
+
...(hasBson ? ["/** The pack's BSON library, resolved from the pack as graphql's loader is (architecture, the MongoDB wire). */", "export const loadBson = (): Promise<unknown> => import('bson');", ''] : []),
|
|
208
212
|
`export function ${name}(options: PackFetchOptions = {}): DerivedFetch {`,
|
|
209
213
|
' return createPackFetch({',
|
|
210
214
|
' manifest,',
|
|
@@ -507,6 +511,27 @@ export function laneInitFiles(vendor, lanes) {
|
|
|
507
511
|
}
|
|
508
512
|
return files;
|
|
509
513
|
}
|
|
514
|
+
/** A unit's fixed files as the manifest and the spec make them now (architecture, "Creating a pack"): a lane's fetch, and
|
|
515
|
+
* a pack's fetch, index, server and cli. A pack with a wire of its own (`src/stream.ts`, SMTP's and Redis's byte-stream
|
|
516
|
+
* door) writes its server and cli itself: they carry its listener beside the fetch, so they are not fixed. `--index`
|
|
517
|
+
* writes these and the grade's `fixed-reproducible` compares them byte for byte. */
|
|
518
|
+
export async function fixedFiles(dir) {
|
|
519
|
+
const files = { 'src/fetch.ts': packFetchFile(dir, fetchName(dir)) };
|
|
520
|
+
if (!existsSync(join(dir, 'package.json')))
|
|
521
|
+
return files;
|
|
522
|
+
const { manifest } = await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')));
|
|
523
|
+
const vendor = manifest?.vendor;
|
|
524
|
+
if (!vendor || !/^[a-z][a-z0-9-]*$/.test(vendor))
|
|
525
|
+
throw new Error('Pack manifest must declare its vendor identity independently of the npm package name');
|
|
526
|
+
files['src/index.ts'] = packIndexFile(dir, vendor);
|
|
527
|
+
if (!existsSync(join(dir, 'src', 'stream.ts'))) {
|
|
528
|
+
// a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form
|
|
529
|
+
const lanesOnly = !hasOwnSurface(dir) && readdirSync(dir).some((n) => existsSync(join(dir, n, 'src', 'manifest.ts')));
|
|
530
|
+
files['src/server.ts'] = serverFile(vendor, !lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json')));
|
|
531
|
+
files['src/cli.ts'] = cliFile(vendor);
|
|
532
|
+
}
|
|
533
|
+
return files;
|
|
534
|
+
}
|
|
510
535
|
export async function createPack(args) {
|
|
511
536
|
const [unit, ...rest] = args;
|
|
512
537
|
if (!unit || (!rest.includes('--index') && !rest.includes('--brief') && !rest.includes('--init'))) {
|
|
@@ -598,9 +623,12 @@ export async function createPack(args) {
|
|
|
598
623
|
writeFileSync(path, handlerIndex(dir));
|
|
599
624
|
console.log(`create-pack: wrote ${path}`);
|
|
600
625
|
}
|
|
601
|
-
// the
|
|
602
|
-
|
|
603
|
-
|
|
626
|
+
// the fixed files, again: a door file, the clock, a screen, a lane or a proto may have been added since, and a pack
|
|
627
|
+
// made by an earlier create-pack takes the current form
|
|
628
|
+
for (const [rel, text] of Object.entries(await fixedFiles(dir))) {
|
|
629
|
+
writeFileSync(join(dir, rel), text);
|
|
630
|
+
console.log(`create-pack: wrote ${join(dir, rel)}`);
|
|
631
|
+
}
|
|
604
632
|
// a pack's own index (a lane's is the vendor's to name): the descriptor with what the kernel derives from the surface
|
|
605
633
|
if (existsSync(join(dir, 'package.json'))) {
|
|
606
634
|
const { manifest } = await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')));
|
|
@@ -617,7 +645,13 @@ export async function createPack(args) {
|
|
|
617
645
|
// a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form,
|
|
618
646
|
// 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
647
|
if (!lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'))) {
|
|
620
|
-
|
|
648
|
+
const wire = existsSync(join(dir, 'src', 'stream.ts'));
|
|
649
|
+
// a raw-tcp pack exporting one create<Name>TwinStream is taken to serve that stream on its main port, and is admitted
|
|
650
|
+
// by its TCP answer alone (world-runtime host.ts servesByteStream); this one serves gRPC there, its stream on a side port
|
|
651
|
+
if (wire && manifest?.descriptor?.transport === 'raw-tcp') {
|
|
652
|
+
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).`);
|
|
653
|
+
}
|
|
654
|
+
writeFileSync(join(dir, 'src', 'server.ts'), serverFile(vendor, true, wire));
|
|
621
655
|
console.log(`create-pack: wrote ${join(dir, 'src', 'server.ts')}`);
|
|
622
656
|
}
|
|
623
657
|
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