@parity/product-lab 0.2.0 → 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.
- package/CHANGELOG.md +12 -0
- package/adapters/environment/engine/README.md +2 -1
- package/adapters/environment/engine/src/index.d.ts +3 -2
- package/adapters/environment/engine/src/index.js +3 -2
- package/adapters/environment/engine/src/setup/engine-build.d.ts +1 -1
- package/adapters/environment/engine/src/setup/engine-build.js +2 -2
- package/adapters/environment/engine/src/setup/engine-tooling.d.ts +33 -0
- package/adapters/environment/engine/src/setup/engine-tooling.js +59 -0
- package/adapters/environment/engine/src/setup/node-workspace.patch +403 -0
- package/adapters/environment/engine/src/setup/patch-catalogue.d.ts +18 -1
- package/adapters/environment/engine/src/setup/patch-catalogue.js +3 -0
- package/adapters/environment/engine/src/setup/patches.d.ts +15 -1
- package/adapters/environment/engine/src/setup/patches.js +17 -2
- package/adapters/environment/engine/src/setup/prepare-engine.d.ts +1 -1
- package/adapters/environment/engine/src/setup/prepare-engine.js +2 -2
- package/composition/README.md +6 -4
- package/composition/src/engine-setup.js +9 -3
- package/composition/src/index.d.ts +2 -1
- package/composition/src/index.js +2 -1
- package/composition/src/people-dev/onboarding.d.ts +17 -3
- package/composition/src/people-dev/onboarding.js +25 -9
- package/composition/src/previewnet-genesis.d.ts +62 -0
- package/composition/src/previewnet-genesis.js +127 -0
- package/core/application/src/runs/failure-rules.js +5 -2
- package/entrypoints/cli/src/setup-commands.d.ts +1 -1
- package/entrypoints/cli/src/setup-commands.js +3 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ declare const patchEntry: z.ZodObject<{
|
|
|
9
9
|
export type PatchEntry = z.infer<typeof patchEntry>;
|
|
10
10
|
/** Repository-level `patches/` directory, shared by every provider. */
|
|
11
11
|
export declare const patchesDirectory: string;
|
|
12
|
-
/** Why a
|
|
12
|
+
/** Why a repository patch's manifest cannot be read here: this installation carries no patches/ folder. */
|
|
13
13
|
export declare const ENGINE_PATCHES_REPOSITORY_ONLY: string;
|
|
14
14
|
/** A patch or manifest was asked for where patches/<upstream>/ does not exist (an installed package). */
|
|
15
15
|
export declare class EnginePatchesNotInstalled extends Error {
|
|
@@ -34,4 +34,18 @@ export declare function resolvePatch(upstream: string, name: string): {
|
|
|
34
34
|
base: string;
|
|
35
35
|
repository: string;
|
|
36
36
|
};
|
|
37
|
+
/** The Engine patch the package ships (adapters/environment/engine/src/setup/), read here and digest-checked. */
|
|
38
|
+
export declare const shippedPatchesDirectory: string;
|
|
39
|
+
/**
|
|
40
|
+
* One Engine patch the package ships (`SHIPPED_ENGINE_PATCHES`), from its file beside the catalogue, refused when the
|
|
41
|
+
* file differs from the catalogue's digest. The same file in product-lab's checkout and in an installed package;
|
|
42
|
+
* `directory` is for tests (a changed copy is refused).
|
|
43
|
+
*/
|
|
44
|
+
export declare function shippedEnginePatch(name: string, directory?: string): {
|
|
45
|
+
name: string;
|
|
46
|
+
path: string;
|
|
47
|
+
sha256: "a4b866a6da4cbb636bd614271c9691280490b00172dae02a8b113ad161055757";
|
|
48
|
+
base: "e11792b234ca0c39a5fc1d47af491f82fc8e2670";
|
|
49
|
+
repository: "https://github.com/paritytech/previewnet-engine.git";
|
|
50
|
+
};
|
|
37
51
|
export {};
|
|
@@ -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 = "
|
|
17
|
-
"Prepare stock Engine
|
|
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,7 +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`:
|
|
32
|
+
* `node-workspace`: the native-node workspace fix at the tested base, which the package ships (patch-catalogue.ts).
|
|
33
33
|
* Default `none`. The retired `owned-processes` value is refused.
|
|
34
34
|
*/
|
|
35
35
|
readonly patch?: EnginePatch;
|
|
@@ -4,7 +4,7 @@ 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 {
|
|
7
|
+
import { shippedEnginePatch } from "./patches.js";
|
|
8
8
|
import { ENGINE_RELEASE, NODE_WORKSPACE_PATCH } from "./patch-catalogue.js";
|
|
9
9
|
import { ENGINE_PATCH_RETIRED, engineBuildIssue } from "./engine-build.js";
|
|
10
10
|
import { applyTurnRelayChoice, turnRelayAvailable } from "../engine/turn-relay.js";
|
|
@@ -45,7 +45,7 @@ export function prepareEngine(root, ref, network, commands, stateDirectory = def
|
|
|
45
45
|
if (source.patch && source.patch !== "none" && source.patch !== "node-workspace")
|
|
46
46
|
throw new Error(ENGINE_PATCH_RETIRED);
|
|
47
47
|
const patch = source.patch ?? "none";
|
|
48
|
-
const patchFile = patch === "node-workspace" ?
|
|
48
|
+
const patchFile = patch === "node-workspace" ? shippedEnginePatch(patch) : undefined;
|
|
49
49
|
if (patchFile && (patchFile.sha256 !== NODE_WORKSPACE_PATCH.sha256 || patchFile.base !== NODE_WORKSPACE_PATCH.base))
|
|
50
50
|
throw new Error("node-workspace patch differs from its tested catalogue");
|
|
51
51
|
const output = source.output ?? "inherit";
|
package/composition/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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";
|
package/composition/src/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
197
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
}
|
|
@@ -104,7 +104,7 @@ const RULES = [
|
|
|
104
104
|
id: "environment-unreachable",
|
|
105
105
|
layer: "environment",
|
|
106
106
|
confidence: "medium",
|
|
107
|
-
test: /(ECONNREFUSED|ECONNRESET|WebSocket|socket hang up|endpoint|not running|cannot start|failed to start|spawn|EADDRINUSE|address already in use|Environment \S+ reports no|No \S+ lab was started for|genesis|baseline)/i,
|
|
107
|
+
test: /(ECONNREFUSED|ECONNRESET|WebSocket|socket hang up|endpoint|not running|cannot start|failed to start|spawn|EADDRINUSE|address already in use|Environment \S+ reports no|No \S+ lab was started for|share a genesis|duplicate genesis|baseline requires|baseline integrity mismatch|fast[- ]baseline|is another chain|genesis \S+ is not)/i,
|
|
108
108
|
expected: () => "the environment is running and reachable",
|
|
109
109
|
actual: (m) => `it is not (${m[1]})`,
|
|
110
110
|
hint: "Infrastructure, not the product: a chain, service or process is down or misconfigured. env_status and tail_logs show which; a negative control failing this way is not a valid result.",
|
|
@@ -168,10 +168,11 @@ export function classifyFailure(error, title, hook = false, control = false) {
|
|
|
168
168
|
actual: firstLine(error),
|
|
169
169
|
hint: "A Before/After hook failed: the environment or the scenario context, not a scenario step. tail_logs on the run.",
|
|
170
170
|
};
|
|
171
|
+
const message = messageLines(error);
|
|
171
172
|
for (const rule of RULES) {
|
|
172
173
|
if (control && rule.layer === "environment")
|
|
173
174
|
continue;
|
|
174
|
-
const match = rule.test.exec(
|
|
175
|
+
const match = rule.test.exec(message);
|
|
175
176
|
if (!match)
|
|
176
177
|
continue;
|
|
177
178
|
const generic = genericValues(error);
|
|
@@ -194,4 +195,6 @@ export function classifyFailure(error, title, hook = false, control = false) {
|
|
|
194
195
|
hint: "No rule matched this error. Read the full error, step_evidence for the step and tail_logs for the run.",
|
|
195
196
|
};
|
|
196
197
|
}
|
|
198
|
+
const STACK_FRAME = /^\s+at\s.*(?::\d+:\d+\)?|\((?:<anonymous>|native)\)|<anonymous>)\s*$/;
|
|
199
|
+
const messageLines = (error) => error.split("\n").filter((line) => !STACK_FRAME.test(line)).join("\n");
|
|
197
200
|
export const firstLine = (text) => text.split("\n").find((line) => line.trim())?.trim().slice(0, 500) ?? "";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LabRuntime } from "@parity/product-lab/ports/driving";
|
|
2
2
|
import type { SyncCommandRunner } from "@parity/product-lab/ports/system";
|
|
3
3
|
import type { VariableReader } from "./environment.js";
|
|
4
|
-
export declare const FETCH_USAGE = "Usage: product-lab fetch [--config lab.config.ts] [--environment <id>]... [--json]\n [--engine-repository <url|path>] [--engine-ref <ref>] [--no-engine-patch]\n\nDownload and verify every release file the lab's lab.lock.json pins for this machine (polkadot-omni-node,\nruntime Wasm) into the shared cache (PRODUCT_LAB_CACHE_DIR, default ~/.cache/product-lab). A file already\ncached and verified is not downloaded again.\n\nThen prepare, in the same cache, what the lab's default environment needs on this machine and never makes\nitself, and what each --environment needs (repeat it, or --environment all): a local copy of a network\n(paseo-copy, previewnet-copy) starts from an Engine checkout, cloned at the tested stock Engine release (or --engine-ref), with no lab patch,\nbuilt, with its network's binaries and the pinned dot CLI (<cache>/engine; Git and network access, a few\nminutes and about 900 MB the first time).
|
|
4
|
+
export declare const FETCH_USAGE = "Usage: product-lab fetch [--config lab.config.ts] [--environment <id>]... [--json]\n [--engine-repository <url|path>] [--engine-ref <ref>] [--no-engine-patch]\n\nDownload and verify every release file the lab's lab.lock.json pins for this machine (polkadot-omni-node,\nruntime Wasm) into the shared cache (PRODUCT_LAB_CACHE_DIR, default ~/.cache/product-lab). A file already\ncached and verified is not downloaded again.\n\nThen prepare, in the same cache, what the lab's default environment needs on this machine and never makes\nitself, and what each --environment needs (repeat it, or --environment all): a local copy of a network\n(paseo-copy, previewnet-copy) starts from an Engine checkout, cloned at the tested stock Engine release (or --engine-ref), with no lab patch,\nbuilt, with its network's binaries and the pinned dot CLI (<cache>/engine; Git and network access, a few\nminutes and about 900 MB the first time). Previewnet from genesis (previewnet-genesis) starts from its own\ncheckout, the same release with the node-workspace patch the package ships (<cache>/engine-node-workspace).\nOne already prepared is only checked. Another Engine:\n --engine-repository <url|path> clone it from this Git URL or local repository instead of GitHub\n --engine-ref <ref> this commit, branch or tag instead of the tested release; starts use it as\n prepared; doctor names it as not the tested release (cache:engine) and fails\n when the copy is asked for (the default environment or --environment)\n --no-engine-patch compatibility flag; stock Engine is the default and the old patch is retired\n\nExit 0 when every file is cached and everything asked for is prepared, 1 otherwise.\nIn the product-lab checkout, `bun run setup` runs this for every example.";
|
|
5
5
|
export declare const DOCTOR_USAGE = "Usage: product-lab doctor [--config lab.config.ts] [--environment <id>]... [--json]\n\nWhat this machine has and lacks to run the lab: Node.js, the shared cache, the lab's locked release files,\nthe selected browser (the full Chromium executable for the default launcher), what its environments need\nprepared (the Engine checkout of a local copy of a network: required for the default environment and each\n--environment, a note for the others), the Engine build the shared cache holds checked against the pinned\nrelease whatever the lab declares (cache:engine: another build, such as the retired patched one, fails where the lab\nwould start from it, as the Engine checkout line does, else a note; a cache holding no Engine says nothing) and the hosts its people run on (in a product, the truAPI WASM test host the package installs and the truAPI CLI host's locked official release:\ncached or not, what it provides; in the product-lab checkout, also Bun and its truAPI CLI host). Then what fits\nthis machine, from its cores, memory and free disk: the lab's local copy of a network as Full or Light, how\nmany people at once on each environment, and whether the cache is set up. Starts nothing, downloads nothing. Exit 0 when everything is\npresent, 1 otherwise, with the one command that fixes it; what fits never changes the exit code.";
|
|
6
6
|
/** The command that fixes a missing piece, for the product-lab checkout. */
|
|
7
7
|
export declare const SETUP_FIX = "bun run setup";
|
|
@@ -20,7 +20,9 @@ Then prepare, in the same cache, what the lab's default environment needs on thi
|
|
|
20
20
|
itself, and what each --environment needs (repeat it, or --environment all): a local copy of a network
|
|
21
21
|
(paseo-copy, previewnet-copy) starts from an Engine checkout, cloned at the tested stock Engine release (or --engine-ref), with no lab patch,
|
|
22
22
|
built, with its network's binaries and the pinned dot CLI (<cache>/engine; Git and network access, a few
|
|
23
|
-
minutes and about 900 MB the first time).
|
|
23
|
+
minutes and about 900 MB the first time). Previewnet from genesis (previewnet-genesis) starts from its own
|
|
24
|
+
checkout, the same release with the node-workspace patch the package ships (<cache>/engine-node-workspace).
|
|
25
|
+
One already prepared is only checked. Another Engine:
|
|
24
26
|
--engine-repository <url|path> clone it from this Git URL or local repository instead of GitHub
|
|
25
27
|
--engine-ref <ref> this commit, branch or tag instead of the tested release; starts use it as
|
|
26
28
|
prepared; doctor names it as not the tested release (cache:engine) and fails
|
package/package.json
CHANGED