@volter/twin-catalog 0.2.28 → 0.2.30

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/catalog.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sourceCommit": "a6584aeee5b2761e628cb8a90b22bdf2c6862359",
3
+ "sourceCommit": "0053b5936eec781b314e51a78486e05d39523ee1",
4
4
  "digest": "3195c901d6068ef7be8e9e23676b9b6ec58fb4e50c2d5604bbea390fefec9fdb",
5
5
  "versions": [
6
6
  {
@@ -5,7 +5,7 @@ not scripts from a platform checkout. Read the [process](process.md) for identit
5
5
 
6
6
  ## Build a pack
7
7
 
8
- Install Bun at the catalog policy's version and the public `@volter/world-core`, `@volter/world-runtime` and
8
+ Install Bun at the catalog policy's version and the public `@volter/world`, `@volter/world-core`, `@volter/world-runtime` and
9
9
  `@volter/twin-standard` versions in `policy.json`. Install TypeScript and its Node/Bun types as development dependencies.
10
10
  Keep the package directory named for the vendor; its npm package name is independent.
11
11
 
@@ -14,7 +14,9 @@ bunx --bun twin-standard create stripe --init stripe --package @example/payments
14
14
  ```
15
15
 
16
16
  The generated files contain explicit fill markers. Author the vendored spec and its provenance, demand, manifest,
17
- stored resource states, decision table, handlers and customer journey in the standard's required order. The package
17
+ stored resource states, decision table, handlers and customer journey in the standard's required order. Start the
18
+ outline from the vendor quickstart and pinned SDK's ordinary default requests, app environment and issued identity.
19
+ Walk that customer entry while building; reducing its requested fields to match the twin does not establish compatibility. The package
18
20
  README describes its vendor surface, refusals and limits. Declare your own public GitHub repository and license in
19
21
  package.json. Complete source requirements before derivation:
20
22
 
@@ -34,7 +36,27 @@ LICENSE. A build and `npm pack --dry-run` must show those paths; no platform-spe
34
36
 
35
37
  ## Assess before submitting
36
38
 
37
- Walk the life's journeys while implementing. For the final assessment, run the released standard through a World:
39
+ Package the customer entry in `journeys/first-use.json`. It has `schemaVersion: 1`, the upstream `source`, an
40
+ `about` describing its scope, and a `files` map of relative paths to text. Include `package.json` with exact registry
41
+ SDK dependencies, `.env.example` with the names the app reads, and its unchanged client script with result
42
+ assertions. `run` is an argument array such as `["node", "workflow.mjs"]`. For stored workflows,
43
+ `retention: { "readback": ["node", "readback.mjs"] }` inspects prior state without creating it again. A stateless
44
+ workflow instead declares `retention: { "none": "<why no stored vendor resource is created>" }`. Use SDKs to create
45
+ stored data and existing vendor doors for synthetic setup; do not use scenario success in place of a stored write.
46
+ The standard owns CLI lifecycle and teardown, so the scripts do not start or stop infrastructure themselves. For a timed workflow, `clock: { "at": "<ISO instant>", "advanceAfterRun": "10s" }` freezes time before the client runs, then advances and inspects the due result before retention is measured. The duration is the journey’s chosen synthetic wait, not a vendor timing guarantee.
47
+
48
+ Walk the life's journeys and this first-use journey while implementing. For final qualification, build and pack
49
+ first, install that tarball in a fresh directory without development links, and run the released standard against
50
+ the installed package. Keep the archive inventory and lockfile. Prepare a new customer app beside that installation:
51
+
52
+ ```sh
53
+ twin-standard prepare-consumer <installed pack dir> --app <new app dir> --cli <absolute installed volter executable> --integrity <packed SHA-512 integrity> --out prepared-consumer.json
54
+ npm install --prefix <new app dir> --ignore-scripts --no-audit --no-fund
55
+ ```
56
+
57
+ This preparation writes fixture files only; SDK dependency installation precedes offline execution. The product
58
+ CLI must resolve the installed candidate through that app's parent dependency directory. For the final assessment,
59
+ run the released standard through a World:
38
60
 
39
61
  Create `assessment.world.config.json`:
40
62
 
@@ -43,15 +65,16 @@ Create `assessment.world.config.json`:
43
65
  ```
44
66
 
45
67
  ```sh
46
- volter-world run assessment.world.config.json --root . --env-out /tmp/new-assessment.env --owner pack-assessment -- bunx --bun twin-standard assess stripe --browser --out assessment.json
68
+ volter-world run assessment.world.config.json --root . --env-out /tmp/new-assessment.env --owner pack-assessment -- twin-standard assess <installed pack dir> --prepared-consumer prepared-consumer.json --browser --out assessment.json
47
69
  ```
48
70
 
49
- The assessment config has no vendor services; the standard creates fresh in-memory World state for each journey.
71
+ The outer assessment config has no vendor services; the standard creates fresh in-memory state for journey replay
72
+ and uses the product CLI to own a separate app World for the installed customer workflow.
50
73
  For an HTTP browser target, use Linux, install Playwright at the catalog policy version and its Chromium binary,
51
74
  and install `libfaketime` (Debian/Ubuntu: `sudo apt-get install libfaketime`). The browser process uses the World
52
75
  clock for native cookie expiry, at one-second precision; JavaScript time retains millisecond precision. Authors on
53
76
  other platforms can omit `--browser` for in-process assessment; catalog Actions supplies the browser target. The report
54
- separates declared served/gap counts, operations actually exercised, deterministic replay, conformance and Chromium
77
+ separates declared served/gap counts, operations actually exercised, deterministic replay, conformance, installed customer workflow and Chromium
55
78
  results. It does not measure DOM interactions, line coverage, real-vendor parity or every possible state transition.
56
79
 
57
80
  Local assessment helps authors; it does not replace the independent catalog assessment of the published tarball.
package/docs/process.md CHANGED
@@ -47,6 +47,8 @@ does not change that pin. Removing a recommendation is not uninstalling a runnin
47
47
  Each assessed release exposes its artifact's description, license, Node requirements, kernel peer requirements,
48
48
  source directory and issue tracker beside its integrity, provenance and measured behavior. README and license links
49
49
  point to the source commit verified by npm provenance. They describe that version, not the publisher's current branch.
50
+ The JSON-only reader also projects installed customer scope, pinned SDK dependencies, actual CLI/kernel/runtime versions, clock conditions, retention and teardown when the bound report measured them. An older report exposes `consumer: null`; it cannot acquire a passing workflow from publisher trust or a later test.
51
+
50
52
  Historical assessments without these retained fields show them as unavailable; current registry metadata does not
51
53
  fill missing historical evidence. A provenance link establishes source and build identity, not vendor fidelity.
52
54
 
@@ -81,7 +83,8 @@ An already admitted package/version cannot acquire different bytes or a differen
81
83
 
82
84
  The catalog evaluates only the tarball fetched at the declared integrity. Package source in the contributor's
83
85
  repository and evidence supplied in the PR are context, never substitutes for running that artifact. A submission
84
- cannot select its own evaluator, thresholds, registry, commands or privileges.
86
+ cannot select its own evaluator, thresholds, registry or privileges. A pack's customer fixture supplies its
87
+ unchanged client invocation; the pinned standard owns initialization, lifecycle, assertions and readiness.
85
88
 
86
89
  ## State and invalidation
87
90
 
@@ -143,7 +146,8 @@ routing, not the isolation boundary: the container enforces the evaluation netwo
143
146
  The trusted controller owns success/failure, hashes and run identity. Candidate output is untrusted data; bounded
144
147
  report parsing never executes it or interpolates it into a shell. It can explain results but cannot confer merge or
145
148
  publication authority. The trusted summary renderer escapes contributor strings and neutralizes mentions. Raw logs
146
- and JSON evidence are downloadable artifacts. There is no execution of arbitrary contributor-supplied test commands.
149
+ and JSON evidence are downloadable artifacts. A packaged first-use fixture executes only in the offline assessment
150
+ container under the same unprivileged boundary as the candidate pack; it has no controller or admission authority.
147
151
  Conformance and journey checks are the versioned standard's own entrypoints. Like any in-process plugin check, they
148
152
  measure cooperative pack behavior, not a proof that malicious executable code cannot deceive a test; moderator
149
153
  source review remains part of admission.
@@ -161,6 +165,21 @@ The released standard owns the development SDK versions used by its client cases
161
165
  The versioned `@volter/twin-standard` owns the checks and machine-readable assessment API. The catalog invokes it;
162
166
  it does not implement another Protocol 3 grader or journey engine. Evaluation occurs inside a task-owned World.
163
167
 
168
+ Installed customer readiness is separate from the HTTP journey replay. The released standard materializes the
169
+ pack's `journeys/first-use.json` in a new application directory; versioned standard fixtures cover older immutable
170
+ artifacts that predate this format. Preparation installs the fixture's exact SDK versions with scripts disabled,
171
+ retains its resolved dependency lock and recipe digest, and pins the product CLI alongside the kernel and runtime.
172
+ Evaluation calls normal `volter world init` and checks that its service uses the exact candidate package/version,
173
+ then runs the unchanged client and its result assertions. A stored workflow must retain its state through `down`
174
+ and `up`; its readback may add no mutations. Timed workflows settle the declared result under an explicit frozen/advanced World clock before the retention baseline, so runtime scheduling delay is not a test input. A stateless workflow names why this does not apply. Final stopped
175
+ status is required. Missing or failing consumer evidence means changes needed, even when source form and replay pass.
176
+ These walks establish the advertised workflow at the recorded SDK and tool versions; they do not establish every
177
+ SDK method, complete vendor parity or production delivery. Historical reports keep their original scope.
178
+
179
+ The Linux runner installs native PostgreSQL tools without a distribution-owned cluster for customer workflows
180
+ that declare that backing. Each World starts and stops its own database through the runtime; the container image
181
+ and the tool/SDK locks identify the execution conditions.
182
+
164
183
  Quick assessment walks the packaged customer journey twice from fresh state with the same clock and World draws.
165
184
  It reports failures, answered steps, response replay equality, and served/gap counts over the entire declared surface. Actual operation coverage comes from kernel dispatch, with
166
185
  every declared gap retained in the denominator; declared support and exercised coverage are separate fields.
package/lib/browse.d.mts CHANGED
@@ -7,6 +7,19 @@ export type Coverage = {
7
7
  };
8
8
  export type Replay = { equal: boolean; runs: number; firstSha256: string; secondSha256: string };
9
9
  export type Measurements = {
10
+ consumer: null | {
11
+ measured: boolean;
12
+ passed: boolean;
13
+ scope: string | null;
14
+ fixture: string | null;
15
+ sdkDependencies: Record<string, string> | null;
16
+ tools: Record<string, string> | null;
17
+ clock: Record<string, unknown> | null;
18
+ retainedReadback: boolean | null;
19
+ retentionNotApplicable: string | null;
20
+ teardownVerified: boolean | null;
21
+ failures: string[] | null;
22
+ };
10
23
  scope: string | null;
11
24
  surface: { total: number; served: number; gap: number } | null;
12
25
  operationCoverage: Coverage | null;
package/lib/browse.mjs CHANGED
@@ -130,6 +130,16 @@ export function browse(root, { vendor, package: packageName, integrity = null }
130
130
  operationCoverage: quick?.operationCoverage ?? null, servedOperations: quick?.servedOperations ?? null,
131
131
  steps: quick?.steps ?? null, answered: quick?.answered ?? null, failures: quick?.failures ?? null,
132
132
  replay: quick?.replay ?? null, conformance: bound.report.conformance ?? null,
133
+ consumer: bound.report.consumer ? {
134
+ measured: bound.report.consumer.measured === true, passed: bound.report.consumer.passed === true,
135
+ scope: bound.report.consumer.scope ?? null, fixture: bound.report.consumer.fixture ?? null,
136
+ sdkDependencies: bound.envelope.prepared?.consumerFixture?.dependencies ?? null,
137
+ tools: bound.report.consumer.tools ?? null, clock: bound.report.consumer.clock ?? null,
138
+ retainedReadback: bound.report.consumer.retainedReadback ?? null,
139
+ retentionNotApplicable: bound.report.consumer.retentionNotApplicable ?? null,
140
+ teardownVerified: bound.report.consumer.teardownVerified ?? null,
141
+ failures: bound.report.consumer.failures ?? null,
142
+ } : null,
133
143
  browser: browser?.measured === true ? browser : null,
134
144
  codeCoverage: quick?.codeCoverage ?? null, stateTransitionCoverage: quick?.stateTransitionCoverage ?? null,
135
145
  domCoverage: browser?.domCoverage ?? null, comparison: bound.report.comparison ?? null } : null };
package/lib/runner.mjs CHANGED
@@ -25,7 +25,8 @@ export function withContainer(root, input, output, execute, { spawn = spawnSync
25
25
  let volume = false;
26
26
  try {
27
27
  const tools = input.policy.tools;
28
- run(['build', '--iidfile', imageFile, '--build-arg', `NODE_IMAGE=${input.policy.nodeImage}`, '--build-arg', `BUN_VERSION=${input.policy.bun}`, '--build-arg', `STANDARD_VERSION=${tools['@volter/twin-standard']}`, '--build-arg', `CORE_VERSION=${tools['@volter/world-core']}`, '--build-arg', `RUNTIME_VERSION=${tools['@volter/world-runtime']}`, '--build-arg', `PLAYWRIGHT_VERSION=${tools.playwright}`, '-f', join(root, 'runner/Dockerfile'), root]);
28
+ requireThat(tools['@volter/world'], 'policy must pin the product CLI for installed customer workflows');
29
+ run(['build', '--iidfile', imageFile, '--build-arg', `NODE_IMAGE=${input.policy.nodeImage}`, '--build-arg', `BUN_VERSION=${input.policy.bun}`, '--build-arg', `STANDARD_VERSION=${tools['@volter/twin-standard']}`, '--build-arg', `CORE_VERSION=${tools['@volter/world-core']}`, '--build-arg', `RUNTIME_VERSION=${tools['@volter/world-runtime']}`, '--build-arg', `CLI_VERSION=${tools['@volter/world']}`, '--build-arg', `PLAYWRIGHT_VERSION=${tools.playwright}`, '-f', join(root, 'runner/Dockerfile'), root]);
29
30
  const image = readFileSync(imageFile, 'utf8').trim();
30
31
  requireThat(/^sha256:[a-f0-9]{64}$/.test(image), 'container image has no immutable identity');
31
32
  run(['volume', 'create', '--label', `twin-catalog.owner=${id}`, '--label', `twin-catalog.run=${runOwner}`, id]); volume = true;
@@ -56,12 +57,15 @@ export function evaluate(root, input, output, options = {}) {
56
57
  const lock = run([...common, '--network', 'none', image, '-e', 'process.stdout.write(require("fs").readFileSync("/work/package-lock.json","utf8"))']).stdout;
57
58
  requireThat(digest(lock) === prepared.dependencyLockSha256, 'dependency lock identity changed');
58
59
  write(`${output}.lock.json`, JSON.parse(lock));
60
+ const consumerLock = run([...common, '--network', 'none', image, '-e', 'process.stdout.write(require("fs").readFileSync("/work/customer-app/package-lock.json","utf8"))']).stdout;
61
+ requireThat(digest(consumerLock) === prepared.consumerDependencyLockSha256, 'customer SDK lock identity changed');
62
+ write(`${output}.consumer-lock.json`, JSON.parse(consumerLock));
59
63
  phase = 'offline-assessment';
60
64
  const result = run([...common, '--network', 'none', '--entrypoint', 'bun', image, '/opt/catalog/node_modules/@volter/world-runtime/src/cli.ts', 'run', '/work/world.json', '--root', '/work', '--env-out', '/work/world.env', '--owner', owner, '--', 'bun', '/opt/catalog/runner/evaluate.mjs'], true);
61
65
  const reportRead = run([...common, '--network', 'none', image, '-e', 'process.stdout.write(require("fs").readFileSync("/work/report.json","utf8"))'], true);
62
66
  const report = reportRead.status === 0 ? JSON.parse(reportRead.stdout) : null;
63
67
  requireThat(!report || (report.schemaVersion === 1 && report.package === input.submission.package && report.version === input.submission.version && report.integrity === input.submission.integrity), 'evaluator returned a mismatched report');
64
- const envelope = { schemaVersion: 1, inputSha256: digest(input), input, image, prepared, dependencyLock: JSON.parse(lock), status: result.status === 0 && report?.ready === true ? 'ready' : 'changes-needed', report, evaluatedAt: new Date().toISOString() };
68
+ const envelope = { schemaVersion: 1, inputSha256: digest(input), input, image, prepared, dependencyLock: JSON.parse(lock), consumerDependencyLock: JSON.parse(consumerLock), status: result.status === 0 && report?.ready === true ? 'ready' : 'changes-needed', report, evaluatedAt: new Date().toISOString() };
65
69
  write(output, envelope);
66
70
  return envelope;
67
71
  }, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-catalog",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "description": "The moderated twin distribution: approved versions and submission tooling for independent publishers.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  "evidence"
39
39
  ],
40
40
  "catalog": {
41
- "sourceCommit": "a6584aeee5b2761e628cb8a90b22bdf2c6862359",
41
+ "sourceCommit": "0053b5936eec781b314e51a78486e05d39523ee1",
42
42
  "digest": "3195c901d6068ef7be8e9e23676b9b6ec58fb4e50c2d5604bbea390fefec9fdb"
43
43
  }
44
44
  }
package/policy.json CHANGED
@@ -2,10 +2,11 @@
2
2
  "schemaVersion": 1,
3
3
  "registry": "https://registry.npmjs.org",
4
4
  "tools": {
5
- "@volter/world-core": "3.0.98",
6
- "@volter/world-runtime": "3.0.98",
7
- "@volter/twin-standard": "1.0.90",
8
- "playwright": "1.63.0"
5
+ "@volter/world-core": "3.0.105",
6
+ "@volter/world-runtime": "3.0.105",
7
+ "@volter/twin-standard": "1.0.97",
8
+ "playwright": "1.63.0",
9
+ "@volter/world": "3.0.105"
9
10
  },
10
11
  "bun": "1.4.2",
11
12
  "nodeImage": "node:24-bookworm-slim",
package/runner/Dockerfile CHANGED
@@ -4,13 +4,18 @@ ARG BUN_VERSION=1.3.11
4
4
  ARG STANDARD_VERSION=1.0.37
5
5
  ARG CORE_VERSION=3.0.45
6
6
  ARG RUNTIME_VERSION=3.0.45
7
+ ARG CLI_VERSION
7
8
  ARG PLAYWRIGHT_VERSION=1.63.0
8
9
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
9
- RUN apt-get update && apt-get install -y --no-install-recommends git openssl ca-certificates libfaketime && rm -rf /var/lib/apt/lists/*
10
+ # Install native PostgreSQL tools without a distribution-owned cluster; each World owns its own data and lifetime.
11
+ # https://manpages.debian.org/bookworm/postgresql-common/pg_createcluster.1.en.html#DEFAULT_VALUES
12
+ RUN mkdir -p /etc/postgresql-common && printf 'create_main_cluster = false\n' > /etc/postgresql-common/createcluster.conf \
13
+ && apt-get update && apt-get install -y --no-install-recommends git openssl ca-certificates libfaketime postgresql-15 && rm -rf /var/lib/apt/lists/*
14
+ ENV VOLTER_WORLD_POSTGRES_BIN=/usr/lib/postgresql/15/bin
10
15
  WORKDIR /opt/catalog
11
16
  RUN npm install --ignore-scripts --no-audit --no-fund --save-exact \
12
17
  @oven/bun-linux-x64@${BUN_VERSION} @volter/twin-standard@${STANDARD_VERSION} \
13
- @volter/world-core@${CORE_VERSION} @volter/world-runtime@${RUNTIME_VERSION} tar@7.5.7 semver@7.8.5 sigstore@4.1.1 playwright@${PLAYWRIGHT_VERSION}
18
+ @volter/world-core@${CORE_VERSION} @volter/world-runtime@${RUNTIME_VERSION} @volter/world@${CLI_VERSION} tar@7.5.7 semver@7.8.5 sigstore@4.1.1 playwright@${PLAYWRIGHT_VERSION}
14
19
  ENV PATH="/opt/catalog/node_modules/@oven/bun-linux-x64/bin:/opt/catalog/node_modules/.bin:${PATH}"
15
20
  RUN ./node_modules/.bin/playwright install --with-deps chromium
16
21
  COPY lib /opt/catalog/lib
@@ -7,9 +7,10 @@ requireThat(Boolean(process.env.VOLTER_WORLD), 'evaluation must run inside a Wor
7
7
  const { submission: s } = read('/input/input.json');
8
8
  const requireFromPack = createRequire('/work/package.json');
9
9
  const { assessPack } = await import(requireFromPack.resolve('@volter/twin-standard'));
10
- const assessment = await assessPack(join('/work/packs', s.vendor), { browser: true });
10
+ const prepared = read('/work/prepared.json');
11
+ const assessment = await assessPack(join('/work/packs', s.vendor), { browser: true, consumer: prepared.consumer });
11
12
  requireThat(assessment.package === s.package && assessment.version === s.version && assessment.vendor === s.vendor, 'assessment returned a different artifact identity');
12
- const report = { ...assessment, integrity: s.integrity, prepared: read('/work/prepared.json') };
13
+ const report = { ...assessment, integrity: s.integrity, prepared };
13
14
  report.comparison = compareReports(report, existsSync('/input/baseline.json') ? read('/input/baseline.json') : null);
14
15
  write('/work/report.json', report);
15
16
  process.exit(report.ready ? 0 : 1);
@@ -81,6 +81,18 @@ const provenance = await verifiedProvenance(await response.json(), source, bytes
81
81
  // Keep a vendor-named package directory for the standard's public directory API; preserve the installed artifact.
82
82
  mkdirSync('/work/packs', { recursive: true });
83
83
  cpSync(join('/work/node_modules', s.package), join('/work/packs', s.vendor), { recursive: true });
84
+ const { prepareConsumerApp } = await import(evaluatorRequire.resolve('@volter/twin-standard'));
85
+ const appDir = '/work/customer-app';
86
+ const consumerFixture = prepareConsumerApp(join('/work/packs', s.vendor), appDir);
87
+ const appInstall = spawnSync('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--registry', policy.registry], { cwd: appDir, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
88
+ requireThat(appInstall.status === 0, `customer SDK preparation failed: ${appInstall.stderr}`);
89
+ const consumerLock = read(join(appDir, 'package-lock.json'));
90
+ for (const [path, entry] of Object.entries(consumerLock.packages ?? {})) {
91
+ if (!path) continue;
92
+ requireThat(!entry.link && entry.resolved && entry.integrity, `${path}: customer dependency is not a locked registry artifact`);
93
+ registryURL(entry.resolved, policy.registry);
94
+ }
95
+ const consumer = { appDir, cli: '/work/node_modules/.bin/volter', artifactIntegrity: s.integrity };
84
96
  // Retain listing facts from these exact bytes, never the registry's moving latest metadata.
85
97
  const packageMetadata = {
86
98
  description: typeof pack.description === 'string' ? pack.description : null,
@@ -90,5 +102,5 @@ const packageMetadata = {
90
102
  repositoryDirectory: typeof pack.repository === 'object' ? pack.repository.directory ?? null : null,
91
103
  bugs: typeof pack.bugs === 'string' ? pack.bugs : pack.bugs?.url ?? null,
92
104
  };
93
- write('/work/prepared.json', { schemaVersion: 1, submission: s, integrity: s.integrity, provenance, packageMetadata, dependencyLockSha256: digest(readFileSync('/work/package-lock.json', 'utf8')), clientSdkLocks, clientDependencyChoices, tools: policy.tools });
105
+ write('/work/prepared.json', { schemaVersion: 1, submission: s, integrity: s.integrity, provenance, packageMetadata, dependencyLockSha256: digest(readFileSync('/work/package-lock.json', 'utf8')), clientSdkLocks, clientDependencyChoices, tools: policy.tools, consumer, consumerFixture, consumerDependencyLockSha256: digest(readFileSync(join(appDir, 'package-lock.json'), 'utf8')) });
94
106
  write('/work/world.json', { id: 'catalog-assessment', network: { egress: [] }, services: [] });