@volter/twin-standard 1.0.88 → 1.0.90
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.js +1 -1
- package/dist/src/conformance.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +3 -1
- package/dist/src/walk/behavior-journey.js +15 -8
- package/dist/src/walk/pack-directory.d.ts +1 -1
- package/dist/src/walk/pack-directory.js +2 -2
- package/dist/src/walk/vendor-fetch.d.ts +3 -3
- package/dist/src/walk/vendor-fetch.js +6 -6
- package/package.json +2 -2
- package/src/assessment.ts +1 -1
- package/src/conformance.ts +1 -1
- package/src/walk/behavior-journey.ts +15 -8
- package/src/walk/pack-directory.ts +2 -2
- package/src/walk/vendor-fetch.ts +6 -6
package/dist/src/assessment.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function assessPack(packDir, options = {}) {
|
|
|
36
36
|
const units = [packDir, ...lanes.map((name) => join(packDir, name))];
|
|
37
37
|
if (packDir.split(/[\\/]/).at(-1) !== vendor)
|
|
38
38
|
throw new Error('Place the artifact in a directory named for its vendor before assessment');
|
|
39
|
-
usePacks(join(packDir, '..'));
|
|
39
|
+
usePacks(join(packDir, '..'), { dependenciesFrom: packDir });
|
|
40
40
|
const gates = await p3Gate(units);
|
|
41
41
|
const life = expandFills(read(join(packDir, 'journeys/customer-life.json')));
|
|
42
42
|
if (!Array.isArray(life.steps) || !life.steps.length)
|
package/dist/src/conformance.js
CHANGED
|
@@ -356,7 +356,7 @@ async function lifeServed(pack) {
|
|
|
356
356
|
if (!existsSync(lifePath))
|
|
357
357
|
return ['journeys/customer-life.json is missing'];
|
|
358
358
|
const { inMemoryWorld, usePacks, walk } = await import("./walk/behavior-journey.js");
|
|
359
|
-
usePacks(dirname(pack));
|
|
359
|
+
usePacks(dirname(pack), { dependenciesFrom: pack });
|
|
360
360
|
const env = globalThis.process.env;
|
|
361
361
|
const served = async (root, options) => {
|
|
362
362
|
const before = env.VOLTER_WORLD_BOOT_ID;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type TwinStream } from '@volter/world-core/runtime';
|
|
2
2
|
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type RespStep } from './journey-kit.js';
|
|
3
|
-
export declare function usePacks(dir: string
|
|
3
|
+
export declare function usePacks(dir: string, options?: {
|
|
4
|
+
dependenciesFrom?: string;
|
|
5
|
+
}): void;
|
|
4
6
|
/** The walk's World: the twin's own code on its in-memory World and blob stores (the backends a serverless
|
|
5
7
|
* entry runs on), so a walk of any length is state changes in memory and never touches the disk; the World
|
|
6
8
|
* clock, pinned to 2026-01-01, is world state in the same store, and the World is sealed. Each root is a fresh key
|
|
@@ -13,7 +13,7 @@ import { gzipSync } from 'node:zlib';
|
|
|
13
13
|
import { parse, visit } from 'graphql';
|
|
14
14
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { databaseSession, getActiveWorldStore, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActiveWorldStore, setDeliveryObserver, setInProcessTwins } from '@volter/world-core/runtime';
|
|
16
|
+
import { databaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessTwins } from '@volter/world-core/runtime';
|
|
17
17
|
import { createHostTable } from '@volter/world-core/host-routing';
|
|
18
18
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
19
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";
|
|
@@ -22,6 +22,8 @@ import { walkPackDirectory } from "./pack-directory.js";
|
|
|
22
22
|
import { xmlMembers } from "./xml-view.js";
|
|
23
23
|
/** The pack repository the walk finds a vendor's pack in, set by the caller before its first walk. */
|
|
24
24
|
let packsRoot;
|
|
25
|
+
/** Installed companions resolve from the selected pack, including its workspace-local dependencies. */
|
|
26
|
+
let dependenciesRoot;
|
|
25
27
|
/** Where a socket step opens: an absolute `ws://` or `wss://` url on its own host, a path on the life's host otherwise. */
|
|
26
28
|
function socketAt(open, host) {
|
|
27
29
|
if (!/^wss?:\/\//.test(open))
|
|
@@ -34,8 +36,9 @@ let walkFacts = baselineFacts;
|
|
|
34
36
|
let walkHosts = createHostTable(walkFacts).hosts;
|
|
35
37
|
const directories = new Map();
|
|
36
38
|
const installedFacts = new Set();
|
|
37
|
-
export function usePacks(dir) {
|
|
39
|
+
export function usePacks(dir, options = {}) {
|
|
38
40
|
packsRoot = dir;
|
|
41
|
+
dependenciesRoot = options.dependenciesFrom ?? dir;
|
|
39
42
|
const installed = {};
|
|
40
43
|
for (const name of readdirSync(dir)) {
|
|
41
44
|
const file = join(dir, name, 'generated/pack-facts.json');
|
|
@@ -53,9 +56,10 @@ export function usePacks(dir) {
|
|
|
53
56
|
}
|
|
54
57
|
const packsDir = () => { if (packsRoot === undefined)
|
|
55
58
|
throw new Error('usePacks(<packs dir>) first'); return packsRoot; };
|
|
59
|
+
const dependenciesDir = () => dependenciesRoot ?? packsDir();
|
|
56
60
|
function directoryFor(vendor) {
|
|
57
61
|
if (!directories.has(vendor)) {
|
|
58
|
-
const directory = walkPackDirectory(packsDir(), vendor);
|
|
62
|
+
const directory = walkPackDirectory(packsDir(), vendor, dependenciesDir());
|
|
59
63
|
directories.set(vendor, directory);
|
|
60
64
|
if (directory) {
|
|
61
65
|
const name = vendor.split('/')[0];
|
|
@@ -80,6 +84,8 @@ function directoryFor(vendor) {
|
|
|
80
84
|
export function inMemoryWorld(label) {
|
|
81
85
|
const previousStore = setActiveWorldStore(new MemoryWorldStore());
|
|
82
86
|
const previousBlobs = setActiveBlobStore(new MemoryBlobStore());
|
|
87
|
+
const previousAssets = getActivePackAssets();
|
|
88
|
+
setActivePackAssets(new LocalPackAssets((vendor) => walkPackDirectory(packsDir(), vendor, dependenciesDir())));
|
|
83
89
|
const previousClock = process.env.TWIN_WORLD_CLOCK_FILE;
|
|
84
90
|
// sealed: nothing a walk does reaches a host outside the World (a webhook to a customer's server is refused, not sent)
|
|
85
91
|
const previousEgress = process.env.VOLTER_TWIN_STRICT_EGRESS;
|
|
@@ -93,6 +99,7 @@ export function inMemoryWorld(label) {
|
|
|
93
99
|
restore: () => {
|
|
94
100
|
setActiveWorldStore(previousStore);
|
|
95
101
|
setActiveBlobStore(previousBlobs);
|
|
102
|
+
setActivePackAssets(previousAssets);
|
|
96
103
|
if (previousClock === undefined)
|
|
97
104
|
delete process.env.TWIN_WORLD_CLOCK_FILE;
|
|
98
105
|
else
|
|
@@ -119,7 +126,7 @@ function defaultType(body) {
|
|
|
119
126
|
return 'text/plain;charset=UTF-8';
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
|
-
const vendorFetch = (vendor) => vendorFetchFrom(packsDir(), vendor);
|
|
129
|
+
const vendorFetch = (vendor) => vendorFetchFrom(packsDir(), vendor, dependenciesDir());
|
|
123
130
|
const shapes = new Map();
|
|
124
131
|
const shapeOf = (vendor) => { if (!shapes.has(vendor))
|
|
125
132
|
shapes.set(vendor, shapeChecker(directoryFor(vendor))); return shapes.get(vendor); };
|
|
@@ -218,7 +225,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
218
225
|
return {};
|
|
219
226
|
}
|
|
220
227
|
const factory = await vendorFetch(journey.vendor);
|
|
221
|
-
const views = await vendorAnswerViews(packsDir(), journey.vendor);
|
|
228
|
+
const views = await vendorAnswerViews(packsDir(), journey.vendor, dependenciesDir());
|
|
222
229
|
if (!factory) {
|
|
223
230
|
collect.push({ step: '(setup)', beat: 'ADAPTER', detail: `no create<Name>TwinFetch adapter enrolled for ${journey.vendor}` });
|
|
224
231
|
return {};
|
|
@@ -370,7 +377,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
370
377
|
}
|
|
371
378
|
const viaServed = served?.stream;
|
|
372
379
|
if (!viaServed)
|
|
373
|
-
streams ??= await vendorStream(packsDir(), journey.vendor);
|
|
380
|
+
streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
374
381
|
if (!viaServed && !streams) {
|
|
375
382
|
collect.push({ step: step.name, beat: 'ADAPTER', detail: `no create<Name>TwinStream adapter enrolled for ${journey.vendor}` });
|
|
376
383
|
return;
|
|
@@ -437,7 +444,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
437
444
|
if (served?.stream && Object.keys(step.connect).length)
|
|
438
445
|
throw new Error('a served RESP walk configures its server, not the client connection');
|
|
439
446
|
if (!served?.stream)
|
|
440
|
-
streams ??= await vendorStream(packsDir(), journey.vendor);
|
|
447
|
+
streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
441
448
|
const factory = served?.stream ?? (respStream ??= streams?.(root, step.connect));
|
|
442
449
|
if (!factory)
|
|
443
450
|
throw new Error('no byte-stream adapter');
|
|
@@ -511,7 +518,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
511
518
|
let body;
|
|
512
519
|
if (step.signIn) {
|
|
513
520
|
if (!served?.stream)
|
|
514
|
-
streams ??= await vendorStream(packsDir(), journey.vendor);
|
|
521
|
+
streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
515
522
|
const factory = served?.stream ?? streams?.(root, {});
|
|
516
523
|
if (!factory)
|
|
517
524
|
throw new Error(`no create<Name>TwinStream adapter enrolled for ${journey.vendor}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function walkPackDirectory(from: string, vendor: string): string | undefined;
|
|
1
|
+
export declare function walkPackDirectory(from: string, vendor: string, dependenciesFrom?: string): string | undefined;
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { installedVendorPacks, isDerivedPack, packVendor } from '@volter/world-core/runtime';
|
|
5
|
-
export function walkPackDirectory(from, vendor) {
|
|
5
|
+
export function walkPackDirectory(from, vendor, dependenciesFrom = from) {
|
|
6
6
|
if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/.test(vendor))
|
|
7
7
|
return undefined;
|
|
8
8
|
const [name, lane] = vendor.split('/');
|
|
9
9
|
const sibling = join(from, name);
|
|
10
10
|
const root = isDerivedPack(sibling) && packVendor(sibling) === name
|
|
11
11
|
? sibling
|
|
12
|
-
: installedVendorPacks(
|
|
12
|
+
: installedVendorPacks(dependenciesFrom, { vendors: [name] }).get(name);
|
|
13
13
|
const unit = root && (lane ? join(root, lane) : root);
|
|
14
14
|
return unit && existsSync(join(unit, 'src', 'index.ts')) ? unit : undefined;
|
|
15
15
|
}
|
|
@@ -7,14 +7,14 @@ export type VendorFetchFactory = (root: string, options?: {
|
|
|
7
7
|
scenarioPath?: string;
|
|
8
8
|
application?: ApplicationStandIn;
|
|
9
9
|
}) => (request: Request) => Response | Promise<Response>;
|
|
10
|
-
export declare function vendorFetch(twinDir: string, vendor: string): Promise<VendorFetchFactory | undefined>;
|
|
10
|
+
export declare function vendorFetch(twinDir: string, vendor: string, dependenciesFrom?: string): Promise<VendorFetchFactory | undefined>;
|
|
11
11
|
/** Named views of an answer: what a journey step's `view` asserts against. The kit's own: `jwt:<field>`, the payload of the
|
|
12
12
|
* JSON Web Token at that field of the answer, decoded as the application verifying it reads it (a session token's
|
|
13
13
|
* claims). A pack's own (`answerViews` in its index) beside them. */
|
|
14
14
|
export type AnswerViews = Record<string, (body: unknown) => unknown>;
|
|
15
|
-
export declare function vendorAnswerViews(twinDir: string, vendor: string): Promise<AnswerViews>;
|
|
15
|
+
export declare function vendorAnswerViews(twinDir: string, vendor: string, dependenciesFrom?: string): Promise<AnswerViews>;
|
|
16
16
|
/** A vendor's byte-stream wire (a line protocol), in process: the pack's `create<Name>TwinStream`, what a
|
|
17
17
|
* hosted World hands a WebSocket's bytes. The walk drives it for a journey's line steps. */
|
|
18
18
|
export type VendorStreamFactory = (root: string, options?: Record<string, unknown>) => TwinStream;
|
|
19
|
-
export declare function vendorStream(twinDir: string, vendor: string): Promise<VendorStreamFactory | undefined>;
|
|
19
|
+
export declare function vendorStream(twinDir: string, vendor: string, dependenciesFrom?: string): Promise<VendorStreamFactory | undefined>;
|
|
20
20
|
export {};
|
|
@@ -13,8 +13,8 @@ import { join } from 'node:path';
|
|
|
13
13
|
import { startPgliteHost } from "./walk-database.js";
|
|
14
14
|
import { worldNow } from '@volter/world-core/runtime';
|
|
15
15
|
import { walkPackDirectory } from "./pack-directory.js";
|
|
16
|
-
export async function vendorFetch(twinDir, vendor) {
|
|
17
|
-
const directory = walkPackDirectory(twinDir, vendor);
|
|
16
|
+
export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
17
|
+
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
18
18
|
if (!directory)
|
|
19
19
|
return undefined;
|
|
20
20
|
let mod;
|
|
@@ -59,9 +59,9 @@ const jwtView = (field) => (body) => {
|
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
-
export async function vendorAnswerViews(twinDir, vendor) {
|
|
62
|
+
export async function vendorAnswerViews(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
63
63
|
let own = {};
|
|
64
|
-
const directory = walkPackDirectory(twinDir, vendor);
|
|
64
|
+
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
65
65
|
try {
|
|
66
66
|
if (directory)
|
|
67
67
|
own = (await import(__rewriteRelativeImportExtension(join(directory, 'src', 'index.ts')))).answerViews ?? {};
|
|
@@ -69,8 +69,8 @@ export async function vendorAnswerViews(twinDir, vendor) {
|
|
|
69
69
|
catch { /* none of its own */ }
|
|
70
70
|
return new Proxy(own, { get: (views, name) => (typeof name === 'string' && name.startsWith('jwt:') ? jwtView(name.slice(4)) : views[name]) });
|
|
71
71
|
}
|
|
72
|
-
export async function vendorStream(twinDir, vendor) {
|
|
73
|
-
const directory = walkPackDirectory(twinDir, vendor);
|
|
72
|
+
export async function vendorStream(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
73
|
+
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
74
74
|
if (!directory)
|
|
75
75
|
return undefined;
|
|
76
76
|
let mod;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-standard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.90",
|
|
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.98",
|
|
79
79
|
"@volter/world-runtime": "*",
|
|
80
80
|
"playwright": "^1.63.0"
|
|
81
81
|
},
|
package/src/assessment.ts
CHANGED
|
@@ -27,7 +27,7 @@ export async function assessPack(packDir: string, options: AssessmentOptions = {
|
|
|
27
27
|
const lanes = readdirSync(packDir).filter((name) => existsSync(join(packDir, name, 'src/manifest.ts')) && !existsSync(join(packDir, name, 'package.json')));
|
|
28
28
|
const units = [packDir, ...lanes.map((name) => join(packDir, name))];
|
|
29
29
|
if (packDir.split(/[\\/]/).at(-1) !== vendor) throw new Error('Place the artifact in a directory named for its vendor before assessment');
|
|
30
|
-
usePacks(join(packDir, '..'));
|
|
30
|
+
usePacks(join(packDir, '..'), { dependenciesFrom: packDir });
|
|
31
31
|
const gates = await p3Gate(units);
|
|
32
32
|
const life = expandFills(read(join(packDir, 'journeys/customer-life.json'))) as Journey;
|
|
33
33
|
if (!Array.isArray(life.steps) || !life.steps.length) throw new Error('Assessment needs a nonempty customer journey');
|
package/src/conformance.ts
CHANGED
|
@@ -318,7 +318,7 @@ async function lifeServed(pack: string): Promise<string[]> {
|
|
|
318
318
|
const lifePath = join(pack, 'journeys', 'customer-life.json');
|
|
319
319
|
if (!existsSync(lifePath)) return ['journeys/customer-life.json is missing'];
|
|
320
320
|
const { inMemoryWorld, usePacks, walk } = await import('./walk/behavior-journey.ts');
|
|
321
|
-
usePacks(dirname(pack));
|
|
321
|
+
usePacks(dirname(pack), { dependenciesFrom: pack });
|
|
322
322
|
const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env;
|
|
323
323
|
const served = async (root: string, options: { scenarioPath?: string }) => {
|
|
324
324
|
const before = env.VOLTER_WORLD_BOOT_ID;
|
|
@@ -14,7 +14,7 @@ import { gzipSync } from 'node:zlib';
|
|
|
14
14
|
import { parse, visit } from 'graphql';
|
|
15
15
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
|
-
import { databaseSession, type DatabaseSession, getActiveWorldStore, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActiveWorldStore, setDeliveryObserver, setInProcessTwins, type InProcessTwin, type TwinStream, type TwinStreamConnection } from '@volter/world-core/runtime';
|
|
17
|
+
import { databaseSession, type DatabaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessTwins, type InProcessTwin, type TwinStream, type TwinStreamConnection } from '@volter/world-core/runtime';
|
|
18
18
|
import { createHostTable, type HostFacts } from '@volter/world-core/host-routing';
|
|
19
19
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
20
20
|
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type SocketStep, type SqlStep, type RespStep, type MysqlStep, isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from './journey-kit.ts';
|
|
@@ -24,6 +24,8 @@ import { xmlMembers } from './xml-view.ts';
|
|
|
24
24
|
|
|
25
25
|
/** The pack repository the walk finds a vendor's pack in, set by the caller before its first walk. */
|
|
26
26
|
let packsRoot: string | undefined;
|
|
27
|
+
/** Installed companions resolve from the selected pack, including its workspace-local dependencies. */
|
|
28
|
+
let dependenciesRoot: string | undefined;
|
|
27
29
|
/** Where a socket step opens: an absolute `ws://` or `wss://` url on its own host, a path on the life's host otherwise. */
|
|
28
30
|
function socketAt(open: string, host: string): { host: string; path: string } {
|
|
29
31
|
if (!/^wss?:\/\//.test(open)) return { host, path: open };
|
|
@@ -36,8 +38,9 @@ let walkFacts: HostFacts = baselineFacts;
|
|
|
36
38
|
let walkHosts = createHostTable(walkFacts).hosts;
|
|
37
39
|
const directories = new Map<string, string | undefined>();
|
|
38
40
|
const installedFacts = new Set<string>();
|
|
39
|
-
export function usePacks(dir: string): void {
|
|
41
|
+
export function usePacks(dir: string, options: { dependenciesFrom?: string } = {}): void {
|
|
40
42
|
packsRoot = dir;
|
|
43
|
+
dependenciesRoot = options.dependenciesFrom ?? dir;
|
|
41
44
|
const installed: Record<string, HostFacts[string]> = {};
|
|
42
45
|
for (const name of readdirSync(dir)) {
|
|
43
46
|
const file = join(dir, name, 'generated/pack-facts.json');
|
|
@@ -51,9 +54,10 @@ export function usePacks(dir: string): void {
|
|
|
51
54
|
directories.clear();
|
|
52
55
|
}
|
|
53
56
|
const packsDir = (): string => { if (packsRoot === undefined) throw new Error('usePacks(<packs dir>) first'); return packsRoot; };
|
|
57
|
+
const dependenciesDir = (): string => dependenciesRoot ?? packsDir();
|
|
54
58
|
function directoryFor(vendor: string): string {
|
|
55
59
|
if (!directories.has(vendor)) {
|
|
56
|
-
const directory = walkPackDirectory(packsDir(), vendor);
|
|
60
|
+
const directory = walkPackDirectory(packsDir(), vendor, dependenciesDir());
|
|
57
61
|
directories.set(vendor, directory);
|
|
58
62
|
if (directory) {
|
|
59
63
|
const name = vendor.split('/')[0]!;
|
|
@@ -79,6 +83,8 @@ function directoryFor(vendor: string): string {
|
|
|
79
83
|
export function inMemoryWorld(label: string): { root: () => string; restore: () => void } {
|
|
80
84
|
const previousStore = setActiveWorldStore(new MemoryWorldStore());
|
|
81
85
|
const previousBlobs = setActiveBlobStore(new MemoryBlobStore());
|
|
86
|
+
const previousAssets = getActivePackAssets();
|
|
87
|
+
setActivePackAssets(new LocalPackAssets((vendor) => walkPackDirectory(packsDir(), vendor, dependenciesDir())));
|
|
82
88
|
const previousClock = process.env.TWIN_WORLD_CLOCK_FILE;
|
|
83
89
|
// sealed: nothing a walk does reaches a host outside the World (a webhook to a customer's server is refused, not sent)
|
|
84
90
|
const previousEgress = process.env.VOLTER_TWIN_STRICT_EGRESS;
|
|
@@ -92,6 +98,7 @@ export function inMemoryWorld(label: string): { root: () => string; restore: ()
|
|
|
92
98
|
restore: () => {
|
|
93
99
|
setActiveWorldStore(previousStore);
|
|
94
100
|
setActiveBlobStore(previousBlobs);
|
|
101
|
+
setActivePackAssets(previousAssets);
|
|
95
102
|
if (previousClock === undefined) delete process.env.TWIN_WORLD_CLOCK_FILE; else process.env.TWIN_WORLD_CLOCK_FILE = previousClock;
|
|
96
103
|
if (previousEgress === undefined) delete process.env.VOLTER_TWIN_STRICT_EGRESS; else process.env.VOLTER_TWIN_STRICT_EGRESS = previousEgress;
|
|
97
104
|
},
|
|
@@ -107,7 +114,7 @@ function defaultType(body: unknown): string {
|
|
|
107
114
|
if (typeof body !== 'string') return 'application/json';
|
|
108
115
|
try { parseExactJson(body); return 'application/json'; } catch { return 'text/plain;charset=UTF-8'; }
|
|
109
116
|
}
|
|
110
|
-
const vendorFetch = (vendor: string) => vendorFetchFrom(packsDir(), vendor);
|
|
117
|
+
const vendorFetch = (vendor: string) => vendorFetchFrom(packsDir(), vendor, dependenciesDir());
|
|
111
118
|
const shapes = new Map<string, Promise<ShapeChecker | undefined>>();
|
|
112
119
|
const shapeOf = (vendor: string): Promise<ShapeChecker | undefined> => { if (!shapes.has(vendor)) shapes.set(vendor, shapeChecker(directoryFor(vendor))); return shapes.get(vendor)!; };
|
|
113
120
|
|
|
@@ -210,7 +217,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
210
217
|
// the life's host is a hostname (the vendor's, `api.x.ai`); a URL there would put a second scheme in every request
|
|
211
218
|
if (journey.host !== undefined && !/^[A-Za-z0-9.-]+(:\d+)?$/.test(journey.host)) { collect.push({ step: '(setup)', beat: 'HOST', detail: `the life's host "${journey.host}" is no hostname: write the vendor's host alone (api.example.com), no scheme or path` }); return {}; }
|
|
212
219
|
const factory = await vendorFetch(journey.vendor);
|
|
213
|
-
const views = await vendorAnswerViews(packsDir(), journey.vendor);
|
|
220
|
+
const views = await vendorAnswerViews(packsDir(), journey.vendor, dependenciesDir());
|
|
214
221
|
if (!factory) { collect.push({ step: '(setup)', beat: 'ADAPTER', detail: `no create<Name>TwinFetch adapter enrolled for ${journey.vendor}` }); return {}; }
|
|
215
222
|
// the life's companions (`companions`): the packs its vendor calls, each over this walk's root, on the same World and
|
|
216
223
|
// clock; reached in process by the vendor's ctx.vendorFetch and by a step to a URL their host rules claim
|
|
@@ -323,7 +330,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
323
330
|
// never a way back to the in-process stream (which would pass a listener wired apart from the World)
|
|
324
331
|
if (served?.stream && Object.keys(step.connect).length) { collect.push({ step: step.name, beat: 'CONNECTION', detail: `a served walk opens "${step.on}" as the application does, over TCP to the listener; connect options (${Object.keys(step.connect).join(', ')}) configure only the in-process stream` }); return; }
|
|
325
332
|
const viaServed = served?.stream;
|
|
326
|
-
if (!viaServed) streams ??= await vendorStream(packsDir(), journey.vendor);
|
|
333
|
+
if (!viaServed) streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
327
334
|
if (!viaServed && !streams) { collect.push({ step: step.name, beat: 'ADAPTER', detail: `no create<Name>TwinStream adapter enrolled for ${journey.vendor}` }); return; }
|
|
328
335
|
const opened: Connection = { conn: undefined as unknown as TwinStreamConnection, out: '', closed: false, shape: {} };
|
|
329
336
|
opened.conn = (viaServed ?? streams!(root, step.connect))({ write: (bytes) => { opened.out += decoder.decode(bytes); }, end: () => { opened.closed = true; } }, '127.0.0.1');
|
|
@@ -367,7 +374,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
367
374
|
try {
|
|
368
375
|
if (step.connect) {
|
|
369
376
|
if (served?.stream && Object.keys(step.connect).length) throw new Error('a served RESP walk configures its server, not the client connection');
|
|
370
|
-
if (!served?.stream) streams ??= await vendorStream(packsDir(), journey.vendor);
|
|
377
|
+
if (!served?.stream) streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
371
378
|
const factory = served?.stream ?? (respStream ??= streams?.(root, step.connect)); if (!factory) throw new Error('no byte-stream adapter');
|
|
372
379
|
const c = { conn: undefined as unknown as TwinStreamConnection, out: '', closed: false };
|
|
373
380
|
c.conn = factory({ write: (b) => { c.out += redis.binaryText(b); }, end: () => { c.closed = true; } }, '127.0.0.1');
|
|
@@ -405,7 +412,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
405
412
|
try {
|
|
406
413
|
let body: Record<string, unknown> | undefined;
|
|
407
414
|
if (step.signIn) {
|
|
408
|
-
if (!served?.stream) streams ??= await vendorStream(packsDir(), journey.vendor);
|
|
415
|
+
if (!served?.stream) streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
409
416
|
const factory = served?.stream ?? streams?.(root, {}); if (!factory) throw new Error(`no create<Name>TwinStream adapter enrolled for ${journey.vendor}`);
|
|
410
417
|
let out: number[] = [];
|
|
411
418
|
const conn = factory({ write: (b) => { out.push(...b); }, end: () => {} }, '127.0.0.1');
|
|
@@ -3,13 +3,13 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { installedVendorPacks, isDerivedPack, packVendor } from '@volter/world-core/runtime';
|
|
5
5
|
|
|
6
|
-
export function walkPackDirectory(from: string, vendor: string): string | undefined {
|
|
6
|
+
export function walkPackDirectory(from: string, vendor: string, dependenciesFrom = from): string | undefined {
|
|
7
7
|
if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/.test(vendor)) return undefined;
|
|
8
8
|
const [name, lane] = vendor.split('/');
|
|
9
9
|
const sibling = join(from, name!);
|
|
10
10
|
const root = isDerivedPack(sibling) && packVendor(sibling) === name
|
|
11
11
|
? sibling
|
|
12
|
-
: installedVendorPacks(
|
|
12
|
+
: installedVendorPacks(dependenciesFrom, { vendors: [name!] }).get(name!);
|
|
13
13
|
const unit = root && (lane ? join(root, lane) : root);
|
|
14
14
|
return unit && existsSync(join(unit, 'src', 'index.ts')) ? unit : undefined;
|
|
15
15
|
}
|
package/src/walk/vendor-fetch.ts
CHANGED
|
@@ -13,8 +13,8 @@ type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
|
|
|
13
13
|
* life's application answers stand in for the application the vendor asks (`application`, this fetch's alone). */
|
|
14
14
|
export type VendorFetchFactory = (root: string, options?: { scenarioPath?: string; application?: ApplicationStandIn }) => (request: Request) => Response | Promise<Response>;
|
|
15
15
|
|
|
16
|
-
export async function vendorFetch(twinDir: string, vendor: string): Promise<VendorFetchFactory | undefined> {
|
|
17
|
-
const directory = walkPackDirectory(twinDir, vendor);
|
|
16
|
+
export async function vendorFetch(twinDir: string, vendor: string, dependenciesFrom = twinDir): Promise<VendorFetchFactory | undefined> {
|
|
17
|
+
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
18
18
|
if (!directory) return undefined;
|
|
19
19
|
let mod: Record<string, unknown>;
|
|
20
20
|
try { mod = (await import(join(directory, 'src', 'index.ts'))) as Record<string, unknown>; } catch { return undefined; }
|
|
@@ -52,9 +52,9 @@ const jwtView = (field: string) => (body: unknown): unknown => {
|
|
|
52
52
|
try { return JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8')); } catch { return null; }
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
export async function vendorAnswerViews(twinDir: string, vendor: string): Promise<AnswerViews> {
|
|
55
|
+
export async function vendorAnswerViews(twinDir: string, vendor: string, dependenciesFrom = twinDir): Promise<AnswerViews> {
|
|
56
56
|
let own: AnswerViews = {};
|
|
57
|
-
const directory = walkPackDirectory(twinDir, vendor);
|
|
57
|
+
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
58
58
|
try {
|
|
59
59
|
if (directory) own = ((await import(join(directory, 'src', 'index.ts'))) as { answerViews?: AnswerViews }).answerViews ?? {};
|
|
60
60
|
} catch { /* none of its own */ }
|
|
@@ -65,8 +65,8 @@ export async function vendorAnswerViews(twinDir: string, vendor: string): Promis
|
|
|
65
65
|
* hosted World hands a WebSocket's bytes. The walk drives it for a journey's line steps. */
|
|
66
66
|
export type VendorStreamFactory = (root: string, options?: Record<string, unknown>) => TwinStream;
|
|
67
67
|
|
|
68
|
-
export async function vendorStream(twinDir: string, vendor: string): Promise<VendorStreamFactory | undefined> {
|
|
69
|
-
const directory = walkPackDirectory(twinDir, vendor);
|
|
68
|
+
export async function vendorStream(twinDir: string, vendor: string, dependenciesFrom = twinDir): Promise<VendorStreamFactory | undefined> {
|
|
69
|
+
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
70
70
|
if (!directory) return undefined;
|
|
71
71
|
let mod: Record<string, unknown>;
|
|
72
72
|
try { mod = (await import(join(directory, 'src', 'index.ts'))) as Record<string, unknown>; } catch { return undefined; }
|