@parity/product-lab 0.1.2 → 0.3.0

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/adapters/environment/engine/README.md +22 -3
  3. package/adapters/environment/engine/src/engine/engine-config.d.ts +1 -0
  4. package/adapters/environment/engine/src/engine/fork-bundle.d.ts +1 -1
  5. package/adapters/environment/engine/src/engine/fork-bundle.js +2 -2
  6. package/adapters/environment/engine/src/engine/fork-refresh.d.ts +4 -7
  7. package/adapters/environment/engine/src/engine/fork-refresh.js +52 -23
  8. package/adapters/environment/engine/src/engine/published-bundle.d.ts +19 -15
  9. package/adapters/environment/engine/src/engine/published-bundle.js +44 -51
  10. package/adapters/environment/engine/src/index.d.ts +3 -2
  11. package/adapters/environment/engine/src/index.js +3 -2
  12. package/adapters/environment/engine/src/saved-fork/saved-fork-tooling.d.ts +1 -1
  13. package/adapters/environment/engine/src/saved-fork/saved-fork-tooling.js +5 -2
  14. package/adapters/environment/engine/src/saved-fork/saved-fork.js +1 -1
  15. package/adapters/environment/engine/src/setup/engine-build.d.ts +4 -4
  16. package/adapters/environment/engine/src/setup/engine-build.js +14 -4
  17. package/adapters/environment/engine/src/setup/engine-tooling.d.ts +33 -0
  18. package/adapters/environment/engine/src/setup/engine-tooling.js +59 -0
  19. package/adapters/environment/engine/src/setup/node-workspace.patch +403 -0
  20. package/adapters/environment/engine/src/setup/patch-catalogue.d.ts +23 -0
  21. package/adapters/environment/engine/src/setup/patch-catalogue.js +8 -0
  22. package/adapters/environment/engine/src/setup/patches.d.ts +15 -1
  23. package/adapters/environment/engine/src/setup/patches.js +17 -2
  24. package/adapters/environment/engine/src/setup/prepare-engine.d.ts +6 -5
  25. package/adapters/environment/engine/src/setup/prepare-engine.js +27 -10
  26. package/composition/README.md +6 -4
  27. package/composition/src/engine-setup.js +9 -3
  28. package/composition/src/index.d.ts +2 -1
  29. package/composition/src/index.js +2 -1
  30. package/composition/src/people-dev/onboarding.d.ts +17 -3
  31. package/composition/src/people-dev/onboarding.js +25 -9
  32. package/composition/src/previewnet-genesis.d.ts +62 -0
  33. package/composition/src/previewnet-genesis.js +127 -0
  34. package/core/application/src/runs/failure-rules.js +5 -2
  35. package/entrypoints/cli/src/setup-commands.d.ts +1 -1
  36. package/entrypoints/cli/src/setup-commands.js +3 -1
  37. package/package.json +1 -1
@@ -2,8 +2,10 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { createHash } from "node:crypto";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { z } from "zod";
8
+ import { SHIPPED_ENGINE_PATCHES } from "./patch-catalogue.js";
7
9
  const patchEntry = z
8
10
  .looseObject({
9
11
  file: z.string().regex(/^[\w.-]+\.(?:patch|json)$/),
@@ -13,8 +15,8 @@ const patchEntry = z
13
15
  adds: z.array(z.string()),
14
16
  });
15
17
  export const patchesDirectory = fileURLToPath(new URL("../../../../../patches/", import.meta.url));
16
- export const ENGINE_PATCHES_REPOSITORY_ONLY = "a patched Engine is prepared from the product-lab repository; this installation of product-lab carries no previewnet-engine patches (patches/ is not part of the package). " +
17
- "Prepare stock Engine instead: `product-lab fetch --environment <id> --no-engine-patch` (EngineSource `patch: \"none\"`)";
18
+ export const ENGINE_PATCHES_REPOSITORY_ONLY = "the previewnet-engine patch manifest is the product-lab repository's; this installation of product-lab carries no patches/ folder (patches/ is not part of the package), only the node-workspace patch it ships. " +
19
+ "Prepare stock Engine, or the shipped node-workspace patch: `product-lab fetch --environment <id>` (EngineSource `patch: \"none\"` or `\"node-workspace\"`)";
18
20
  export class EnginePatchesNotInstalled extends Error {
19
21
  directory;
20
22
  constructor(directory) {
@@ -46,3 +48,16 @@ export function resolvePatch(upstream, name) {
46
48
  throw new Error(`${upstream}/${entry.file} differs from its manifest digest`);
47
49
  return { name, path, sha256, base: entry.base, repository: entry.repository };
48
50
  }
51
+ export const shippedPatchesDirectory = fileURLToPath(new URL("./", import.meta.url));
52
+ export function shippedEnginePatch(name, directory = shippedPatchesDirectory) {
53
+ const entry = Object.hasOwn(SHIPPED_ENGINE_PATCHES, name) ? SHIPPED_ENGINE_PATCHES[name] : undefined;
54
+ if (!entry)
55
+ throw new Error(`Unknown shipped Engine patch: ${name} (shipped: ${Object.keys(SHIPPED_ENGINE_PATCHES).join(", ")})`);
56
+ const path = join(directory, entry.file);
57
+ if (!existsSync(path))
58
+ throw new Error(`The shipped Engine patch ${name} is missing from this installation (no ${path}): reinstall @parity/product-lab`);
59
+ const sha256 = createHash("sha256").update(readFileSync(path)).digest("hex");
60
+ if (sha256 !== entry.sha256)
61
+ throw new Error(`The shipped Engine patch ${entry.file} differs from its catalogue digest`);
62
+ return { name, path, sha256, base: entry.base, repository: entry.repository };
63
+ }
@@ -29,6 +29,7 @@ export interface EngineSource {
29
29
  /**
30
30
  * `none`: stock Engine at the ref, without the owned-processes patch (an Engine that carries the fixes upstream),
31
31
  * recorded as such (engine-build.json `patch: "none"`, an empty `patchSha256`; engine.json `enginePatch: "none"`).
32
+ * `node-workspace`: the native-node workspace fix at the tested base, which the package ships (patch-catalogue.ts).
32
33
  * Default `none`. The retired `owned-processes` value is refused.
33
34
  */
34
35
  readonly patch?: EnginePatch;
@@ -47,7 +48,7 @@ export interface EngineBuildRecord {
47
48
  repository?: string;
48
49
  /** `none`: stock Engine, without the owned-processes patch (absent in an older record: the patch). */
49
50
  patch?: EnginePatch;
50
- /** The owned-processes patch's digest; empty for a stock build. */
51
+ /** The selected patch's digest; empty for a stock build. */
51
52
  patchSha256: string;
52
53
  preparedAt: string;
53
54
  }
@@ -68,15 +69,15 @@ export interface PreparedEngine {
68
69
  export declare class EngineNotPrepared extends Error {
69
70
  readonly reason: string;
70
71
  readonly fix: string;
71
- /** The requested stock build, or a retired legacy choice. */
72
+ /** The requested build or a retired legacy choice. */
72
73
  readonly patch: EnginePatch;
73
74
  constructor(reason: string, fix: string,
74
- /** The requested stock build, or a retired legacy choice. */
75
+ /** The requested build or a retired legacy choice. */
75
76
  patch?: EnginePatch);
76
77
  }
77
- /** `as-prepared` accepts the recorded choice only when it is stock; legacy patched records are refused. */
78
+ /** `as-prepared` accepts stock or the current repository fix; legacy owned-processes records are refused. */
78
79
  export type PreparedEnginePatch = EnginePatch | "as-prepared";
79
- /** Check the record against the pin (or explicit ref), checkout HEAD, stock digest, build and dot CLI. */
80
+ /** Check the record against the pin (or explicit ref), checkout HEAD, selected digest, build and dot CLI. */
80
81
  export declare function preparedEngine(root: string, options: {
81
82
  stateDirectory?: string;
82
83
  fix: string;
@@ -4,7 +4,8 @@ import { createHash } from "node:crypto";
4
4
  import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  import { checkedSync } from "./sync-command.js";
7
- import { ENGINE_RELEASE } from "./patch-catalogue.js";
7
+ import { shippedEnginePatch } from "./patches.js";
8
+ import { ENGINE_RELEASE, NODE_WORKSPACE_PATCH } from "./patch-catalogue.js";
8
9
  import { ENGINE_PATCH_RETIRED, engineBuildIssue } from "./engine-build.js";
9
10
  import { applyTurnRelayChoice, turnRelayAvailable } from "../engine/turn-relay.js";
10
11
  export const defaultStateDirectory = ".product-lab";
@@ -41,8 +42,12 @@ export function prepareEngine(root, ref, network, commands, stateDirectory = def
41
42
  throw new Error("Network must be previewnet or paseo-next-v2");
42
43
  setupPaths(root, ref, "", stateDirectory);
43
44
  const repository = engineRepository(source);
44
- if (source.patch && source.patch !== "none")
45
+ if (source.patch && source.patch !== "none" && source.patch !== "node-workspace")
45
46
  throw new Error(ENGINE_PATCH_RETIRED);
47
+ const patch = source.patch ?? "none";
48
+ const patchFile = patch === "node-workspace" ? shippedEnginePatch(patch) : undefined;
49
+ if (patchFile && (patchFile.sha256 !== NODE_WORKSPACE_PATCH.sha256 || patchFile.base !== NODE_WORKSPACE_PATCH.base))
50
+ throw new Error("node-workspace patch differs from its tested catalogue");
46
51
  const output = source.output ?? "inherit";
47
52
  const revisions = /^[0-9a-f]{40}$/i.test(ref)
48
53
  ? [[ref, ref]]
@@ -65,7 +70,9 @@ export function prepareEngine(root, ref, network, commands, stateDirectory = def
65
70
  revisions[0])?.[0];
66
71
  if (!resolvedRef || !/^[0-9a-f]{40}$/i.test(resolvedRef))
67
72
  throw new Error("Could not resolve an immutable Engine revision");
68
- const paths = setupPaths(root, resolvedRef, "", stateDirectory);
73
+ if (patchFile && resolvedRef !== patchFile.base)
74
+ throw new Error("node-workspace patch requires its tested Engine base");
75
+ const paths = setupPaths(root, resolvedRef, patchFile?.sha256 ?? "", stateDirectory);
69
76
  mkdirSync(paths.base, { recursive: true });
70
77
  const run = (command, args, cwd = paths.checkout) => checkedSync(commands, { command, args, cwd, output, timeoutMs: 1_800_000 });
71
78
  if (!existsSync(join(paths.checkout, ".git"))) {
@@ -83,6 +90,13 @@ export function prepareEngine(root, ref, network, commands, stateDirectory = def
83
90
  }
84
91
  else if (head !== resolvedRef)
85
92
  throw new Error("Prepared Engine checkout changed; refusing to overwrite it");
93
+ if (patchFile) {
94
+ const applied = commands.run({ command: "git", args: ["apply", "--reverse", "--check", patchFile.path], cwd: paths.checkout, timeoutMs: 30_000 });
95
+ if (applied.code !== 0) {
96
+ run("git", ["apply", "--check", patchFile.path]);
97
+ run("git", ["apply", patchFile.path]);
98
+ }
99
+ }
86
100
  const manifest = JSON.parse(readFileSync(join(paths.checkout, "package.json"), "utf8"));
87
101
  if (manifest.packageManager !== "pnpm@10.33.0")
88
102
  throw new Error("Engine package manager changed; review setup before updating its toolchain");
@@ -113,7 +127,7 @@ export function prepareEngine(root, ref, network, commands, stateDirectory = def
113
127
  dashboard: "http://127.0.0.1:8090",
114
128
  extraPath: [dotBin],
115
129
  startupTimeoutMs: 1_800_000,
116
- enginePatch: "none",
130
+ enginePatch: patch,
117
131
  };
118
132
  const configFile = resolve(root, stateDirectory, "engine.json");
119
133
  writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
@@ -121,8 +135,8 @@ export function prepareEngine(root, ref, network, commands, stateDirectory = def
121
135
  requestedRef: ref,
122
136
  resolvedRef,
123
137
  repository,
124
- patch: "none",
125
- patchSha256: "",
138
+ patch,
139
+ patchSha256: patchFile?.sha256 ?? "",
126
140
  preparedAt: new Date().toISOString(),
127
141
  }, null, 2) + "\n");
128
142
  return configFile;
@@ -163,16 +177,19 @@ export function preparedEngine(root, options) {
163
177
  throw refuse(prepared === "none"
164
178
  ? `${stateDirectory} holds stock Engine ${String(build.resolvedRef).slice(0, 12)} (prepared without the owned-processes patch), not the patched one`
165
179
  : `${stateDirectory} holds the patched Engine ${String(build.resolvedRef).slice(0, 12)}, not stock Engine`, wanted);
166
- if (wanted !== "none")
180
+ if (wanted !== "none" && wanted !== "node-workspace")
167
181
  throw refuse(ENGINE_PATCH_RETIRED, wanted);
168
182
  const ref = options.ref ?? (options.patch === "as-prepared" ? build.resolvedRef : testedEngineRef);
169
183
  if (!/^[0-9a-f]{40}$/i.test(build.resolvedRef))
170
184
  throw refuse(`${recordFile} has no immutable Engine revision`, wanted);
171
185
  if (/^[0-9a-f]{40}$/i.test(ref) && build.resolvedRef !== ref)
172
186
  throw refuse(`${stateDirectory} holds Engine ${String(build.resolvedRef).slice(0, 12)}, this checkout of product-lab tests ${ref.slice(0, 12)}`, wanted);
173
- if (build.patchSha256 !== "")
174
- throw refuse(`${recordFile} records a patch digest for stock Engine`, wanted);
175
- const paths = setupPaths(stateDirectory, build.resolvedRef, "", ".");
187
+ const digest = wanted === "node-workspace" ? NODE_WORKSPACE_PATCH.sha256 : "";
188
+ if (wanted === "node-workspace" && build.resolvedRef !== NODE_WORKSPACE_PATCH.base)
189
+ throw refuse("node-workspace patch requires its tested Engine base", wanted);
190
+ if (build.patchSha256 !== digest)
191
+ throw refuse(wanted === "none" ? `${recordFile} records a patch digest for stock Engine` : `${recordFile} records another node-workspace patch digest`, wanted);
192
+ const paths = setupPaths(stateDirectory, build.resolvedRef, digest, ".");
176
193
  const issue = existsSync(join(paths.checkout, ".git")) ? engineBuildIssue(paths.checkout, options.commands, wanted) : `there is no checkout at ${paths.checkout}`;
177
194
  if (issue)
178
195
  throw refuse(issue, wanted);
@@ -32,7 +32,7 @@ its own, so its result never depends on a developer's running lab and never rewr
32
32
  | Part | Built from |
33
33
  | --- | --- |
34
34
  | System ports | `NodeSystem` (`src/node-system.ts`, `new NodeSystem(env, { redactor })`): files, clock, ids, logger (a `ConsoleLogger` that scrubs each line with `redactor`; `createLabRuntime` passes the runtime's own `SecretRedactor`, backlog 89), processes, bounded asynchronous and synchronous commands, locks, ports, JSON-RPC (`@product-lab/system-node`, `@product-lab/storage-filesystem`); run manifests under `<lab root>/.product-lab/runs` |
35
- | Engine setup CLI | `src/engine-setup.ts` parses setup options (`--engine-ref`, `--network`, `--state-dir`, `--dry-run`; since 16 October 2026 `--engine-repository <url\|path>` and `--no-patch`, stock Engine recorded as such) and supplies `NodeSyncCommandRunner` to env-engine's synchronous `prepareEngine`, keeping adapter construction in composition. A product prepares the saved forks' Engine with `product-lab fetch --environment paseo-copy` instead (the fork's `EnvironmentProvider.tooling`, reached through `LabRuntime.prepareEnvironmentTooling`; the attached runtime answers it locally) |
35
+ | Engine setup CLI | `src/engine-setup.ts` parses setup options (`--engine-ref`, `--network`, `--state-dir`, `--dry-run`; since 16 October 2026 `--engine-repository <url\|path>` and `--no-patch`, stock Engine recorded as such; since 12 November 2026 `--engine-patch none\|node-workspace`, the shipped node-workspace patch at the tested release) and supplies `NodeSyncCommandRunner` to env-engine's synchronous `prepareEngine`, keeping adapter construction in composition. A product prepares the saved forks' Engine with `product-lab fetch --environment paseo-copy` instead (the fork's `EnvironmentProvider.tooling`, reached through `LabRuntime.prepareEnvironmentTooling`; the attached runtime answers it locally) |
36
36
  | Console redaction (backlog 89) | `redactConsole(redactor, console?)` (`src/redact-console.ts`, exported): every `console.log`/`info`/`debug`/`warn`/`error` line goes through the runtime's redactor until restored, so a lab or provider printing a secret of the lab (a `fromEnv` URL with its token) never shows it. The CLI's `src/main.ts` installs it for one invocation, `featureRuntime` (bundled runs) and the Cucumber runner's `cucumberRuntime` for their process; the MCP server scrubs its captured console itself (`captureConsole`'s `redact`). Tests: `tests/redact-console.test.ts`, `tests/composition.test.ts` (the logger a provider gets, `system.logger`), and end to end `entrypoints/cli/tests/secret-console.test.ts`, `entrypoints/cli/tests/rpc-flags.test.ts`, `entrypoints/mcp/tests/stdio.test.ts` |
37
37
  | Protected stdio worker | `NodeSystem.stdioWorker` constructs the Node adapter for MCP's fd-3 protocol channel; `NodeSystem.syncCommands` supplies bounded synchronous CLI/AI commands; `NodeSystem.terminalCommands` runs an interactive program on the terminal (the terminal app's agent beside the lab) |
38
38
  | Environment adapters | the kinds the lab selects in `defineLab({ environmentAdapters: { <kind>: <settings> } })`, from `ENVIRONMENT_ADAPTERS` (`engine`, `relayless`, `chopsticks` with the attached adapter's probe); imported when first selected |
@@ -40,6 +40,7 @@ its own, so its result never depends on a developer's running lab and never rewr
40
40
  | Host tool services | `src/host-tool-services.ts`: `hostToolServices({ clock, env })` gives the runtime a `NodeCommandRunner`, a `NodeProcessSandbox` (the official build's behavioural probes run under it) and `githubUpstreamResolver`, an `UpstreamResolver` on the GitHub version adapter (`latest` = newest release in the request's tag family, skipping other packages' tags, drafts and prereleases; `GITHUB_TOKEN` when set). `host rebase-check` receives all three in its tool context (`tests/host-tool-services.test.ts`, a fake API) |
41
41
  | What a lab's own code gets (refactor step 9) | `SessionContext.system` and `EnvironmentStartContext.system` (the lab's system ports, `NodeSystem.forLab`, including `commands: NodeCommandRunner` for bounded one-shot commands), `SessionContext.browsers` (`BROWSERS`: `PlaywrightBrowserLauncher`, imported on first launch; `defineLab({ browsers })` overrides) |
42
42
  | Local copies of live networks (the user's decision 5) | `paseoCopy(options?)` and `previewnetCopy(options?)` (`src/network-copies.ts`): one line of a lab's config, `environments: { ...paseoCopy() }`, gives it "Local copy of Paseo", Full or Light (backlog 184), each env-engine's `savedForkEnvironment` in its own folder (`<lab>/.product-lab/environments/<id>`, `<id>-light`) with the live reads composed here (`readLiveRuntimes`, `liveCodeFetcher`), the lab's cache (PRODUCT_LAB_CACHE_DIR) and seeds (PRODUCT_LAB_SHARED_FIXTURES), and a port block (`portBase`, else PRODUCT_LAB_FORK_PORT_BASE, else 42000). `savedForkOptions` builds the options for a lab's own forks (Notes). `tests/network-copies.test.ts` |
43
+ | Previewnet from genesis (12 November 2026) | `previewnetGenesis(options?)` (`src/previewnet-genesis.ts`): one line of a lab's config, `environments: { ...previewnetGenesis() }`, gives it "Previewnet from genesis" (id `previewnet-genesis`, kind `fast-local`): env-engine's `engineEnvironment` over `engineProfiles.genesisSubset({ chains })` (default People, Asset Hub and Bulletin, `PREVIEWNET_GENESIS_CHAINS`), `mode: "genesis"`, a new owned network every start with Engine's readiness. It binds Engine's default ports (relay 10000 to 10005, People 10010, Asset Hub 10020, Bulletin 10030, dashboard 8090, dub 8092, dub's database 5433, and Engine's other defaults in its config/ports.env: eth-rpc 8545, IPFS 8080, 5001 and 4001, TURN 3478, 3479 and its relay range 49152 to 49407, p2p 30333 to 30336), not the lab's port block: one Engine network per machine at a time (a port option is backlog 253 in docs/backlog.md). Its Engine checkout carries the node-workspace patch the package ships, in a cache folder of its own (`<cache>/engine-node-workspace`, env-engine's `engineCheckoutTooling`), so `product-lab fetch --environment previewnet-genesis` prepares it, `product-lab doctor` checks it, and the copies' stock `<cache>/engine` is never changed; a start without it is refused naming that fetch. Once Engine is ready, and after every reset, People's rings are prepared for new people by the genesis sudo key //Alice (`prepareGenesisRings`, people-dev's `PeopleAttesterChain.prepareRings`): the ZK chunk pages from `people-chunks.json` a genesis lacks and an onboarding size of 1 for the lite people collection (`PEOPLE_LITE_COLLECTION`), each skipped when the chain has it; the transactions are in the provenance (`peopleRings`) and a notice, the journal beside Engine's reports. `peopleRings: false` turns it off; a failed preparation stops the network and fails the start. Options: `id`, `label`, `description`, `chains`, `cacheDir`, `reports`, `peopleRings`, `startupTimeoutMs`. `tests/previewnet-genesis.test.ts` (live start) |
43
44
  | The product config (config redesign, lane L1) | `labDefinitionFrom(config, { root, env, environmentPorts?, path? })` (`src/default-lab.ts`, exported): `defineLab({ app, … })` (core/application's `product-lab-config.ts`) onto the definition the runtime runs, starting nothing. The app's id is its first step set's; the others are offered beside it as standalone integrations, available wherever their own requirements hold (7 October 2026: before, each was "used by no app of this lab" and every sentence of theirs refused). Sr25519 keys; fixture `inputs` default `FIXTURE_INPUTS`. It also returns `resolvedConfig` (`@product-lab/ports/driving`'s `ResolvedLabConfig`: every value and where it came from, a secret by its variable's name only), which `createLabRuntime` passes to the registry (`LabRegistry.config`). `createLabRuntime` translates every product config through it, object or function form (`PRODUCT_LAB_MODE` is the function's `mode`; it may be async). `tests/default-lab.test.ts` |
44
45
  | The product config's environments | `fast` is `relaylessTopology` over People and the app's chains it can make; `paseo-copy` `paseoCopy`; `previewnet-copy` and `chopsticks` only when listed; `live.<network>` `liveNetworkEnvironment` (read only without `people`; with them read-write, each person pre-onboarded from the variable named, `newPeople.fund` in planck on the first app chain). `defaultEnvironment` and a mode may name the lab's environments or the keys of `advanced.environments`. Each environment's run folders are under `<output>/environments` (`PRODUCT_LAB_OUTPUT_DIR`, default `<lab>/.product-lab`; Chopsticks' in `chopsticks/` there, its adapter also when `advanced` brings a Chopsticks topology). `tests/default-lab.test.ts` |
45
46
  | The product config's people on `fast` | Persons (`persons` launch option; the default session's `src/default-session-persons.ts` calls people-dev's `peopleOnboarding` and `standInForXcm`, People's genesis from `peopleDevGenesis`) when the app's chains include bulletin or its requires name the Statement Store, else funded wallets. `environments.fast.personProduct` names the label of the person product their member keys are derived under (`"<label>"` alone; the lab adds each network's suffix), shown as `environments.fast.personProduct`; unset, the released truAPI host's, so the keys are today's; refused when it is not a label, and, when the app needs persons, with `onboarding: "truapi-cli"` (the CLI host derives under its own preset). `tests/default-lab.test.ts`, `tests/people-dev-member-key.test.ts` |
@@ -53,7 +54,7 @@ its own, so its result never depends on a developer's running lab and never rewr
53
54
  | The product config's ports and workers | A lab without `ports` claims blocks of 100 in 20000-32699 (`NodeSystem.ports.next()`, `@product-lab/system-node`'s `claimPortBlock`; the window's and the local dev chain's blocks seeded by the lab's directory, so a reopened lab finds them again); `ports` or `PRODUCT_LAB_PORTS` names a range instead, carved from the top. The function form's `claimPorts(size)` (`LabPreparation.claimPorts`) gives exactly `size` ports from the same allocator for what the config runs itself (Notes' own sessions, 500), or refuses by name. `ports: { size }` (1-10000) makes the window's and its sessions' block exactly that size, seeded `<lab>:config` as `claimPorts` is, with the chains' 100 exactly beside it: under PRODUCT_LAB_PORTS from the top, refused naming the range, the size and what is left when it cannot hold them (before, a lab got at most half of what was left); `resolvedConfig`'s `ports` shows the block ("500 ports: 48100-48599"). A lab that states no size gets the blocks it got before. `tests/ports-size.test.ts` A `workers` declaration: each worker `PRODUCT_LAB_OUTPUT_DIR`, its own claimed ports or a slice of a fixed range. `tests/default-lab.test.ts` |
54
55
  | The product config's `advanced` | Today's definition fields the shape does not cover yet, merged over the translation: an app's, a host's or an adapter's fields one by one, `keepBetweenStarts` whole, other records by key, anything else replaces; each listed in `resolvedConfig` with its `advancedWhy`, an adapter by its label (`ADAPTER_LABELS`, `src/adapter-catalogue.ts`, held equal to each factory's by `tests/adapter-catalogue.test.ts`), never its code id; lint warns on a why for a field `advanced` does not set. `tests/default-lab.test.ts` |
55
56
  | The product config's versions (lane L4) | `productVersions` (`src/product-versions.ts`): the policy selects every component lab.lock.json pins (runtimes and polkadot-omni-node by `versions.runtime`, a pinned host by `versions.hosts`; `locked` by default) and, at `latest` only, runtimes and hosts the lock does not pin (a `locked` selector the lock cannot answer would refuse every run); sources for every component that can move, with polkadot-omni-node's added (`NODE_VERSION_SOURCES`: paritytech/polkadot-sdk, the whole tag as the version, the assets as the lock records them, ranked by `order` since polkadot-sdk's tags are not dotted versions; checked against the live repository's release listing on 7 October 2026: the lock's `polkadot-stable2606-2` and the newest stable release carry both assets under those names, and the latest published release, `polkadot-stable2603-7`, is a patch of an older line without them, which `order` ranks below 2606-2; `tests/product-versions.test.ts` replays that listing); a lock that is missing or pins nothing is declared under `unsupported["*"]`, so a mode's `latest` row says the lab has nothing to resolve, naming the lock (`tests/product-versions.test.ts`). |
56
- | The local dev chain's people (backlog 91; from Notes, 6 October 2026) | `src/people-dev/`, product-neutral, for the default session: `peopleDevGenesis(chains)` (People's people and people-lite collections at genesis, `relaylessTopology({ chains, ...peopleDevGenesis(chains) })`); `peopleOnboarding(options)` (a person per new actor: the truAPI CLI provisioner where an identity backend runs, else the lab attester, which reads the wallet's People member key and ring (`environments.fast.onboarding`: `"released"`, the default, with `releasedAllocCheck` in `src/people-dev/member-key.ts`: the ring-VRF entropy in TypeScript, per network (`<label>.<suffix>`: the label `environments.fast.personProduct`, by default the one the released truAPI host derives under, `DEFAULT_PERSON_PRODUCT`; the suffix the routes' namespace) as host-rust-core 0.23 derives it, pinned to its vectors, the released core's `ringVrfMember` and People's `Members` storage at the finalized block, printed in `alloc-check`'s lines; or `"truapi-cli"`, the CLI host's `alloc-check`, in product-lab's checkout, and in a product once the CLI host's locked release takes the lab's local endpoints (the official release, no patches; refused by name with #1166 when it does not; the locked 0.26.0 does; `hostLockFile` names another host lock); `truapiCliAllocCheck` runs it alone), recognises it by //Alice once People's sudo key is checked to be //Alice, prepares the ring on a fresh genesis and waits until the ring includes it; `memberStanding` reads where a member stands); `standInForXcm(environment, options)` (the lab answers each Bulletin allowance a relay-less People cannot send over XCM, by Bulletin's //Alice, declared as lab setup) and `xcmStandIn(environment)` to find it again, `skipNext()` being the lab's self-test hook. `personReadings(person, onboarding, people)` (backlog 160): a new person's screen (the app ready with their account) and People (the member key in a ring, and the lab's own read of `Members.Members` at the finalized head) readings, which the default session offers as `actorReadings` where its people are persons; the window shows them as the step's two checks. Only for apps that write to Bulletin or the Statement Store (the coordinator's rule, 6 October 2026). `tests/people-dev-onboarding.test.ts`, `tests/people-dev-member-key.test.ts` (the vectors, the alloc-check vector, the reader's lines and controls), `tests/people-dev-xcm-stand-in.test.ts`, `tests/people-dev-readings.test.ts`; live cross-check: `tools/verify/tests/member-key-live.test.ts` |
57
+ | The local dev chain's people (backlog 91; from Notes, 6 October 2026) | `src/people-dev/`, product-neutral, for the default session: `peopleDevGenesis(chains)` (People's people and people-lite collections at genesis, `relaylessTopology({ chains, ...peopleDevGenesis(chains) })`); `peopleOnboarding(options)` (a person per new actor: the truAPI CLI provisioner where an identity backend runs, else the lab attester, which reads the wallet's People member key and ring (`environments.fast.onboarding`: `"released"`, the default, with `releasedAllocCheck` in `src/people-dev/member-key.ts`: the ring-VRF entropy in TypeScript, per network (`<label>.<suffix>`: the label `environments.fast.personProduct`, by default the one the released truAPI host derives under, `DEFAULT_PERSON_PRODUCT`; the suffix the routes' namespace) as host-rust-core 0.23 derives it, pinned to its vectors, the released core's `ringVrfMember` and People's `Members` storage at the finalized block, printed in `alloc-check`'s lines; or `"truapi-cli"`, the CLI host's `alloc-check`, in product-lab's checkout, and in a product once the CLI host's locked release takes the lab's local endpoints (the official release, no patches; refused by name with #1166 when it does not; the locked 0.26.0 does; `hostLockFile` names another host lock); `truapiCliAllocCheck` runs it alone), recognises it by //Alice once People's sudo key is checked to be //Alice, prepares the ring on a fresh genesis (`PeopleAttesterChain.prepareRings(collections)`: the ZK chunk pages and an onboarding size of 1 for each collection, each skipped when the chain has it, after `requireDevSudo`; `previewnetGenesis` runs it for `PEOPLE_LITE_COLLECTION`) and waits until the ring includes it; `memberStanding` reads where a member stands); `standInForXcm(environment, options)` (the lab answers each Bulletin allowance a relay-less People cannot send over XCM, by Bulletin's //Alice, declared as lab setup) and `xcmStandIn(environment)` to find it again, `skipNext()` being the lab's self-test hook. `personReadings(person, onboarding, people)` (backlog 160): a new person's screen (the app ready with their account) and People (the member key in a ring, and the lab's own read of `Members.Members` at the finalized head) readings, which the default session offers as `actorReadings` where its people are persons; the window shows them as the step's two checks. Only for apps that write to Bulletin or the Statement Store (the coordinator's rule, 6 October 2026). `tests/people-dev-onboarding.test.ts`, `tests/people-dev-member-key.test.ts` (the vectors, the alloc-check vector, the reader's lines and controls), `tests/people-dev-xcm-stand-in.test.ts`, `tests/people-dev-readings.test.ts`; live cross-check: `tools/verify/tests/member-key-live.test.ts` |
57
58
  | Keep chain and people between starts (backlog 183; from Notes, 6 October 2026) | `src/people-keep/`, product-neutral: `keptPeople(options)`, the default session's entry point. A kept start (`keep`, a host that keeps app storage, an environment that names its chain generation) gets the folder `<root>/<environment>/<generation>` with `saved()` (the people to reopen, with their wallets), `remember`, `forget` (a deleted person, with their folders), `hosts()` (the truAPI CLI host's state root in that folder) and `fixtureState()`; at the start a replaced generation's people are discarded and those of generations no longer kept swept (`discard.ts`), each said in `lines`. `SavedPeople` (the records, bound to the generation) and `PeopleLifecycle` (Stop and Start keeping wallet, place and account). `tests/people-keep-*.test.ts` |
58
59
  | Default keys | `definition.keys ?? new Sr25519KeyDerivation()`: a lab whose secrets need a signer (`secrets.signer(name)`) does not need to declare a key adapter itself |
59
60
  | Default chain connections | `connectChains` (`src/chain-connections.ts`): one live `ChainConnection` (`@product-lab/chain-papi`'s `PapiChainAccess`) per chain the started environment declares (`RunningEnvironment.routes`, and since 30 September 2026 each chain its offer lists with a loopback WebSocket endpoint but no route, such as a Chopsticks fork: backlog 16, so the lab can check a time advance itself; since 3 October 2026, backlog A2, a routed chain off this machine connects only under the routes' own `publicRoutes` reason, passed as `publicEndpoint`, never without one), given to `SessionContext.chains`/`ActionContext.chains`/`AssertionContext.chains` (core/lab's `CompileOptions.chains`) and closed with the environment. Since 4 October 2026 (backlog 91) it also builds `readers` (`ChainReaders`, given to `AssertionContext.readers`): `statements(chain)` (`RpcStatementStoreReader` on that chain's endpoint) and `bulletin()` (`BulletinContentReader` through the runtime's `JsonRpcClient`), on this machine only (a chain the environment does not run, or one routed off this machine, is refused, without printing its URL); `chainConnector` (same file) is the matching `ChainConnector` a lab-level integration (`defineLab({ integrations })`, for example `@product-lab/deploy-bulletin`'s platform integration) uses instead of building its own `PapiChainAccess`/`PapiTransactionSubmitter` pair |
@@ -277,5 +278,6 @@ binary retain the checkout's routing-patch declaration. This keeps an all-releas
277
278
  patched provisioner for its identities. `tests/people-dev-onboarding.test.ts` checks a verified official cache hit and
278
279
  the older-release fallback with no host process or network request.
279
280
 
280
- Engine setup (`src/engine-setup.ts`) prepares stock v0.11.0 without a patch by default.
281
- The compatible `--no-patch` flag is optional; the release pin and build checks live in env-engine.
281
+ Engine setup (`src/engine-setup.ts`) prepares stock v0.11.0 without a patch by default, or with the node-workspace
282
+ patch the package ships (`--engine-patch node-workspace`). The compatible `--no-patch` flag is optional and refuses
283
+ `--engine-patch`; the release pin and build checks live in env-engine.
@@ -1,7 +1,7 @@
1
1
  // Copyright 2026 Parity Technologies (UK) Ltd.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { parseArgs } from "node:util";
4
- import { defaultEngineRepository, defaultStateDirectory, engineRepository, prepareEngine, setupPaths, testedEngineRef, } from "@parity/product-lab/env-engine";
4
+ import { defaultEngineRepository, defaultStateDirectory, engineRepository, prepareEngine, setupPaths, shippedEnginePatch, testedEngineRef, } from "@parity/product-lab/env-engine";
5
5
  import { NodeSyncCommandRunner } from "@parity/product-lab/system-node";
6
6
  const root = process.cwd();
7
7
  try {
@@ -10,15 +10,21 @@ try {
10
10
  "engine-ref": { type: "string", default: testedEngineRef },
11
11
  "engine-repository": { type: "string", default: defaultEngineRepository },
12
12
  "no-patch": { type: "boolean", default: false },
13
+ "engine-patch": { type: "string", default: "none" },
13
14
  network: { type: "string", default: "paseo-next-v2" },
14
15
  "state-dir": { type: "string", default: defaultStateDirectory },
15
16
  "dry-run": { type: "boolean", default: false },
16
17
  },
17
18
  });
18
- const source = { repository: engineRepository({ repository: values["engine-repository"] }), patch: "none" };
19
+ const asked = ["none", "node-workspace"].find((patch) => patch === values["engine-patch"]);
20
+ if (!asked)
21
+ throw new Error(`--engine-patch takes none or node-workspace, not ${JSON.stringify(values["engine-patch"])}`);
22
+ if (values["no-patch"] && asked !== "none")
23
+ throw new Error(`--no-patch prepares stock Engine: it cannot be combined with --engine-patch ${asked}`);
24
+ const source = { repository: engineRepository({ repository: values["engine-repository"] }), patch: asked };
19
25
  if (values["dry-run"])
20
26
  console.log(JSON.stringify({
21
- ...setupPaths(root, values["engine-ref"], "", values["state-dir"]),
27
+ ...setupPaths(root, values["engine-ref"], source.patch === "none" ? "" : shippedEnginePatch(source.patch).sha256, values["state-dir"]),
22
28
  network: values.network,
23
29
  engineRef: values["engine-ref"],
24
30
  repository: source.repository,
@@ -12,7 +12,7 @@ export { chainConnector, connectChains, transactionSubmitter } from "./chain-con
12
12
  export { deployedRuntimeSource } from "./versions/version-source-catalogue.js";
13
13
  export { downloadLiveCode, liveCodeFetcher, readLiveRuntimes, requireLiveCode } from "./fork-live.js";
14
14
  export { PEOPLE_DEV_GENESIS, peopleDevGenesis } from "./people-dev/genesis.js";
15
- export { DEFAULT_FAST_ONBOARDING, labAttester, MEMBER_READERS, memberStanding, PEOPLE_COLLECTION, PeopleAttesterChain, peopleOnboarding, RING_TIMEOUT_MS, sameKey, truapiCliAllocCheck, type AllocCheck, type FastOnboarding, type LabAttesterDeps, type MemberStanding, type PeopleOnboarding, type PeopleOnboardingOptions } from "./people-dev/onboarding.js";
15
+ export { collectionName, DEFAULT_FAST_ONBOARDING, labAttester, MEMBER_READERS, memberStanding, PEOPLE_COLLECTION, PEOPLE_LITE_COLLECTION, PeopleAttesterChain, peopleOnboarding, RING_TIMEOUT_MS, sameKey, truapiCliAllocCheck, type AllocCheck, type FastOnboarding, type LabAttesterDeps, type MemberStanding, type PeopleOnboarding, type PeopleOnboardingOptions } from "./people-dev/onboarding.js";
16
16
  export { chainCode, indexBytes, papiRingStorage, PERSON_COLLECTIONS, personProductId, personRingEntropy, releasedAllocCheck, RING_LOOKBACK, type MemberKeyOf, type RingStorage } from "./people-dev/member-key.js";
17
17
  export { personReadings, type ReadPerson } from "./people-dev/readings.js";
18
18
  export { allocationFailures, BULLETIN_ALLOWANCES, continuesChain, followBlocks, StandInLedger, standInForXcm, unansweredReason, PROBE_BLOB_STORED, XCM_STAND_IN_LABEL, XcmStandIn, xcmStandIn, type AllocationFailure, type ProbeBlob, type FinalizedBlock, type FinalizedBlocks, type PeopleEvent, type StandInForXcmOptions, type XcmStandInOptions } from "./people-dev/xcm-stand-in.js";
@@ -21,6 +21,7 @@ export { SavedPeople, type SavedPerson } from "./people-keep/saved-people.js";
21
21
  export { PeopleLifecycle, type ActorEntry, type LifecycleActor, type StoppedActor } from "./people-keep/lifecycle.js";
22
22
  export { discardedLine, discardGeneration, replacedFolder, sweepGenerations, sweptLine, type DiscardedGeneration } from "./people-keep/discard.js";
23
23
  export { DEFAULT_FORK_PORT_BASE, paseoCopy, previewnetCopy, savedForkOptions, type NetworkCopyOptions } from "./network-copies.js";
24
+ export { PREVIEWNET_GENESIS_CHAINS, PREVIEWNET_GENESIS_ENGINE_FOLDER, PREVIEWNET_GENESIS_ID, PREVIEWNET_GENESIS_LABEL, PREVIEWNET_GENESIS_RING_COLLECTIONS, PREVIEWNET_GENESIS_STARTUP_MS, prepareGenesisRings, previewnetGenesis, previewnetGenesisEngine, previewnetGenesisEnvironment, type PeopleRingsPrepared, type PrepareRings, type PreviewnetGenesisOptions, type PreviewnetGenesisParts } from "./previewnet-genesis.js";
24
25
  export { forkPortBase, MACHINE_VARIABLES, machineInputs, type MachineInputs } from "./machine-inputs.js";
25
26
  export { attachedForkEnvironment, forkEndpoints, PASEO_FORK_NETWORK, readSnapshotFile } from "./fork-attach.js";
26
27
  export { defaultSessions } from "./default-session.js";
@@ -15,7 +15,7 @@ export { chainConnector, connectChains, transactionSubmitter } from "./chain-con
15
15
  export { deployedRuntimeSource } from "./versions/version-source-catalogue.js";
16
16
  export { downloadLiveCode, liveCodeFetcher, readLiveRuntimes, requireLiveCode } from "./fork-live.js";
17
17
  export { PEOPLE_DEV_GENESIS, peopleDevGenesis } from "./people-dev/genesis.js";
18
- export { DEFAULT_FAST_ONBOARDING, labAttester, MEMBER_READERS, memberStanding, PEOPLE_COLLECTION, PeopleAttesterChain, peopleOnboarding, RING_TIMEOUT_MS, sameKey, truapiCliAllocCheck } from "./people-dev/onboarding.js";
18
+ export { collectionName, DEFAULT_FAST_ONBOARDING, labAttester, MEMBER_READERS, memberStanding, PEOPLE_COLLECTION, PEOPLE_LITE_COLLECTION, PeopleAttesterChain, peopleOnboarding, RING_TIMEOUT_MS, sameKey, truapiCliAllocCheck } from "./people-dev/onboarding.js";
19
19
  export { chainCode, indexBytes, papiRingStorage, PERSON_COLLECTIONS, personProductId, personRingEntropy, releasedAllocCheck, RING_LOOKBACK } from "./people-dev/member-key.js";
20
20
  export { personReadings } from "./people-dev/readings.js";
21
21
  export { allocationFailures, BULLETIN_ALLOWANCES, continuesChain, followBlocks, StandInLedger, standInForXcm, unansweredReason, PROBE_BLOB_STORED, XCM_STAND_IN_LABEL, XcmStandIn, xcmStandIn } from "./people-dev/xcm-stand-in.js";
@@ -24,6 +24,7 @@ export { SavedPeople } from "./people-keep/saved-people.js";
24
24
  export { PeopleLifecycle } from "./people-keep/lifecycle.js";
25
25
  export { discardedLine, discardGeneration, replacedFolder, sweepGenerations, sweptLine } from "./people-keep/discard.js";
26
26
  export { DEFAULT_FORK_PORT_BASE, paseoCopy, previewnetCopy, savedForkOptions } from "./network-copies.js";
27
+ export { PREVIEWNET_GENESIS_CHAINS, PREVIEWNET_GENESIS_ENGINE_FOLDER, PREVIEWNET_GENESIS_ID, PREVIEWNET_GENESIS_LABEL, PREVIEWNET_GENESIS_RING_COLLECTIONS, PREVIEWNET_GENESIS_STARTUP_MS, prepareGenesisRings, previewnetGenesis, previewnetGenesisEngine, previewnetGenesisEnvironment } from "./previewnet-genesis.js";
27
28
  export { forkPortBase, MACHINE_VARIABLES, machineInputs } from "./machine-inputs.js";
28
29
  export { attachedForkEnvironment, forkEndpoints, PASEO_FORK_NETWORK, readSnapshotFile } from "./fork-attach.js";
29
30
  export { defaultSessions } from "./default-session.js";
@@ -112,8 +112,12 @@ export interface LabAttesterDeps {
112
112
  export declare function labAttester(deps: LabAttesterDeps): Omit<PeopleOnboarding, "close">;
113
113
  /** The people collection's identifier in People's `Members` pallet (`pop:polkadot.network/people`, space padded). */
114
114
  export declare const PEOPLE_COLLECTION = "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652020202020";
115
+ /** The lite people collection's identifier in People's `Members` pallet (`pop:polkadot.network/people-lite`, 32 bytes). */
116
+ export declare const PEOPLE_LITE_COLLECTION = "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465";
115
117
  /** Whether an address the chain read is this public key's (any SS58 prefix); false for anything that is no address. */
116
118
  export declare function sameKey(address: unknown, publicKey: Uint8Array): boolean;
119
+ /** A collection identifier as its text when it is printable (`pop:polkadot.network/people-lite`), else as given. */
120
+ export declare function collectionName(identifier: string): string;
117
121
  /**
118
122
  * The lab attester's People connection: `Sudo(People.force_recognize_personhood)` by the dev chain's sudo key //Alice
119
123
  * (the role an identity backend plays on a full network). People's offchain worker then puts the keys in a ring and
@@ -132,11 +136,21 @@ export declare class PeopleAttesterChain {
132
136
  * What a ring needs on a development genesis, each step skipped when the chain already has it:
133
137
  * - People's sudo key is the dev chain's //Alice (else nothing is sent: the lab cannot recognise anyone here);
134
138
  * - the people collection: created at genesis (genesis.ts, `PEOPLE_DEV_GENESIS`);
139
+ * - the ring chunk pages and one person at a time for the people collection (`prepareRings`).
140
+ */
141
+ private preparePeople;
142
+ /** People's sudo key is the dev chain's //Alice, or a refusal before anything is sent. */
143
+ requireDevSudo(): Promise<void>;
144
+ /**
145
+ * What People's rings need on a development genesis before anyone joins one, sent by the dev chain's sudo key //Alice
146
+ * (check it with `requireDevSudo` first). Each step is skipped when the chain already has it, so it may run again on
147
+ * the same chain:
135
148
  * - the R2e9 ZK chunk pages (`people-chunks.json`): `Sudo(Utility.dispatch_as(Authorized,
136
149
  * ChunksManager.add_chunks))`; the chain accepts a page only when its blake2-256 is the one its genesis names;
137
- * - one person at a time: `Sudo(Members.set_onboarding_size(people, 1))`; the offchain worker onboards only cohorts
138
- * of at least that size, which one lab actor at a time never fills.
150
+ * - one person at a time in each of `collections`: `Sudo(Members.set_onboarding_size(<collection>, 1))`; the offchain
151
+ * worker onboards only cohorts of at least that size, which one lab actor at a time never fills.
152
+ * Every transaction sent is returned (none when the chain already had all of it).
139
153
  */
140
- private preparePeople;
154
+ prepareRings(collections: readonly string[]): Promise<TransactionReceipt[]>;
141
155
  close(): void;
142
156
  }
@@ -155,6 +155,7 @@ export function labAttester(deps) {
155
155
  };
156
156
  }
157
157
  export const PEOPLE_COLLECTION = "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652020202020";
158
+ export const PEOPLE_LITE_COLLECTION = "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465";
158
159
  const PEOPLE_CHUNKS = JSON.parse(readFileSync(new URL("./people-chunks.json", import.meta.url), "utf8"));
159
160
  export function sameKey(address, publicKey) {
160
161
  if (typeof address !== "string")
@@ -168,6 +169,10 @@ export function sameKey(address, publicKey) {
168
169
  throw error;
169
170
  }
170
171
  }
172
+ export function collectionName(identifier) {
173
+ const text = Buffer.from(identifier.replace(/^0x/, ""), "hex").toString("utf8").trimEnd();
174
+ return /^[\x20-\x7e]+$/.test(text) ? text : identifier;
175
+ }
171
176
  const DEV_SUDO = "//Alice";
172
177
  export class PeopleAttesterChain {
173
178
  connection;
@@ -193,14 +198,23 @@ export class PeopleAttesterChain {
193
198
  return sent;
194
199
  }
195
200
  async preparePeople() {
196
- const { tx, query } = this.connection.api;
197
- const sudo = tx.Sudo.sudo;
198
- const sent = [];
199
- const key = await query.Sudo?.Key?.getValue({ at: "best" });
200
- if (!sameKey(key, this.authority.publicKey))
201
- throw new Error(`People's sudo key is ${String(key)}, not the dev chain's ${DEV_SUDO} (${this.authority.address}): the lab attester makes persons on the local dev chain only`);
201
+ const { query } = this.connection.api;
202
+ await this.requireDevSudo();
202
203
  if (!(await query.People.PeopleCollectionCreated.getValue({ at: "best" })))
203
204
  throw new Error("People has no people collection: its genesis must create it (genesis edit people.createCollection, PEOPLE_DEV_GENESIS)");
205
+ return this.prepareRings([PEOPLE_COLLECTION]);
206
+ }
207
+ async requireDevSudo() {
208
+ const key = await this.connection.api.query.Sudo?.Key?.getValue({ at: "best" });
209
+ if (!sameKey(key, this.authority.publicKey))
210
+ throw new Error(`People's sudo key is ${String(key)}, not the dev chain's ${DEV_SUDO} (${this.authority.address}): the lab prepares People's rings on a development chain only`);
211
+ }
212
+ async prepareRings(collections) {
213
+ const { tx, query } = this.connection.api;
214
+ const sudo = tx.Sudo?.sudo;
215
+ if (!sudo)
216
+ throw new Error("People runtime has no Sudo.sudo: the lab cannot prepare its rings");
217
+ const sent = [];
204
218
  const exponent = Enum(PEOPLE_CHUNKS.ringExponent);
205
219
  for (const [index, page] of PEOPLE_CHUNKS.pages.entries()) {
206
220
  if ((await query.ChunksManager.Chunks.getValue(exponent, index, { at: "best" })) !== undefined)
@@ -209,9 +223,11 @@ export class PeopleAttesterChain {
209
223
  const authorized = tx.Utility.dispatch_as({ as_origin: Enum("system", Enum("Authorized")), call: add.decodedCall });
210
224
  sent.push(await this.executor.submit(`Add ${PEOPLE_CHUNKS.ringExponent} ZK chunk page ${index}`, sudo({ call: authorized.decodedCall }), this.authority.signer));
211
225
  }
212
- const size = Number(await query.Members.OnboardingSize.getValue(PEOPLE_COLLECTION, { at: "best" }));
213
- if (size !== 1)
214
- sent.push(await this.executor.submit(`Onboard people one at a time (onboarding size ${size} to 1)`, sudo({ call: tx.Members.set_onboarding_size({ identifier: PEOPLE_COLLECTION, onboarding_size: 1 }).decodedCall }), this.authority.signer));
226
+ for (const collection of collections) {
227
+ const size = Number(await query.Members.OnboardingSize.getValue(collection, { at: "best" }));
228
+ if (size !== 1)
229
+ sent.push(await this.executor.submit(`Onboard people one at a time in ${collectionName(collection)} (onboarding size ${size} to 1)`, sudo({ call: tx.Members.set_onboarding_size({ identifier: collection, onboarding_size: 1 }).decodedCall }), this.authority.signer));
230
+ }
215
231
  return sent;
216
232
  }
217
233
  close() {
@@ -0,0 +1,62 @@
1
+ import type { TopologyChain } from "@parity/product-lab/domain";
2
+ import { type EngineCheckoutSettings, type EngineEnvironmentSettings, type EngineTopology } from "@parity/product-lab/env-engine";
3
+ import type { EnvironmentProvider, TopologyEnvironment } from "@parity/product-lab/ports/environment";
4
+ import type { SystemPorts } from "@parity/product-lab/ports/system";
5
+ /** The environment's id unless the lab names another, after the copies' naming (`<network>-copy`). */
6
+ export declare const PREVIEWNET_GENESIS_ID = "previewnet-genesis";
7
+ /** Its name unless the lab names another. */
8
+ export declare const PREVIEWNET_GENESIS_LABEL = "Previewnet from genesis";
9
+ /** The chains it starts unless the lab names others: what a hosted app needs (hosts route People, Asset Hub, Bulletin). */
10
+ export declare const PREVIEWNET_GENESIS_CHAINS: readonly TopologyChain[];
11
+ /** The cache folder its Engine checkout is prepared in (beside the copies' `engine/`). */
12
+ export declare const PREVIEWNET_GENESIS_ENGINE_FOLDER = "engine-node-workspace";
13
+ /** The People collections given an onboarding size of 1 on genesis: the lite people collection, which new people join. */
14
+ export declare const PREVIEWNET_GENESIS_RING_COLLECTIONS: readonly string[];
15
+ /** How long a start may take before Engine's dashboard answers (a first start downloads binaries). */
16
+ export declare const PREVIEWNET_GENESIS_STARTUP_MS = 1800000;
17
+ /** What a lab may change of the environment. */
18
+ export interface PreviewnetGenesisOptions {
19
+ readonly id?: string;
20
+ readonly label?: string;
21
+ readonly description?: string;
22
+ /** Previewnet's chains to start (default PREVIEWNET_GENESIS_CHAINS); People is needed for the ring preparation. */
23
+ readonly chains?: readonly TopologyChain[];
24
+ /** The lab's cache (default PRODUCT_LAB_CACHE_DIR, else the user's cache): the Engine checkout is its `engine-node-workspace/`. */
25
+ readonly cacheDir?: string;
26
+ /** Engine's evidence (logs, session, run manifest), relative to lab.config.ts unless absolute. Default `.product-lab/engine`. */
27
+ readonly reports?: string;
28
+ /** Prepare People's rings for new people after each start (default true). */
29
+ readonly peopleRings?: boolean;
30
+ /** How long a start may take before Engine's dashboard answers (default PREVIEWNET_GENESIS_STARTUP_MS). */
31
+ readonly startupTimeoutMs?: number;
32
+ }
33
+ /** What the ring preparation sent, for the evidence. */
34
+ export interface PeopleRingsPrepared {
35
+ /** Every transaction sent (none when the chain already had all of it). */
36
+ readonly transactions: readonly {
37
+ readonly hash: string;
38
+ readonly block: number;
39
+ }[];
40
+ /** The JSON-lines journal of the preparation. */
41
+ readonly journal: string;
42
+ }
43
+ /** Prepares People's rings on a running genesis network (the default: `PeopleAttesterChain.prepareRings` as //Alice). */
44
+ export type PrepareRings = (people: {
45
+ readonly ws: string;
46
+ readonly journalDirectory: string;
47
+ readonly signal: AbortSignal;
48
+ readonly system: Partial<SystemPorts>;
49
+ }) => Promise<PeopleRingsPrepared>;
50
+ /** What a start composes (tests replace them; nothing else does). */
51
+ export interface PreviewnetGenesisParts {
52
+ readonly engine?: (topology: EngineTopology, settings: EngineEnvironmentSettings, system: Partial<SystemPorts>) => TopologyEnvironment;
53
+ readonly prepareRings?: PrepareRings;
54
+ }
55
+ /** The default ring preparation: //Alice's sudo calls on People, each skipped when the chain already has it. */
56
+ export declare const prepareGenesisRings: PrepareRings;
57
+ /** The Engine checkout settings of the environment `id` with cache `cacheDir`. */
58
+ export declare function previewnetGenesisEngine(id: string, cacheDir: string): EngineCheckoutSettings;
59
+ /** The environment as a provider (what `previewnetGenesis` names and returns under its id). */
60
+ export declare function previewnetGenesisEnvironment(options?: PreviewnetGenesisOptions, parts?: PreviewnetGenesisParts): TopologyEnvironment;
61
+ /** "Previewnet from genesis": an owned Engine network of Previewnet started from genesis on this machine, a new one every start. */
62
+ export declare function previewnetGenesis(options?: PreviewnetGenesisOptions): Record<string, EnvironmentProvider>;
@@ -0,0 +1,127 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { join } from "node:path";
4
+ import { nameEnvironment } from "@parity/product-lab";
5
+ import { ScenarioJournal } from "@parity/product-lab/chain";
6
+ import { engineCheckoutTooling, engineEnvironment, engineProfiles, preparedEngineCheckout, recordedPatch, engineConfiguration, } from "@parity/product-lab/env-engine";
7
+ import { FileEvidenceJournal } from "@parity/product-lab/storage-filesystem";
8
+ import { SystemClock } from "@parity/product-lab/system-node";
9
+ import { productLabCacheDir } from "@parity/product-lab/versions-github";
10
+ import { processEnvironment } from "./create-lab-runtime.js";
11
+ import { PEOPLE_LITE_COLLECTION, PeopleAttesterChain } from "./people-dev/onboarding.js";
12
+ export const PREVIEWNET_GENESIS_ID = "previewnet-genesis";
13
+ export const PREVIEWNET_GENESIS_LABEL = "Previewnet from genesis";
14
+ export const PREVIEWNET_GENESIS_CHAINS = ["people", "asset-hub", "bulletin"];
15
+ export const PREVIEWNET_GENESIS_ENGINE_FOLDER = "engine-node-workspace";
16
+ export const PREVIEWNET_GENESIS_RING_COLLECTIONS = [PEOPLE_LITE_COLLECTION];
17
+ export const PREVIEWNET_GENESIS_STARTUP_MS = 1_800_000;
18
+ const PORT_NAMES = { people: "People", "asset-hub": "Asset Hub", bulletin: "Bulletin", "web3-storage": "Web3 Storage", ethRpc: "eth-rpc", identityBackend: "dub", dashboard: "dashboard" };
19
+ const portWords = (ports) => Object.entries(ports).flatMap(([key, port]) => (port === undefined ? [] : [`${PORT_NAMES[key] ?? key} ${port}`])).join(", ");
20
+ const stamp = () => new Date().toISOString().replace(/[:.]/g, "-");
21
+ export const prepareGenesisRings = async ({ ws, journalDirectory, signal, system }) => {
22
+ const clock = system.clock ?? new SystemClock();
23
+ const evidence = new FileEvidenceJournal(journalDirectory).open();
24
+ const journal = new ScenarioJournal(evidence, () => { }, { clock });
25
+ const chain = new PeopleAttesterChain(ws, journal, signal);
26
+ try {
27
+ await chain.requireDevSudo();
28
+ const sent = await chain.prepareRings(PREVIEWNET_GENESIS_RING_COLLECTIONS);
29
+ return { transactions: sent.map((receipt) => ({ hash: receipt.txHash, block: receipt.block.number })), journal: journal.file };
30
+ }
31
+ finally {
32
+ chain.close();
33
+ }
34
+ };
35
+ export function previewnetGenesisEngine(id, cacheDir) {
36
+ return { state: join(cacheDir, PREVIEWNET_GENESIS_ENGINE_FOLDER), network: "previewnet", patch: "node-workspace", environment: id };
37
+ }
38
+ export function previewnetGenesisEnvironment(options = {}, parts = {}) {
39
+ const id = options.id ?? PREVIEWNET_GENESIS_ID;
40
+ const env = processEnvironment();
41
+ const cacheDir = options.cacheDir ?? productLabCacheDir({ PRODUCT_LAB_CACHE_DIR: env.get("PRODUCT_LAB_CACHE_DIR"), XDG_CACHE_HOME: env.get("XDG_CACHE_HOME") });
42
+ const chains = [...(options.chains ?? PREVIEWNET_GENESIS_CHAINS)];
43
+ const topology = engineProfiles.genesisSubset({ chains });
44
+ const engine = previewnetGenesisEngine(id, cacheDir);
45
+ const rings = options.peopleRings !== false && chains.includes("people");
46
+ const makeEngine = parts.engine ?? engineEnvironment;
47
+ const prepareRings = parts.prepareRings ?? prepareGenesisRings;
48
+ return {
49
+ label: options.label ?? PREVIEWNET_GENESIS_LABEL,
50
+ description: options.description ?? `Previewnet's ${chains.join(", ")} started from genesis on this machine by Engine (a relay chain, collators and Engine's services), a new network every start, on Engine's default ports (${portWords(topology.ports)}, and its others): one Engine network per machine at a time${rings ? "; People's rings are prepared for new people once it is ready" : ""}`,
51
+ topology,
52
+ offer: topology.offer,
53
+ tooling: engineCheckoutTooling(engine),
54
+ async start(context) {
55
+ const system = context.system ?? {};
56
+ if (!system.syncCommands)
57
+ throw new Error(`The ${id} environment checks its Engine checkout through the lab's sync command adapter (SystemPorts.syncCommands), which this lab lacks`);
58
+ const prepared = preparedEngineCheckout(engine, system.syncCommands);
59
+ const environment = makeEngine(topology, {
60
+ engineConfig: engineConfiguration.parse({
61
+ checkout: prepared.checkout,
62
+ workspace: prepared.workspace,
63
+ network: "previewnet",
64
+ mode: "genesis",
65
+ extraPath: prepared.extraPath,
66
+ enginePatch: recordedPatch(prepared.build),
67
+ startupTimeoutMs: options.startupTimeoutMs ?? PREVIEWNET_GENESIS_STARTUP_MS,
68
+ }),
69
+ ...(options.reports ? { reports: options.reports } : {}),
70
+ }, system);
71
+ const running = await environment.start(context);
72
+ const preparations = [];
73
+ const prepare = async (signal) => {
74
+ if (!rings)
75
+ return;
76
+ const ws = running.endpoints?.people;
77
+ if (!ws)
78
+ throw new Error(`The ${id} environment started without a People endpoint, so People's rings cannot be prepared`);
79
+ const reports = running.provenance?.reports;
80
+ const journalDirectory = join(typeof reports === "string" ? reports : join(context.root, ".product-lab", "engine"), `people-rings-${stamp()}`);
81
+ context.progress?.("People: preparing its rings for new people (ZK chunk pages, one person at a time)");
82
+ const done = await prepareRings({ ws, journalDirectory, signal, system });
83
+ preparations.push(done);
84
+ context.progress?.(`People: rings ready (${done.transactions.length} transaction${done.transactions.length === 1 ? "" : "s"} sent)`);
85
+ };
86
+ try {
87
+ await prepare(context.signal);
88
+ }
89
+ catch (error) {
90
+ await running.close();
91
+ throw error;
92
+ }
93
+ const wrapped = Object.defineProperties({}, Object.getOwnPropertyDescriptors(running));
94
+ const last = () => preparations.at(-1);
95
+ Object.defineProperties(wrapped, {
96
+ notices: {
97
+ enumerable: true,
98
+ get: () => {
99
+ const done = last();
100
+ const words = done && (done.transactions.length ? `${done.transactions.length} transaction${done.transactions.length === 1 ? "" : "s"} by //Alice (${done.journal})` : "nothing to send, the chain had it");
101
+ return [...(running.notices ?? []), ...(words ? [`People's rings prepared for new people: ${words}`] : [])];
102
+ },
103
+ },
104
+ provenance: {
105
+ enumerable: true,
106
+ get: () => ({ ...(running.provenance ?? {}), ...(last() ? { peopleRings: last() } : {}) }),
107
+ },
108
+ });
109
+ if (running.reset) {
110
+ const reset = running.reset.bind(running);
111
+ Object.defineProperty(wrapped, "reset", {
112
+ enumerable: true,
113
+ value: async (signal) => {
114
+ preparations.length = 0;
115
+ await reset(signal);
116
+ await prepare(signal);
117
+ },
118
+ });
119
+ }
120
+ return wrapped;
121
+ },
122
+ };
123
+ }
124
+ export function previewnetGenesis(options = {}) {
125
+ const id = options.id ?? PREVIEWNET_GENESIS_ID;
126
+ return { [id]: nameEnvironment(previewnetGenesisEnvironment(options), { kind: "fast-local", label: options.label ?? PREVIEWNET_GENESIS_LABEL }) };
127
+ }