@volter/twin-standard 1.0.147 → 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 +8 -2
- 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 +36 -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/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 +9 -3
- 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/types.ts +2 -0
- package/src/walk/behavior-journey.ts +59 -2
- package/src/walk/journey-kit.ts +14 -1
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
|
}
|
|
@@ -8,7 +8,8 @@ import { attributionName } from "../verdict.js";
|
|
|
8
8
|
// score-pack walks the life through it twice, on two fresh roots, and compares what they answer.
|
|
9
9
|
//
|
|
10
10
|
// The caller names the pack repository first (usePacks).
|
|
11
|
-
import { mysql, redis } from '@volter/world-core';
|
|
11
|
+
import { mongodb, mysql, redis } from '@volter/world-core';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
12
13
|
import { gzipSync } from 'node:zlib';
|
|
13
14
|
import { parse, visit } from 'graphql';
|
|
14
15
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
@@ -16,7 +17,7 @@ import { join } from 'node:path';
|
|
|
16
17
|
import { databaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessTwins } from '@volter/world-core/runtime';
|
|
17
18
|
import { createHostTable } from '@volter/world-core/host-routing';
|
|
18
19
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
19
|
-
import { isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from "./journey-kit.js";
|
|
20
|
+
import { isMongo, isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from "./journey-kit.js";
|
|
20
21
|
import { lanesOf, lineShape, shapeChecker } from "./shape.js";
|
|
21
22
|
import { walkPackDirectory } from "./pack-directory.js";
|
|
22
23
|
import { xmlMembers } from "./xml-view.js";
|
|
@@ -579,6 +580,99 @@ export async function walk(journey, root, collect, opts) {
|
|
|
579
580
|
opts.hooks?.after?.(step, link, answered, collect.slice(before));
|
|
580
581
|
}
|
|
581
582
|
};
|
|
583
|
+
// A MongoDB step speaks the pack's MongoDB wire as an application's driver does (@volter/world-core/mongodb's client), on
|
|
584
|
+
// the same byte-stream adapter the owned TCP listener serves. The document codec is BSON, the library the pack
|
|
585
|
+
// installs: the walk loads it from the pack's own dependencies, so a step is encoded by the library the pack decodes with.
|
|
586
|
+
const mongoLinks = new Map();
|
|
587
|
+
let bson;
|
|
588
|
+
const bareNumber = (value, at = 'mongo') => {
|
|
589
|
+
if (typeof value === 'number')
|
|
590
|
+
return at;
|
|
591
|
+
if (Array.isArray(value)) {
|
|
592
|
+
for (const [i, item] of value.entries()) {
|
|
593
|
+
const found = bareNumber(item, `${at}[${i}]`);
|
|
594
|
+
if (found)
|
|
595
|
+
return found;
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
if (value && typeof value === 'object')
|
|
600
|
+
for (const [key, item] of Object.entries(value)) {
|
|
601
|
+
const found = bareNumber(item, `${at}.${key}`);
|
|
602
|
+
if (found)
|
|
603
|
+
return found;
|
|
604
|
+
}
|
|
605
|
+
return undefined;
|
|
606
|
+
};
|
|
607
|
+
const mongoStep = async (step) => {
|
|
608
|
+
const before = collect.length;
|
|
609
|
+
let answered = { status: 0, body: undefined };
|
|
610
|
+
const link = step.link ?? 'mongo';
|
|
611
|
+
opts.hooks?.before?.(step, link, step.mongo);
|
|
612
|
+
try {
|
|
613
|
+
if (!bson) {
|
|
614
|
+
const pack = walkPackDirectory(packsDir(), journey.vendor, dependenciesDir());
|
|
615
|
+
if (!pack)
|
|
616
|
+
throw new Error(`no pack directory for ${journey.vendor} to load its BSON library from`);
|
|
617
|
+
bson = createRequire(join(pack, 'package.json'))('bson');
|
|
618
|
+
}
|
|
619
|
+
const codec = { serialize: (document) => bson.serialize(document), deserialize: (bytes) => bson.deserialize(bytes, { promoteValues: false, promoteLongs: false, bsonRegExp: true }) };
|
|
620
|
+
let held = mongoLinks.get(link);
|
|
621
|
+
if (!held) {
|
|
622
|
+
if (!served?.stream)
|
|
623
|
+
streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
624
|
+
const factory = served?.stream ?? streams?.(root, {});
|
|
625
|
+
if (!factory)
|
|
626
|
+
throw new Error(`no create<Name>TwinStream adapter enrolled for ${journey.vendor}`);
|
|
627
|
+
let out = [];
|
|
628
|
+
const conn = factory({ write: (b) => { out.push(...b); }, end: () => { } }, '127.0.0.1');
|
|
629
|
+
const exchange = async (bytes) => { conn.data(bytes); await conn.settled(); const got = Uint8Array.from(out); out = []; return got; };
|
|
630
|
+
held = { client: mongodb.mongoClient(exchange, codec), database: 'test', close: () => conn.close() };
|
|
631
|
+
mongoLinks.set(link, held);
|
|
632
|
+
}
|
|
633
|
+
if (step.database)
|
|
634
|
+
held.database = fill(step.database, captured);
|
|
635
|
+
const written = fillJson(step.mongo, captured);
|
|
636
|
+
const bare = bareNumber(written);
|
|
637
|
+
if (bare)
|
|
638
|
+
throw new Error(`${bare} is a bare JSON number: write its kind ({ "$numberInt": "…" }, $numberLong or $numberDouble)`);
|
|
639
|
+
const reply = await held.client.command(held.database, bson.EJSON.deserialize(written, { relaxed: false }));
|
|
640
|
+
const body = bson.EJSON.serialize(reply, { relaxed: false });
|
|
641
|
+
const ok = body.ok, failed = !ok || Number(Object.values(ok)[0]) !== 1;
|
|
642
|
+
const code = failed ? Number(Object.values(body.code ?? { none: Number.NaN })[0]) : undefined;
|
|
643
|
+
answered = { status: code ?? 0, body };
|
|
644
|
+
if (opts.verbose)
|
|
645
|
+
console.log(` ${String(code ?? 'OK').padEnd(5)} ${link.padEnd(6)} ${String(Object.keys(step.mongo)[0] ?? '').padEnd(20)} ${step.name}`);
|
|
646
|
+
if (step.refused !== undefined ? code !== step.refused : failed) {
|
|
647
|
+
collect.push({ step: step.name, beat: 'STATUS', detail: failed ? `MongoDB refused it: ${code} ${String(body.codeName ?? '')} ${String(body.errmsg ?? '')}${step.refused ? ` (wanted ${step.refused})` : ''}` : `MongoDB completed it, wanted ${step.refused}` });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
for (const [name, dot] of Object.entries(step.capture ?? {})) {
|
|
651
|
+
const value = captureAt(body, dot);
|
|
652
|
+
if (value === undefined || value === null)
|
|
653
|
+
collect.push({ step: step.name, beat: 'CAPTURE', detail: `nothing at "${dot}" in the answer to capture as {${name}}` });
|
|
654
|
+
else
|
|
655
|
+
remember(name, value);
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
for (const detail of expectFailures(step.expect ?? [], body, captured))
|
|
659
|
+
collect.push({ step: step.name, beat: 'EXPECT', detail });
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
collect.push({ step: step.name, beat: 'CAPTURE', detail: e.message });
|
|
663
|
+
}
|
|
664
|
+
if (step.close) {
|
|
665
|
+
mongoLinks.get(link)?.close();
|
|
666
|
+
mongoLinks.delete(link);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
collect.push({ step: step.name, beat: 'MONGO', detail: e.message });
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
opts.hooks?.after?.(step, link, answered, collect.slice(before));
|
|
674
|
+
}
|
|
675
|
+
};
|
|
582
676
|
// A git step is a git client's clone or push on the pack's smart-HTTP plane (packages/twin-standard/src/walk/git-client.ts); its answer is
|
|
583
677
|
// checked as a request's is (status, captures, expectations).
|
|
584
678
|
const gitStep = async (step) => {
|
|
@@ -931,6 +1025,10 @@ export async function walk(journey, root, collect, opts) {
|
|
|
931
1025
|
try {
|
|
932
1026
|
for (const step of journey.steps) {
|
|
933
1027
|
stepNow = step.name;
|
|
1028
|
+
if (isMongo(step)) {
|
|
1029
|
+
await mongoStep(step);
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
934
1032
|
if (isMysql(step)) {
|
|
935
1033
|
await mysqlStep(step);
|
|
936
1034
|
continue;
|
|
@@ -226,7 +226,7 @@ export type Journey = {
|
|
|
226
226
|
}) => boolean;
|
|
227
227
|
steps: Step[];
|
|
228
228
|
};
|
|
229
|
-
export type Step = JourneyStep | WaitStep | LineStep | GitStep | SocketStep | SqlStep | RespStep | MysqlStep;
|
|
229
|
+
export type Step = JourneyStep | WaitStep | LineStep | GitStep | SocketStep | SqlStep | RespStep | MysqlStep | MongoStep;
|
|
230
230
|
/** A step on a MySQL wire (the kernel's @volter/world-core/mysql, a pack's `create<Name>TwinStream`), spoken as an
|
|
231
231
|
* application's driver speaks it. `signIn` opens a named connection (`link`, `mysql` by default) with a user and
|
|
232
232
|
* password (mysql_native_password); `mysql` sends a statement on it: a text query, or a prepared statement when
|
|
@@ -249,6 +249,26 @@ export type MysqlStep = {
|
|
|
249
249
|
expect?: JourneyStep['expect'];
|
|
250
250
|
};
|
|
251
251
|
export declare const isMysql: (step: Step) => step is MysqlStep;
|
|
252
|
+
/** A step on a pack's MongoDB wire, sent as an application's driver sends it (`@volter/world-core/mongodb`'s client)
|
|
253
|
+
* on a named `link`, opened by the first step that names it. `mongo` is ONE command document, written in
|
|
254
|
+
* canonical extended JSON so that what a document holds is written as it is: an ObjectId as `{ "$oid": … }`, a date
|
|
255
|
+
* as `{ "$date": … }`, and a number with its kind (`{ "$numberInt": "1" }`, `$numberLong`, `$numberDouble`); a bare
|
|
256
|
+
* JSON number is refused, because JSON does not say which of three kinds it is. `database` is the database it is
|
|
257
|
+
* sent to (the link's last one, else `test`). Its answer is the reply document in the same form. A reply whose
|
|
258
|
+
* `ok` is 0 fails the step unless `refused` names its `code`. */
|
|
259
|
+
export type MongoStep = {
|
|
260
|
+
name: string;
|
|
261
|
+
mongo: Record<string, unknown>;
|
|
262
|
+
database?: string;
|
|
263
|
+
link?: string;
|
|
264
|
+
close?: boolean;
|
|
265
|
+
refused?: number;
|
|
266
|
+
capture?: JourneyStep['capture'];
|
|
267
|
+
expect?: JourneyStep['expect'];
|
|
268
|
+
example?: string;
|
|
269
|
+
exampleDiffers?: Record<string, string>;
|
|
270
|
+
};
|
|
271
|
+
export declare const isMongo: (step: Step) => step is MongoStep;
|
|
252
272
|
/** A typed request/reply stream step. `resp` is one command or a pipeline of commands, on a named connection.
|
|
253
273
|
* Captures and expectations see `{ replies, last, error }`; binary arguments use `{ base64 }`.
|
|
254
274
|
* `read` drains pushes or a blocking command's answer without sending bytes. */
|
|
@@ -198,6 +198,7 @@ export function expandFills(journey) {
|
|
|
198
198
|
return steps.some((x, i) => x !== journey.steps[i]) ? { ...journey, steps } : journey;
|
|
199
199
|
}
|
|
200
200
|
export const isMysql = (step) => 'mysql' in step || 'signIn' in step;
|
|
201
|
+
export const isMongo = (step) => 'mongo' in step;
|
|
201
202
|
export const isResp = (step) => 'connection' in step;
|
|
202
203
|
export const isLine = (step) => 'on' in step;
|
|
203
204
|
export const isSocket = (step) => 'socket' in step;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-standard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.149",
|
|
4
4
|
"description": "The Protocol 3 standard for Volter twin packs: the criteria a pack is graded by, the grader, the gate a pack repository and the twin catalog run on every change and submission, and the spec derivation the grade checks against. Pin a version: every place a pack is judged judges by that standard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"yaml": "2.9.0"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@volter/world-core": "3.0.
|
|
78
|
+
"@volter/world-core": "3.0.156",
|
|
79
79
|
"@volter/world-runtime": "*",
|
|
80
80
|
"playwright": "^1.63.0"
|
|
81
81
|
},
|
package/src/conformance.ts
CHANGED
|
@@ -65,6 +65,38 @@ export function unpublishedUnitFiles(unit: string): string[] {
|
|
|
65
65
|
|
|
66
66
|
/** Each of UNIT_FILES a unit lacks where conformance reads it: the manifest of every unit, the life at its vendor's root
|
|
67
67
|
* (lifeServed), the spec's record and the decision table of a unit with a spec (decided()). */
|
|
68
|
+
const trackedJourneySources = new Map<string, string[]>();
|
|
69
|
+
/** Table ownership follows one index snapshot per checkout/run. */
|
|
70
|
+
function hasJourneySource(unit: string): boolean {
|
|
71
|
+
const dir = join(unit, 'journeys');
|
|
72
|
+
let checkout = resolve(unit);
|
|
73
|
+
while (!existsSync(join(checkout, '.git'))) {
|
|
74
|
+
const parent = dirname(checkout);
|
|
75
|
+
if (parent === checkout) {
|
|
76
|
+
// An unpacked published package has no Git index; its delivered files
|
|
77
|
+
// are its source. Markers and empty directories are not table inputs.
|
|
78
|
+
if (!existsSync(dir)) return false;
|
|
79
|
+
const holdsFile = (at: string): boolean => readdirSync(at, { withFileTypes: true }).some(entry => entry.name !== '.metadata_never_index'
|
|
80
|
+
&& (entry.isFile() || (entry.isDirectory() && holdsFile(join(at, entry.name)))));
|
|
81
|
+
return holdsFile(dir);
|
|
82
|
+
}
|
|
83
|
+
checkout = parent;
|
|
84
|
+
}
|
|
85
|
+
const env = { ...process.env };
|
|
86
|
+
for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_PREFIX', 'GIT_INDEX_FILE']) delete env[key];
|
|
87
|
+
if (process.env.VOLTER_PACK_HOOK_INDEX) env.GIT_INDEX_FILE = process.env.VOLTER_PACK_HOOK_INDEX;
|
|
88
|
+
const key = `${checkout}\0${env.GIT_INDEX_FILE ?? ''}`;
|
|
89
|
+
let source = trackedJourneySources.get(key);
|
|
90
|
+
if (!source) {
|
|
91
|
+
const files = spawnSync('git', ['-C', checkout, 'ls-files', '-z', '--cached', '--', ':(glob)**/journeys/**'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024 });
|
|
92
|
+
if (files.status !== 0 || files.error) throw new Error(`cannot read tracked journey source: ${String(files.stderr || files.error?.message)}`);
|
|
93
|
+
source = files.stdout.split('\0').filter(path => path && !path.split('/').includes('.metadata_never_index'));
|
|
94
|
+
trackedJourneySources.set(key, source);
|
|
95
|
+
}
|
|
96
|
+
const prefix = relative(checkout, dir).replaceAll('\\', '/') + '/';
|
|
97
|
+
return source.some(path => path.startsWith(prefix));
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
export function unitFilesMissing(unit: string): string[] {
|
|
69
101
|
const vendor = existsSync(join(unit, 'package.json')) ? unit : dirname(unit);
|
|
70
102
|
const spec = hasOwnSurface(unit);
|
|
@@ -72,7 +104,7 @@ export function unitFilesMissing(unit: string): string[] {
|
|
|
72
104
|
'src/manifest.ts': unit,
|
|
73
105
|
'spec/SOURCE.md': spec ? unit : undefined,
|
|
74
106
|
'journeys/customer-life.json': vendor,
|
|
75
|
-
'journeys/decisions.json': spec ? (
|
|
107
|
+
'journeys/decisions.json': spec ? (hasJourneySource(unit) ? unit : vendor) : undefined,
|
|
76
108
|
};
|
|
77
109
|
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)`);
|
|
78
110
|
}
|
|
@@ -125,7 +157,7 @@ async function decided(pack: string): Promise<string[]> {
|
|
|
125
157
|
for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
|
|
126
158
|
const name = nameOf(pack, unit);
|
|
127
159
|
const key = unit === pack ? '' : basename(unit);
|
|
128
|
-
const journeys =
|
|
160
|
+
const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
|
|
129
161
|
const owners = byUnit ? byUnit[key] ?? {} : (await makeOf(unit))?.().owners?.();
|
|
130
162
|
if (!owners) { out.push(`${name}: its fetch (${fetchName(unit)}) names no owners() to check the decision table against`); continue; }
|
|
131
163
|
const table = readJson(join(journeys, 'decisions.json')) as { decisions?: Decision[] } | undefined;
|
package/src/create-pack.ts
CHANGED
|
@@ -209,7 +209,7 @@ export function packFetchFile(dir: string, name: string): string {
|
|
|
209
209
|
...(hasGap ? [' gap,'] : []),
|
|
210
210
|
...(hasSockets ? [' sockets,'] : []),
|
|
211
211
|
...(hasLive ? [' live,'] : []),
|
|
212
|
-
...(hasGraphql ? [' graphql: { ...graphql, operations: graphqlSurface.operations as NonNullable<typeof graphql.operations>, sdl: schema.sdl },'] : []),
|
|
212
|
+
...(hasGraphql ? [' graphql: { ...graphql, operations: graphqlSurface.operations as NonNullable<typeof graphql.operations>, sdl: schema.sdl, loadLibrary: () => import(\'graphql\') },'] : []),
|
|
213
213
|
...(screens.length ? [` screens: { ${screens.map((f) => `${JSON.stringify(id(f))}: ${identifier(`screen_${id(f)}`)}`).join(', ')} },`] : []),
|
|
214
214
|
...(lanes.length ? [` lanes: ${laneParts},`] : []),
|
|
215
215
|
' }, options);',
|
|
@@ -567,7 +567,7 @@ export async function createPack(args: string[]): Promise<void> {
|
|
|
567
567
|
console.log(`create-pack: wrote ${join(dir, 'src', 'fetch.ts')}`);
|
|
568
568
|
// a pack's own index (a lane's is the vendor's to name): the descriptor with what the kernel derives from the surface
|
|
569
569
|
if (existsSync(join(dir, 'package.json'))) {
|
|
570
|
-
const { manifest } = await import(join(dir, 'src', 'manifest.ts')) as { manifest?: { vendor?: string } };
|
|
570
|
+
const { manifest } = await import(join(dir, 'src', 'manifest.ts')) as { manifest?: { vendor?: string; descriptor?: { transport?: string } } };
|
|
571
571
|
const vendor = manifest?.vendor;
|
|
572
572
|
if (!vendor || !/^[a-z][a-z0-9-]*$/.test(vendor)) throw new Error('Pack manifest must declare its vendor identity independently of the npm package name');
|
|
573
573
|
writeFileSync(join(dir, 'src', 'index.ts'), packIndexFile(dir, vendor));
|
|
@@ -577,7 +577,13 @@ export async function createPack(args: string[]): Promise<void> {
|
|
|
577
577
|
// a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form,
|
|
578
578
|
// 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)
|
|
579
579
|
if (!lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'))) {
|
|
580
|
-
|
|
580
|
+
const wire = existsSync(join(dir, 'src', 'stream.ts'));
|
|
581
|
+
// a raw-tcp pack exporting one create<Name>TwinStream is taken to serve that stream on its main port, and is admitted
|
|
582
|
+
// by its TCP answer alone (world-runtime host.ts servesByteStream); this one serves gRPC there, its stream on a side port
|
|
583
|
+
if (wire && manifest?.descriptor?.transport === 'raw-tcp') {
|
|
584
|
+
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).`);
|
|
585
|
+
}
|
|
586
|
+
writeFileSync(join(dir, 'src', 'server.ts'), serverFile(vendor, true, wire));
|
|
581
587
|
console.log(`create-pack: wrote ${join(dir, 'src', 'server.ts')}`);
|
|
582
588
|
} else if ((manifest as { live?: unknown[] } | undefined)?.live?.length) {
|
|
583
589
|
// a unit that takes broadcasts over RTMP serves its ingest beside its HTTP host, and its bin takes the ingest's
|
package/src/derive.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { gunzipSync } from 'node:zlib';
|
|
|
14
14
|
import { CRUD_CLASSES, fromOpenAPI, fromSmithy, fromSwagger2, slug, type SpecIR } from './spec-ir.ts';
|
|
15
15
|
import { fromCommandReplies, type LineSpec } from './spec-ir-lines.ts';
|
|
16
16
|
import { fromProto, protoSchema, readProtoFiles } from './spec-ir-proto.ts';
|
|
17
|
-
import { fromRedisCommands, type CommandTableSpec } from './spec-ir-commands.ts';
|
|
17
|
+
import { fromDriverCommandBuilders, fromRedisCommands, type CommandTableSpec, type DocumentCommandTableSpec, type DriverCommandBuilders } from './spec-ir-commands.ts';
|
|
18
18
|
import { fromClientCalls, type ClientOpsSpec } from './spec-ir-client.ts';
|
|
19
19
|
import { discoveryMethods, fromDiscovery, isDiscovery } from './spec-ir-discovery.ts';
|
|
20
20
|
import { handlerCitations, quotedPage, sourceHeld, type Patch, type SourceRecord, urlsIn } from './check-sources.ts';
|
|
@@ -28,22 +28,28 @@ if (!vendor) {
|
|
|
28
28
|
console.error('usage: twin-standard derive <pack dir> [--out <dir>]');
|
|
29
29
|
process.exit(1);
|
|
30
30
|
}
|
|
31
|
+
// A source-declared refusal is deterministic over the pinned bytes. Unexpected
|
|
32
|
+
// loader, filesystem and process failures keep status 1 and must be retried.
|
|
33
|
+
const SOURCE_REFUSAL = 2;
|
|
31
34
|
const PKG = unitAt(vendor).dir;
|
|
32
35
|
// an OpenAPI document, an AWS Smithy JSON model (smithy.json), the RFC a line protocol is (rfcNNNN.txt), or the
|
|
33
36
|
// Protocol Buffers files an RPC service is published as (spec/proto/, every file the service imports), or the calls a
|
|
34
37
|
// vendor's own client makes where the vendor publishes no spec (client-ops.json, spec-ir-client.ts), or one API's several
|
|
35
38
|
// OpenAPI documents (spec/openapi/, read as their union: spec-documents.ts), or a Google API Discovery document
|
|
36
|
-
// (discovery.json, spec-ir-discovery.ts)
|
|
39
|
+
// (discovery.json, spec-ir-discovery.ts), or the command documents a vendor's own client builds where the vendor
|
|
40
|
+
// publishes no table a pack may derive from (command-builders.json, spec-ir-commands.ts)
|
|
37
41
|
const rfc = existsSync(join(PKG, 'spec')) ? readdirSync(join(PKG, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
|
|
38
|
-
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));
|
|
42
|
+
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));
|
|
39
43
|
if (!specPath) {
|
|
40
|
-
console.error(`derive-pack: no spec at ${join(PKG, 'spec')}/openapi.{json,yaml}[.gz], smithy.json[.gz], rfcNNNN.txt, proto/, commands
|
|
41
|
-
process.exit(
|
|
44
|
+
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`);
|
|
45
|
+
process.exit(SOURCE_REFUSAL);
|
|
42
46
|
}
|
|
43
47
|
const rpcService = basename(specPath) === 'proto';
|
|
44
48
|
const severalDocuments = basename(specPath) === 'openapi';
|
|
45
49
|
// a command language's table, one vendored file per command (Redis's src/commands/*.json), with its version in SOURCE.md
|
|
46
50
|
const commandTable = basename(specPath) === 'commands';
|
|
51
|
+
// a language whose commands are documents: the table a pack's extractor read from the vendor's client
|
|
52
|
+
const commandBuilders = basename(specPath) === 'command-builders.json';
|
|
47
53
|
const protoTree = (dir: string, at = ''): Record<string, string> => 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')]] : [])));
|
|
48
54
|
const bytes = rpcService || commandTable || severalDocuments ? Buffer.from('') : readFileSync(specPath);
|
|
49
55
|
const text = (specPath.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
@@ -139,7 +145,7 @@ for (const c of handlerCitations(PKG)) {
|
|
|
139
145
|
const patchPath = join(PKG, 'spec', 'patches.json');
|
|
140
146
|
if (existsSync(patchPath)) {
|
|
141
147
|
for (const p of parseExactJson(readFileSync(patchPath, 'utf8')).patches as Array<{ op: string; path: string; from?: string; value?: unknown; why: string }>) {
|
|
142
|
-
if (!p.why?.trim()) { console.error(`${vendor}: spec patch ${p.op} ${p.path} gives no why`); process.exit(
|
|
148
|
+
if (!p.why?.trim()) { console.error(`${vendor}: spec patch ${p.op} ${p.path} gives no why`); process.exit(SOURCE_REFUSAL); }
|
|
143
149
|
{
|
|
144
150
|
const s = (p as Patch).source;
|
|
145
151
|
const held = s?.recording !== undefined ? unheldRecording(s.recording, s.quote)
|
|
@@ -156,7 +162,7 @@ if (severalDocuments) {
|
|
|
156
162
|
const { doc: merged, contradictions } = mergeDocuments(doc);
|
|
157
163
|
if (contradictions.length) {
|
|
158
164
|
console.error(`${vendor}: the documents contradict each other; a patch (citing what shows which holds) rules each:\n ${contradictions.join('\n ')}`);
|
|
159
|
-
process.exit(
|
|
165
|
+
process.exit(SOURCE_REFUSAL);
|
|
160
166
|
}
|
|
161
167
|
doc = merged;
|
|
162
168
|
}
|
|
@@ -166,8 +172,9 @@ const out = outAt >= 0 && process.argv[outAt + 1] ? process.argv[outAt + 1]! : j
|
|
|
166
172
|
mkdirSync(out, { recursive: true });
|
|
167
173
|
// A line protocol's surface is its commands and the replies each may answer; its manifest's transitions
|
|
168
174
|
// name commands and cite the RFC.
|
|
169
|
-
if (
|
|
170
|
-
|
|
175
|
+
if (commandBuilders) doc = fromDriverCommandBuilders(doc as DriverCommandBuilders);
|
|
176
|
+
if (lineProtocol || commandTable || commandBuilders) {
|
|
177
|
+
const spec = doc as LineSpec | CommandTableSpec | DocumentCommandTableSpec;
|
|
171
178
|
writeFileSync(join(out, 'surface.gen.json'), `${JSON.stringify({ generatedBy: 'scripts/derive-pack.ts', spec: basename(specPath), ...spec })}\n`);
|
|
172
179
|
const manifestPath = join(PKG, 'src', 'manifest.ts');
|
|
173
180
|
if (existsSync(manifestPath)) {
|
|
@@ -184,7 +191,7 @@ if (lineProtocol || commandTable) {
|
|
|
184
191
|
}
|
|
185
192
|
if (evidence.length) {
|
|
186
193
|
console.error(`${vendor}: ${evidence.length} citation(s) do not hold:\n ${[...new Set(evidence)].join('\n ')}`);
|
|
187
|
-
process.exit(
|
|
194
|
+
process.exit(SOURCE_REFUSAL);
|
|
188
195
|
}
|
|
189
196
|
console.log(`${vendor}: spec ${spec.version}, ${spec.commands.length} commands${'anyCommand' in spec ? `; any command may answer ${spec.anyCommand.join(', ')}` : ''}`);
|
|
190
197
|
process.exit(0);
|
|
@@ -288,7 +295,7 @@ if (existsSync(manifestPath)) {
|
|
|
288
295
|
}
|
|
289
296
|
if (evidence.length) {
|
|
290
297
|
console.error(`${vendor}: ${evidence.length} citation(s) do not hold:\n ${[...new Set(evidence)].join('\n ')}`);
|
|
291
|
-
process.exit(
|
|
298
|
+
process.exit(SOURCE_REFUSAL);
|
|
292
299
|
}
|
|
293
300
|
// every state candidate of a resource the pack models is ruled on: a state field, or named not one
|
|
294
301
|
const unruled = ir.resources.flatMap((r) => {
|
|
@@ -297,7 +304,7 @@ if (existsSync(manifestPath)) {
|
|
|
297
304
|
});
|
|
298
305
|
if (unruled.length) {
|
|
299
306
|
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)`);
|
|
300
|
-
process.exit(
|
|
307
|
+
process.exit(SOURCE_REFUSAL);
|
|
301
308
|
}
|
|
302
309
|
// a resource's refresh scope ("The real-system adapters", What a pack declares) names the list that enumerates it or
|
|
303
310
|
// its retrieve, or says why the vendor offers no read-back; whether every stored resource declares one is
|
|
@@ -368,14 +375,14 @@ if (existsSync(manifestPath)) {
|
|
|
368
375
|
});
|
|
369
376
|
if (scopes.length) {
|
|
370
377
|
console.error(`${vendor}: ${scopes.length} refresh scope(s) do not hold:\n ${scopes.join('\n ')}`);
|
|
371
|
-
process.exit(
|
|
378
|
+
process.exit(SOURCE_REFUSAL);
|
|
372
379
|
}
|
|
373
380
|
// the architecture's mechanical rules over the pack's own code (./p3-rules.ts): a line breaking one records why, or
|
|
374
381
|
// the build stops
|
|
375
382
|
const hits = p3RuleHits(PKG);
|
|
376
383
|
if (hits.length) {
|
|
377
384
|
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 ')}`);
|
|
378
|
-
process.exit(
|
|
385
|
+
process.exit(SOURCE_REFUSAL);
|
|
379
386
|
}
|
|
380
387
|
}
|
|
381
388
|
|
package/src/gate.ts
CHANGED
|
@@ -42,6 +42,39 @@ export function unitsOf(repo: string): string[] {
|
|
|
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: string): string[] {
|
|
47
|
+
const path = join(dir, 'src', 'semantics', 'seed.ts');
|
|
48
|
+
if (!existsSync(path)) return [];
|
|
49
|
+
const text = readFileSync(path, 'utf8');
|
|
50
|
+
if (ts.transpileModule(text, { fileName: path, reportDiagnostics: true }).diagnostics?.some(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error)) return ['handler-contract: root seed has invalid TypeScript syntax'];
|
|
51
|
+
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
|
|
52
|
+
const literal = (node: ts.Expression): boolean => {
|
|
53
|
+
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node) || [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind)) return true;
|
|
54
|
+
if (ts.isPrefixUnaryExpression(node)) return node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand);
|
|
55
|
+
if (ts.isArrayLiteralExpression(node)) return node.elements.every(element => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && literal(element));
|
|
56
|
+
return ts.isObjectLiteralExpression(node) && node.properties.every(property => ts.isPropertyAssignment(property)
|
|
57
|
+
&& !ts.isComputedPropertyName(property.name) && literal(property.initializer));
|
|
58
|
+
};
|
|
59
|
+
let imports = 0, seeds = 0;
|
|
60
|
+
for (const statement of source.statements) {
|
|
61
|
+
if (ts.isImportDeclaration(statement)) {
|
|
62
|
+
const clause = statement.importClause, bindings = clause?.namedBindings;
|
|
63
|
+
if (!clause?.isTypeOnly || clause.name || !bindings || !ts.isNamedImports(bindings)
|
|
64
|
+
|| bindings.elements.length !== 1 || bindings.elements[0]!.name.text !== 'SeedCall' || bindings.elements[0]!.propertyName
|
|
65
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== '@volter/world-core') return ['handler-contract: root seed imports only the SeedCall type from world-core'];
|
|
66
|
+
imports++; continue;
|
|
67
|
+
}
|
|
68
|
+
if (!ts.isVariableStatement(statement) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
69
|
+
|| !(statement.declarationList.flags & ts.NodeFlags.Const) || statement.declarationList.declarations.length !== 1) return ['handler-contract: root seed exports only const seed: SeedCall[] literal data'];
|
|
70
|
+
const declaration = statement.declarationList.declarations[0]!;
|
|
71
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'seed' || !declaration.type || !ts.isArrayTypeNode(declaration.type)
|
|
72
|
+
|| !ts.isTypeReferenceNode(declaration.type.elementType) || declaration.type.elementType.typeName.getText(source) !== 'SeedCall' || declaration.type.elementType.typeArguments?.length
|
|
73
|
+
|| !declaration.initializer || !ts.isArrayLiteralExpression(declaration.initializer) || !literal(declaration.initializer)) return ['handler-contract: root seed must be literal SeedCall[] data'];
|
|
74
|
+
seeds++;
|
|
75
|
+
}
|
|
76
|
+
return imports === 1 && seeds === 1 ? [] : ['handler-contract: root seed requires one SeedCall type import and one seed export'];
|
|
77
|
+
}
|
|
45
78
|
/** 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
79
|
function ignored(dir: string, names: string[]): Set<string> {
|
|
47
80
|
if (!names.length) return new Set();
|
|
@@ -55,15 +88,16 @@ export function laneRootFailures(dir: string): string[] {
|
|
|
55
88
|
const skip = ignored(dir, stray);
|
|
56
89
|
const outside = [
|
|
57
90
|
...stray.filter((n) => !skip.has(n)),
|
|
58
|
-
...(existsSync(join(dir, 'spec')) ? readdirSync(join(dir, 'spec')).filter((n) => !['sources.json', 'SOURCE.md', 'README.md'].includes(n)).map((n) => `spec/${n}`) : []),
|
|
59
|
-
...(existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((n) => !ROOT_SRC.has(n) && !n.endsWith('.d.ts') && n !== 'semantics').map((n) => `src/${n}`) : []),
|
|
91
|
+
...(existsSync(join(dir, 'spec')) ? readdirSync(join(dir, 'spec')).filter((n) => !isPackArtifact(n) && !['sources.json', 'SOURCE.md', 'README.md'].includes(n)).map((n) => `spec/${n}`) : []),
|
|
92
|
+
...(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}`) : []),
|
|
60
93
|
// what the lanes share, and nothing else (architecture, "Pack layout": a vendor whose every API is a lane)
|
|
61
|
-
...(existsSync(join(dir, 'src', 'semantics')) ? readdirSync(join(dir, 'src', 'semantics')).filter((n) => n
|
|
94
|
+
...(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}`) : []),
|
|
62
95
|
];
|
|
63
96
|
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` : ''}`] : [];
|
|
64
97
|
// the vendor's front is declared and the kernel serves it: a manifest, and a fetch that is createVendorFetch over it
|
|
65
98
|
const src = (n: string): string => (existsSync(join(dir, 'src', n)) ? readFileSync(join(dir, 'src', n), 'utf8') : '');
|
|
66
99
|
failures.push(...unpublishedUnitFiles(dir).map((f) => `layout: ${f}`));
|
|
100
|
+
failures.push(...rootSeedFailures(dir));
|
|
67
101
|
if (!src('manifest.ts')) failures.push("layout: no src/manifest.ts: a vendor of lanes declares its lanes' routes, discovery and descriptor");
|
|
68
102
|
if (!/\bcreateVendorFetch\(/.test(src('fetch.ts'))) failures.push('layout: src/fetch.ts builds no createVendorFetch: the kernel routes a vendor\'s lanes from its manifest, nothing by hand');
|
|
69
103
|
// the root's own files hold to the pack contract as a unit's do (the grade's kernel-contract)
|
|
@@ -85,9 +119,9 @@ export function laneRootFailures(dir: string): string[] {
|
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
/** Each unit's blocking form failures (empty when it may be in the repository). */
|
|
88
|
-
export async function p3Gate(units: string[], opts: Pick<GradeOptions, 'onCriterion'> = {}): Promise<Array<{ unit: string; failures: string[] }>> {
|
|
122
|
+
export async function p3Gate(units: string[], opts: Pick<GradeOptions, 'onCriterion'> = {}): Promise<Array<{ unit: string; failures: string[]; cacheable?: boolean }>> {
|
|
89
123
|
return withCheckoutFiles(async () => {
|
|
90
|
-
const out: Array<{ unit: string; failures: string[] }> = [];
|
|
124
|
+
const out: Array<{ unit: string; failures: string[]; cacheable?: boolean }> = [];
|
|
91
125
|
for (const unit of units) {
|
|
92
126
|
// a vendor whose every API is a lane has no spec of its own: its lanes are graded, and its root is only the vendor's
|
|
93
127
|
// front (architecture, "Pack layout", "Other wires: lanes")
|
|
@@ -95,7 +129,7 @@ export async function p3Gate(units: string[], opts: Pick<GradeOptions, 'onCriter
|
|
|
95
129
|
if (lanesOnly || !existsSync(join(unit, 'src', 'manifest.ts'))) { out.push({ unit, failures: laneRootFailures(unit) }); continue; }
|
|
96
130
|
const grades = await grade(unit, { static: true, sections: ['form'], ...opts });
|
|
97
131
|
const form = Object.values(grades).flatMap((g) => g.criteria).filter((c) => !c.pass);
|
|
98
|
-
out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`) });
|
|
132
|
+
out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`), cacheable: Object.values(grades).every(g => g.criteria.every(c => c.cacheable !== false)) });
|
|
99
133
|
}
|
|
100
134
|
return out;
|
|
101
135
|
});
|
package/src/grade.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { unitAt } from './unit.ts';
|
|
|
10
10
|
/** The latest version of each standard. A new protocol, or a revision, replaces or joins this list. */
|
|
11
11
|
export const STANDARDS: Standard[] = [protocol3];
|
|
12
12
|
|
|
13
|
-
export interface CriterionGrade { id: string; section: string; asks: string; pass: boolean; reason: string }
|
|
13
|
+
export interface CriterionGrade { id: string; section: string; asks: string; pass: boolean; reason: string; cacheable?: boolean }
|
|
14
14
|
export interface StandardGrade { grade: number; met: number; total: number; criteria: CriterionGrade[]; planned: string[] }
|
|
15
15
|
|
|
16
16
|
export type GradeOptions = {
|
|
@@ -40,7 +40,7 @@ export async function grade(dir: string, opts: GradeOptions = {}): Promise<Recor
|
|
|
40
40
|
if (opts.sections && !opts.sections.includes(c.section)) continue;
|
|
41
41
|
opts.onCriterion?.(name, c.id);
|
|
42
42
|
let result;
|
|
43
|
-
try { result = await c.check(unit); } catch (e) { result = { pass: false, reason: `the check threw: ${(e as Error).message.slice(0, 160)}` }; }
|
|
43
|
+
try { result = await c.check(unit); } catch (e) { result = { pass: false, cacheable: false, reason: `the check threw: ${(e as Error).message.slice(0, 160)}` }; }
|
|
44
44
|
opts.onCriterion?.(name, c.id, result);
|
|
45
45
|
criteria.push({ id: c.id, section: c.section, asks: c.asks, ...result });
|
|
46
46
|
}
|
package/src/pack-facts.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type PackFacts = {
|
|
|
37
37
|
/** The Prisma driver adapter this database twin is reached through (see TwinPack.prismaAdapter). */
|
|
38
38
|
prismaAdapter?: { adapter: string; export: string; urlEnv: string };
|
|
39
39
|
/** The managed-infrastructure kind this pack is the World's backing for (architecture.md, "The descriptor"). */
|
|
40
|
-
managedService?: { kind: 'redis' | 'mongodb'; scheme: string; note: string };
|
|
40
|
+
managedService?: { kind: 'redis' | 'mongodb' | 'clickhouse'; scheme: string; note: string; configurationArg?: '--configuration'; originalInputs?: { version: 1; manifestArg: '--original-inputs'; rootArg: '--original-root'; digestArg: '--original-inputs-sha256'; paths: string[] } };
|
|
41
41
|
/** The World's managed database the runtime binds the twin to (see TwinPack.managedDatabase). */
|
|
42
42
|
managedDatabase?: { kind: 'postgres'; arg: string; note: string };
|
|
43
43
|
/** Listeners the twin opens beside its main port (see TwinPack.sidePorts). */
|
|
@@ -191,10 +191,17 @@ export async function compilePackFacts(dirs: Array<{ vendor: string; dir: string
|
|
|
191
191
|
facts.managedDatabase = pack.managedDatabase;
|
|
192
192
|
}
|
|
193
193
|
if (pack.managedService !== undefined) {
|
|
194
|
-
const ms = pack.managedService as { kind?: unknown; scheme?: unknown; note?: unknown };
|
|
195
|
-
if (ms.kind !== 'redis' && ms.kind !== 'mongodb') throw new Error(`pack-facts: ${vendor} declares managedService.kind "${String(ms.kind)}"; the managed-infrastructure kinds a twin backs are redis and
|
|
194
|
+
const ms = pack.managedService as { kind?: unknown; scheme?: unknown; note?: unknown; configurationArg?: unknown; originalInputs?: unknown };
|
|
195
|
+
if (ms.kind !== 'redis' && ms.kind !== 'mongodb' && ms.kind !== 'clickhouse') throw new Error(`pack-facts: ${vendor} declares managedService.kind "${String(ms.kind)}"; the managed-infrastructure kinds a twin backs are redis, mongodb and clickhouse`);
|
|
196
196
|
if (typeof ms.scheme !== 'string' || !/^[a-z][a-z0-9+.-]*$/.test(ms.scheme)) throw new Error(`pack-facts: ${vendor} declares managedService.scheme "${String(ms.scheme)}"; it is the published URL's scheme`);
|
|
197
197
|
if (typeof ms.note !== 'string' || ms.note.trim() === '') throw new Error(`pack-facts: ${vendor} declares managedService with no note`);
|
|
198
|
+
if (ms.configurationArg !== undefined && ms.configurationArg !== '--configuration') throw new Error(`pack-facts: ${vendor} has an unsupported managed operator-configuration flag`);
|
|
199
|
+
if (ms.originalInputs !== undefined) {
|
|
200
|
+
const input = ms.originalInputs as { version?: unknown; manifestArg?: unknown; rootArg?: unknown; digestArg?: unknown; paths?: unknown };
|
|
201
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || input.version !== 1 || input.manifestArg !== '--original-inputs' || input.rootArg !== '--original-root' || input.digestArg !== '--original-inputs-sha256'
|
|
202
|
+
|| !Array.isArray(input.paths) || !input.paths.length || new Set(input.paths).size !== input.paths.length
|
|
203
|
+
|| input.paths.some(path => typeof path !== 'string' || !path.startsWith('/') || path === '/' || path.slice(1).split('/').some(part => !part || part === '.' || part === '..' || /[\\\0\r\n]/.test(part)))) throw new Error(`pack-facts: ${vendor} has an invalid original-input consumer`);
|
|
204
|
+
}
|
|
198
205
|
if (pack.bin === undefined) throw new Error(`pack-facts: ${vendor} declares managedService with no bin; the managed infrastructure starts the pack by its bin`);
|
|
199
206
|
const other = Object.entries(packs).find(([, f]) => f.managedService?.kind === ms.kind);
|
|
200
207
|
if (other) throw new Error(`pack-facts: ${vendor} and ${other[0]} both declare managedService.kind "${ms.kind}"; one pack backs a kind`);
|
package/src/pack-files.ts
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: string): boolean {
|
|
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
|
}
|