wicked-garden 12.43.0 → 12.44.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -1
- package/docs/cross-cli-skill-format.md +6 -8
- package/package.json +1 -1
- package/scripts/demo/walkthrough-lint.mjs +248 -0
- package/scripts/demo/walkthrough.mjs +40 -20
- package/scripts/mcp/install.py +443 -0
- package/scripts/mcp/openapi.py +200 -0
- package/scripts/mcp/probe.py +39 -10
- package/scripts/mcp/scaffold.py +71 -3
- package/skills/core/SKILL.md +1 -1
- package/skills/demo/SKILL.md +21 -1
- package/skills/demo/refs/walkthrough-author.md +83 -0
- package/skills/governed-worker/SKILL.md +20 -4
- package/skills/mcp-scaffold/SKILL.md +190 -38
- package/skills/mcp-scaffold/assets/typescript/LICENSE-HEADER.txt +2 -0
- package/skills/mcp-scaffold/assets/typescript/README.md +71 -0
- package/skills/mcp-scaffold/assets/typescript/env.example +18 -0
- package/skills/mcp-scaffold/assets/typescript/eslint.config.js +11 -0
- package/skills/mcp-scaffold/assets/typescript/gitignore +3 -0
- package/skills/mcp-scaffold/assets/typescript/mcp-server.config.json +14 -0
- package/skills/mcp-scaffold/assets/typescript/package.json +46 -0
- package/skills/mcp-scaffold/assets/typescript/src/auth.ts +98 -0
- package/skills/mcp-scaffold/assets/typescript/src/config.ts +90 -0
- package/skills/mcp-scaffold/assets/typescript/src/logging.ts +57 -0
- package/skills/mcp-scaffold/assets/typescript/src/rest.ts +160 -0
- package/skills/mcp-scaffold/assets/typescript/src/server.ts +52 -0
- package/skills/mcp-scaffold/assets/typescript/src/telemetry.ts +65 -0
- package/skills/mcp-scaffold/assets/typescript/src/tools/example.ts +22 -0
- package/skills/mcp-scaffold/assets/typescript/src/tools/generated.ts +64 -0
- package/skills/mcp-scaffold/assets/typescript/src/tools/registry.ts +76 -0
- package/skills/mcp-scaffold/assets/typescript/test/conformance.test.ts +112 -0
- package/skills/mcp-scaffold/assets/typescript/test/contract/example.test.ts +75 -0
- package/skills/mcp-scaffold/assets/typescript/test/global-setup.ts +10 -0
- package/skills/mcp-scaffold/assets/typescript/tools.json +3 -0
- package/skills/mcp-scaffold/assets/typescript/tsconfig.json +17 -0
- package/skills/mcp-scaffold/assets/typescript/vitest.config.ts +11 -0
- package/skills/mem/SKILL.md +7 -7
- package/skills/repo-learn/SKILL.md +19 -18
- package/skills/search/SKILL.md +47 -7
- package/skills/search/codebase-narrator/SKILL.md +4 -2
- package/skills/wickedizer/SKILL.md +4 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, real multi-model second opinions. Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.
|
|
1
|
+
{"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, real multi-model second opinions. Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.44.0"}], "version": "12.44.0"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.44.0",
|
|
4
4
|
"description": "Your coding agent's harness already plans and swarms; Wicked Garden is the curated toolkit for what it can't do alone. Proof, not claims \u2014 gates re-derive 'done' through the internal loom engine (fail-closed, independently attested via wicked-vault), so a self-asserted pass can't lie its way green. Relationships grep can't see \u2014 the wicked-estate code graph + injected bus/dispatch/capability/archetype edges power blast-radius & lineage. Plus deterministic multi-file refactor (wicked-patch), cross-session memory + knowledge (the mem domain over wicked-estate), real multi-model second opinions (jam:council), portable expertise as on-demand skill-refs, and evidence-gated testing (the in-catalog qe domain). It reads the shape of your work to apply the right rigor \u2014 steering, not a pipeline \u2014 then gets out of the way. The compiler stamps a self-contained evidence gate into any repo that runs with no wicked-garden installed. Don't fight the harness; fill its gaps. The evidence gate requires wicked-vault, a self-contained infra peer wicked-garden installs directly; wicked-bus is an opt-in layer \u2014 install what you'll use, the toolkit works without the rest.",
|
|
5
5
|
"wicked_vault_version": "^0.5.0",
|
|
6
6
|
"wicked_ledger_version": "^0.4.0",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.44.0] — 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`mcp-scaffold`: the `openapi` and `install` actions (DES-mcp-server-workflow rev 2, slice 2 of 2).** `scripts/mcp/openapi.py` turns an OpenAPI 3 document into the server's `tools.json` through wicked-crew's conversion service — `POST /api/v1/mcp/servers/preview` with `{name, kind: "rest", url, openapiUrl | openapi, operations?}` at `--crew-url`, else `$WICKED_CREW_URL`, else `http://127.0.0.1:7701` — writing `{"source": {specUrl|specFile, baseUrl, convertedAt}, "tools": [{name, description, inputSchema, annotations, class, rest}], "skipped": [...]}` and setting `baseUrl` in `mcp-server.config.json`; it refuses to overwrite generated tools, never sends a `secret`/`auth` key, exits 1 carrying the daemon's refusal and exits 2 when no daemon answers ("write the tools by hand (branch B)") — it never invents tools. `scripts/mcp/install.py` installs or updates the built server for running, idempotent by the config's `key`: it locates the one `mcp-server.config.json` (`--from-run` under `$WICKED_TREE`, or `--dir`), builds and stages `dist/` + manifests into `<root>/<key>/next/` (`~/.wicked/mcp-servers` unless `--install-root`/`$WICKED_MCP_INSTALL_ROOT`; `--from-npm <pkg>@<ver>` installs a published package instead), smokes it with `probe.py --env <SERVER>_TOKEN` (a failure naming that variable is `pending_credential`, disclosed; any other failure exits 1 with `current/` untouched), swaps `current/` → `previous/` and `next/` → `current/` so the command `node <root>/<key>/current/dist/server.js` never changes, registers it in crew's MCP registry with `auth: {ref: "env:<SERVER>_TOKEN"}` — the one-secret contract: a reference the daemon resolves, never a value (`secret_missing` → `registered: false` with both remedies; no daemon → the reason; an existing key → `registered: "updated"`), writes the operator's CLI MCP configs only through `wicked-installer mcp upsert` (an installer without the verb is reported as skipped), and records `<root>/<key>/installed.json` `{key, version, source, command, args, envNames, installedAt, registered, smoke}`. `--uninstall <key>` deletes the registration, the CLI entries and the root. Exit 0 staged, 1 staging/smoke/registry failure, 2 bad arguments. The skill gains both actions, the `mcp-server` workflow's per-phase guide (scope, source-discovery, design, build, install) and the "Use when" phrases; the core router row is now `scaffold · openapi · probe · install`. Tests: `tests/mcp/test_mcp_openapi.py`, `tests/mcp/test_mcp_install.py` (stub `npm` and `wicked-installer` on PATH, a local http.server as the daemon, a real smoke probe).
|
|
9
|
+
- **`mcp-scaffold`: a TypeScript server template, now the default (`scaffold.py --lang typescript`; DES-mcp-server-workflow rev 2, slice 1 of 2).** `skills/mcp-scaffold/assets/typescript/` is a whole project: fastmcp 4 on Node >= 22, loglayer (OpenTelemetry plugin, redaction by name, a console transport bound to stderr — stdout carries protocol frames only — and a request-scoped child logger through `AsyncLocalStorage`), OpenTelemetry through `@opentelemetry/sdk-node` (spans `mcp.tool.call` and `http.client.request`, instruments `mcp.tool.calls`/`mcp.tool.duration`/`mcp.tool.errors`; the OTLP trace, metric and log exporters are armed only when `OTEL_EXPORTER_OTLP_ENDPOINT` is set, otherwise nothing leaves the process), and authentication on every tool (`canAccess`; httpStream refuses a request without `MCP_SERVER_BEARER` with 401). The server reads ONE secret, `<SERVER>_TOKEN` (bearer token, API key, basic password or OAuth2 client secret, as the scheme says) — the one variable crew's broker and registry inject — and fails at startup naming it; every non-secret parameter lives in the committed `mcp-server.config.json` (`key`, `version`, `transport`, `baseUrl`, `auth`, `rateLimitPerMin`). `defineTool` wraps each tool in the span, the child logger, the instruments and a per-tool token bucket; `src/tools/generated.ts` registers every `tools.json` entry (the conversion service's output) with annotations derived from its class, calling `src/rest.ts`, which pins scheme/host/port/base path, sends allowlisted arguments only, never follows a redirect, refuses auth and hop-by-hop headers from arguments and retries 429/5xx for read tools only. `npm test` builds and runs a stdio conformance smoke (initialize, tools/list, tools/call; a missing secret exits non-zero naming the variable; httpStream 401) and a per-tool contract-test shape. Every `.ts` file starts with an SPDX line. `scaffold.py` copies the directory stamping the name, its env prefix and the year; `--out` may be an existing directory, but every file it would write must be absent — it refuses naming the first collision and writes nothing. `node` and `python` stay as the dependency-free one-file templates. `probe.py --env NAME` passes exactly that variable through to the server (never printed) and, on a failure, reports only which of those names the server's stderr mentioned (`stderrNames`). The skill gains the skeleton's contract and a "Steering this server follows" checklist (MCPS-1001…1007, sourced from wicked-core's `governance/packs/mcp-server`). Tests: `tests/mcp/test_mcp_scaffold.py` (default lang, stamping, SPDX, no secret values, scaffold into a populated dir, collision refusal, `--env` passthrough; the npm install/build/lint/test/probe end-to-end behind `WICKED_GARDEN_E2E_NPM=1`).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Governed-worker floor: foreground evidence, tool debris under the notes root, verdict scope (wicked-garden#1229, #1223).** New **C6**: no turn ends with tasks pending — the done-when recipe runs in order in the foreground with each command and exit code pasted; "running in the background" is never a closing line (three creator attempts in run ad5a4ca7 ended that way, F35). New **C7**: reporter output, coverage, screenshots, logs and scratch files go under the unit's notes root (`$WICKED_NOTES_ROOT` or the path the brief names), never in the repository — pass the tool's output flag or delete the file before the turn ends (S17b left a 2.3 MB `.vitest/json/output.json` in the worktree; crew's deliver skip-list, wicked-crew#861, is the backstop). **C2** now reads: a failure called pre-existing cites its run on the base tree (command + exit code) or it is a failure. New **E3**: every verdict item is in scope unless a human ruling strikes it by name (companion to wicked-core#760). Provenance rows added; `tests/test_governed_worker_rules.py` pins the text.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Walkthrough: the `lint` action the `walkthrough_plan` pin calls now exists, and `record` reads the storyline the author wrote (wicked-garden#1231, the garden half of wicked-core#753).** The engine pins `walkthrough_plan` to `"${WICKED_GARDEN_ROOT}/scripts/wicked-garden" run scripts/demo/walkthrough.mjs lint --root "${WICKED_EVIDENCE_ROOT}"`, but `walkthrough.mjs` accepted only `record|seal` and exited 2 with usage — so every walkthrough was denied, storyline or not. `lint --root <author dir>` (new `scripts/demo/walkthrough-lint.mjs`, DES-walkthrough-proof §4.5) imports `<author dir>/storyline.mjs`, prints `{ok, storyline, chapters, checks, findings: [{rule, where, detail}]}` and exits 0 only with no finding (1 with any, 2 on a usage error; `--root` defaults to `$WICKED_EVIDENCE_ROOT`). It refuses: a missing or unloadable storyline; an empty plan; `baseUrl` other than `"fixture"`; a `fixture.start` that is not a plain argv, runs inline code (`-e`, `-c`, `--eval`, ...), names an absolute path or `..`, or (with `--tree <dir>`) a script the tree does not hold; a secret-shaped `fixture.env` name; a chapter without `proves` step ids (with `--steps a,b`, ids outside the plan), without a `locator` (on_screen) check, without a `probe`/`artifact` (state) check, or without a `join` (cross_check) of the two; a walkthrough without a `guard` (must_not_happen) check; unknown check kinds, a locator without a selector, a probe not in `fixture.probes`, an artifact path escaping `DATA_DIR`, a join not naming two earlier checks; a `verify` that is not `jq_pred` or that `jq` rejects; and a check with no `negative` sample or whose verifier PASSES one of its samples (a tautology; run through `jq -e` exactly as the vault does, failing closed without `jq`). `record` took its storyline from `<dirname(WICKED_EVIDENCE_ROOT)>/author/<WICKED_RUN_UNIT>/` — the unit **ordinal** — so a storyline written where the engine said was reported "storyline not found"; it now reads `--storyline`, else `$WICKED_WALKTHROUGH_AUTHOR/storyline.mjs` (the author dir wicked-core's `record_launch` hands it), and with neither refuses `storyline_refused` naming both. The demo skill gains a walkthrough-author section and `refs/walkthrough-author.md` (where the file goes, every rule id, a passing example the tests lint). Tests: `tests/demo/test_walkthrough_lint.py` (27: a clean pass, the exact pinned launcher command, one per rule, the documented example, a storyline that throws while read still gets a report), `tests/demo/test_walkthrough.py` (4: the author dir is read with an ordinal `WICKED_RUN_UNIT`, a missing author storyline names its path, no source refuses by name, the unjailed result lists the author storyline's chapters). The record tool does not yet re-run the lint before recording (§4.4 step 1); that follows separately.
|
|
16
|
+
- **`mcp-scaffold` TypeScript template: the conformance smoke no longer times out on a loaded host.** Loading fastmcp and the OpenTelemetry SDK can take well over 10 s when the machine is busy; the stdio exchange now waits up to 60 s, the httpStream readiness poll up to 60 s, `testTimeout` is 90 s, and the npm end-to-end test (`WICKED_GARDEN_E2E_NPM=1`) probes with `--timeout 30`.
|
|
17
|
+
- **CI no longer installs the retired `wicked-loom` package; the trust-spine tests need only node + wicked-vault (closes the wicked-garden#1132 residual).** Loom runs in-process (`scripts/loom/`, absorbed from wicked-loom), but `.github/workflows/test.yml` still ran `npm i -g wicked-vault wicked-loom` and `tests/qe/test_prove.py` / `tests/e2e/test_e2e_gate_cli.py` skipped (or, under `WICKED_REQUIRE_E2E=1`, failed) unless a `wicked-loom` binary was on PATH — and then pinned `WICKED_LOOM_BIN` to it, so CI exercised the deprecated external binary instead of the in-process gate garden ships. The tests now require node + a resolvable vault only, pass `WICKED_LOOM_BIN` through only when the caller set it, and CI installs `wicked-vault` alone. `docs/cross-cli-skill-format.md` no longer describes the deleted cross-CLI baseline or its `--write-baseline` flag, and the repo's agent guide (`.claude/CLAUDE.md`) no longer calls wicked-loom a required peer. Proof: `WICKED_REQUIRE_E2E=1 python3 -m pytest tests/qe/test_prove.py tests/e2e/test_e2e_gate_cli.py` → 17 passed with only `wicked-vault` on PATH.
|
|
18
|
+
|
|
5
19
|
## [12.43.0] — 2026-10-05
|
|
6
20
|
|
|
7
21
|
### Fixed
|
|
@@ -980,7 +994,8 @@ through `v9.2.18`. Major themes across that span:
|
|
|
980
994
|
|
|
981
995
|
For each of those releases, see the corresponding tag in git history.
|
|
982
996
|
|
|
983
|
-
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.
|
|
997
|
+
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.44.0...HEAD
|
|
998
|
+
[12.44.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.43.0...v12.44.0
|
|
984
999
|
[12.43.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.42.0...v12.43.0
|
|
985
1000
|
[12.42.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.41.0...v12.42.0
|
|
986
1001
|
[12.41.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.40.0...v12.41.0
|
|
@@ -4,8 +4,8 @@ wicked-garden is installed into Claude Code, Codex, OpenCode, Pi and Antigravity
|
|
|
4
4
|
parties and snapshotted by wicked-crew for every seat. Only Claude Code reads Claude Code's
|
|
5
5
|
plugin frontmatter — so a skill's identity, dispatch shape and prose must not depend on it.
|
|
6
6
|
This page is the format; `scripts/_skill_meta.py` is its one reader; the lint
|
|
7
|
-
(`tests/test_skill_portability.py`) enforces it
|
|
8
|
-
|
|
7
|
+
(`tests/test_skill_portability.py`) enforces it, strictly: the 12.37.x translation batches are
|
|
8
|
+
done and the baseline they burned down was deleted (B18), so every hit fails.
|
|
9
9
|
|
|
10
10
|
## Frontmatter: the closed key set
|
|
11
11
|
|
|
@@ -75,8 +75,8 @@ generically instead.
|
|
|
75
75
|
|
|
76
76
|
## Products and size
|
|
77
77
|
|
|
78
|
-
`wicked-testing`, `wicked-brain`, `wicked-signals` are retired and `wicked-loom`
|
|
79
|
-
by the in-process gate — name their successors (the `qe` domain, wicked-estate through the `mem` /
|
|
78
|
+
`wicked-testing`, `wicked-brain`, `wicked-signals` are retired and `wicked-loom` was replaced (CI no longer installs it; DES-L6 B6 / review F3)
|
|
79
|
+
by the in-process gate (`scripts/loom/`) — name their successors (the `qe` domain, wicked-estate through the `mem` /
|
|
80
80
|
`search` skills, the built-in gate) or mark the line `<!-- historical -->` (`retired-product-ref`).
|
|
81
81
|
A SKILL.md stays under 500 lines and 24 KiB (`skill-too-large`; `scripts/ci/validate.py` applies
|
|
82
82
|
the same bound to routers and modules); detail goes in `refs/`. Skill-relative paths, the
|
|
@@ -84,9 +84,7 @@ the same bound to routers and modules); detail goes in `refs/`. Skill-relative p
|
|
|
84
84
|
|
|
85
85
|
## Tooling
|
|
86
86
|
|
|
87
|
-
- `python3 tests/test_skill_portability.py` — the report (0 violations
|
|
88
|
-
|
|
89
|
-
only**; afterwards the file only shrinks (a batch deletes the entries it translated; a stale entry
|
|
90
|
-
fails CI; B18 deletes the file).
|
|
87
|
+
- `python3 tests/test_skill_portability.py` — the report (0 violations). There is no baseline: it
|
|
88
|
+
was deleted at B18 and `test_cross_cli_baseline_is_gone` fails if one comes back.
|
|
91
89
|
- `python3 scripts/ci/validate.py && python3 scripts/_validate_registry.py` — the release gate,
|
|
92
90
|
role-aware.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.44.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Curated toolkit for what AI coding agents can't do alone: evidence-gated work, codegraph relationships, deterministic multi-file refactor, multi-model second opinions, and cross-session memory. Installs as a Claude Code plugin.",
|
|
6
6
|
"keywords": [
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Walkthrough storyline lint (DES-walkthrough-proof §4.5; wicked-garden#1231).
|
|
2
|
+
//
|
|
3
|
+
// wicked-garden run scripts/demo/walkthrough.mjs lint --root <author dir> [--storyline <path>]
|
|
4
|
+
// [--tree <dir>] [--steps <id,id,...>]
|
|
5
|
+
//
|
|
6
|
+
// The `walkthrough_plan` step's pinned validator runs this against the author dir the engine hands it
|
|
7
|
+
// (wicked-core `WALKTHROUGH_LINT_SCRIPT`: `--root "${WICKED_EVIDENCE_ROOT}"`, where that root is
|
|
8
|
+
// `<evidence root>/author/<plan step>`). It reads `<root>/storyline.mjs`, prints one JSON object
|
|
9
|
+
// `{ok, storyline, chapters, checks, findings: [{rule, where, detail}]}` on stdout and exits 0 only when
|
|
10
|
+
// there is no finding; 1 when there is at least one; 2 on a usage error.
|
|
11
|
+
//
|
|
12
|
+
// A check's `kind` is the record tool's collector (`runCollector` in walkthrough.mjs). The DES's check
|
|
13
|
+
// classes map onto them: locator = on_screen, probe/artifact = saved_state/events/side_effects/output,
|
|
14
|
+
// guard = must_not_happen, join = cross_check.
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { pathToFileURL } from "node:url";
|
|
20
|
+
|
|
21
|
+
export const CHECK_CLASS = Object.freeze({
|
|
22
|
+
__proto__: null,
|
|
23
|
+
locator: "on_screen",
|
|
24
|
+
probe: "state",
|
|
25
|
+
artifact: "state",
|
|
26
|
+
guard: "must_not_happen",
|
|
27
|
+
join: "cross_check",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** The verifier the record tool hands the vault for a check (kept in step with walkthrough.mjs verifierForCheck). */
|
|
31
|
+
export function defaultVerifier(kind) {
|
|
32
|
+
switch (kind) {
|
|
33
|
+
case "locator": return { kind: "jq_pred", params: { expr: ".inner_text != null" } };
|
|
34
|
+
case "probe": return { kind: "jq_pred", params: { expr: ".raw.exit_code == 0" } };
|
|
35
|
+
case "artifact": return { kind: "jq_pred", params: { expr: ".sha256 != null" } };
|
|
36
|
+
case "guard": return { kind: "jq_pred", params: { expr: "(.foreign_writes | length) == 0 and ((.console_errors // []) | length) == 0" } };
|
|
37
|
+
case "join": return { kind: "jq_pred", params: { expr: ".joined | to_entries | all(.value != null)" } };
|
|
38
|
+
default: return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SEGMENT_KEY = /^[a-z0-9][a-z0-9-]*$/;
|
|
43
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
44
|
+
const PLAIN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
45
|
+
// Env names a storyline may not hand the fixture: credentials never reach the jailed app (§7).
|
|
46
|
+
const SECRET_ENV = /(SECRET|TOKEN|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|CREDENTIAL|AUTH)/i;
|
|
47
|
+
// Inline-code flags: `node -e`, `python -c`, `sh -c`, ... — the fixture must be a script the tree declares.
|
|
48
|
+
const INLINE_FLAGS = new Set(["-e", "-c", "--eval", "-p", "--print", "--command"]);
|
|
49
|
+
const SCRIPT_LIKE = /\.(?:m?js|cjs|ts|mts|py|sh|rb|pl)$/i;
|
|
50
|
+
|
|
51
|
+
/** The first line of a thrown value's message; never throws itself (a storyline can throw anything). */
|
|
52
|
+
function why(e) {
|
|
53
|
+
try { return String(e?.message ?? e).split("\n")[0]; } catch { return "the storyline threw a value that cannot be read"; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasDotDot(p) { return /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p); }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Lint a loaded storyline object. Pure apart from running `jq` for the negative samples.
|
|
60
|
+
* @param {object} story the storyline's default export
|
|
61
|
+
* @param {{tree?: string|null, steps?: string[]|null, jq?: string}} opts
|
|
62
|
+
* @returns {{findings: {rule: string, where: string, detail: string}[], chapters: number, checks: number}}
|
|
63
|
+
*/
|
|
64
|
+
export function lintStoryline(story, { tree = null, steps = null, jq = "jq" } = {}) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
const add = (rule, where, detail) => findings.push({ rule, where, detail });
|
|
67
|
+
|
|
68
|
+
if (!story || typeof story !== "object") {
|
|
69
|
+
add("storyline_shape", "default export", "the storyline's default export must be an object");
|
|
70
|
+
return { findings, chapters: 0, checks: 0 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- baseUrl + fixture ------------------------------------------------------------------------------
|
|
74
|
+
if (story.baseUrl !== "fixture") {
|
|
75
|
+
add("base_url", "baseUrl", `baseUrl must be "fixture" (the record tool films the fixture it starts), got ${JSON.stringify(story.baseUrl)}`);
|
|
76
|
+
}
|
|
77
|
+
const fixture = story.fixture;
|
|
78
|
+
if (!fixture || typeof fixture !== "object") {
|
|
79
|
+
add("fixture_start", "fixture", "fixture is required: { start: [argv], ready, probes?, env? }");
|
|
80
|
+
} else {
|
|
81
|
+
const start = fixture.start;
|
|
82
|
+
if (!Array.isArray(start) || start.length === 0 || !start.every((a) => typeof a === "string" && a.length > 0)) {
|
|
83
|
+
add("fixture_start", "fixture.start", "fixture.start must be a non-empty argv array of strings (no shell)");
|
|
84
|
+
} else {
|
|
85
|
+
for (const arg of start.slice(1)) {
|
|
86
|
+
if (INLINE_FLAGS.has(arg)) add("fixture_start", "fixture.start", `inline code (${arg}) is refused: start a script the tree declares`);
|
|
87
|
+
}
|
|
88
|
+
for (const arg of start) {
|
|
89
|
+
if (path.isAbsolute(arg) || /^[A-Za-z]:[\\/]/.test(arg)) add("fixture_start", "fixture.start", `${JSON.stringify(arg)} is an absolute path; the fixture runs from app/ (the tree)`);
|
|
90
|
+
else if (hasDotDot(arg)) add("fixture_start", "fixture.start", `${JSON.stringify(arg)} leaves app/ (the tree)`);
|
|
91
|
+
}
|
|
92
|
+
if (tree) {
|
|
93
|
+
for (const arg of start.slice(1)) {
|
|
94
|
+
if (arg.startsWith("-") || path.isAbsolute(arg) || hasDotDot(arg)) continue;
|
|
95
|
+
if (!(arg.includes("/") || SCRIPT_LIKE.test(arg))) continue;
|
|
96
|
+
if (!fs.existsSync(path.join(tree, arg))) add("fixture_start", "fixture.start", `${JSON.stringify(arg)} is not in the tree (${tree})`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (fixture.env !== undefined) {
|
|
101
|
+
if (!fixture.env || typeof fixture.env !== "object" || Array.isArray(fixture.env)) {
|
|
102
|
+
add("fixture_env", "fixture.env", "fixture.env must be an object of name: value");
|
|
103
|
+
} else {
|
|
104
|
+
for (const name of Object.keys(fixture.env)) {
|
|
105
|
+
if (SECRET_ENV.test(name)) add("secret_env", `fixture.env.${name}`, "a secret-shaped env name is refused: no credential reaches the jailed app");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (fixture.probes !== undefined && (!fixture.probes || typeof fixture.probes !== "object" || Array.isArray(fixture.probes))) {
|
|
110
|
+
add("fixture_probes", "fixture.probes", "fixture.probes must be an object of name: argv");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const probes = fixture && typeof fixture.probes === "object" && fixture.probes ? fixture.probes : {};
|
|
114
|
+
|
|
115
|
+
// ---- segments ---------------------------------------------------------------------------------------
|
|
116
|
+
const segments = Array.isArray(story.segments) ? story.segments : null;
|
|
117
|
+
if (!segments) {
|
|
118
|
+
add("segments", "segments", "segments must be an array");
|
|
119
|
+
return { findings, chapters: 0, checks: 0 };
|
|
120
|
+
}
|
|
121
|
+
const chapters = segments.filter((s) => s && !s.intro);
|
|
122
|
+
if (chapters.length === 0) add("segments", "segments", "the storyline has no chapter (an empty plan proves nothing)");
|
|
123
|
+
|
|
124
|
+
const seenKeys = new Set();
|
|
125
|
+
let checkCount = 0;
|
|
126
|
+
let mustNotHappen = 0;
|
|
127
|
+
|
|
128
|
+
segments.forEach((seg, i) => {
|
|
129
|
+
const where = `segments[${i}]${seg && typeof seg.key === "string" ? ` (${seg.key})` : ""}`;
|
|
130
|
+
if (!seg || typeof seg !== "object") { add("segment_shape", where, "a segment must be an object"); return; }
|
|
131
|
+
if (!SEGMENT_KEY.test(String(seg.key ?? ""))) add("segment_key", where, `key ${JSON.stringify(seg.key)}: lowercase letters, digits and hyphens only`);
|
|
132
|
+
else if (seenKeys.has(seg.key)) add("segment_key", where, `key ${JSON.stringify(seg.key)} is used twice`);
|
|
133
|
+
seenKeys.add(seg.key);
|
|
134
|
+
if (typeof seg.run !== "function") add("segment_run", where, "run(ctx) must be a function");
|
|
135
|
+
if (seg.intro) return;
|
|
136
|
+
|
|
137
|
+
// proves: plan step ids
|
|
138
|
+
if (!Array.isArray(seg.proves) || seg.proves.length === 0) {
|
|
139
|
+
add("proves", where, "proves[] must name at least one plan step id");
|
|
140
|
+
} else {
|
|
141
|
+
for (const id of seg.proves) {
|
|
142
|
+
if (typeof id !== "string" || !PLAIN_ID.test(id)) add("proves", where, `proves id ${JSON.stringify(id)} is not a plain step id`);
|
|
143
|
+
else if (steps && !steps.includes(id)) add("proves", where, `proves id ${JSON.stringify(id)} is not a step of the plan (${steps.join(", ")})`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const checks = Array.isArray(seg.checks) ? seg.checks : null;
|
|
148
|
+
if (!checks || checks.length === 0) { add("chapter_checks", where, "a chapter needs checks[]"); return; }
|
|
149
|
+
const byId = new Map();
|
|
150
|
+
const classes = { on_screen: [], state: [], must_not_happen: [], cross_check: [] };
|
|
151
|
+
|
|
152
|
+
checks.forEach((c, j) => {
|
|
153
|
+
const cw = `${where}.checks[${j}]${c && typeof c.id === "string" ? ` (${c.id})` : ""}`;
|
|
154
|
+
if (!c || typeof c !== "object") { add("check_shape", cw, "a check must be an object"); return; }
|
|
155
|
+
checkCount += 1;
|
|
156
|
+
if (!SAFE_ID.test(String(c.id ?? ""))) add("check_id", cw, `id ${JSON.stringify(c.id)}: letters, digits, '.', '_' and '-' only`);
|
|
157
|
+
else if (byId.has(c.id)) add("check_id", cw, `id ${JSON.stringify(c.id)} is used twice in this chapter`);
|
|
158
|
+
const cls = typeof c.kind === "string" && Object.hasOwn(CHECK_CLASS, c.kind) ? CHECK_CLASS[c.kind] : null;
|
|
159
|
+
if (!cls) { add("check_kind", cw, `kind ${JSON.stringify(c.kind)} is not one of ${Object.keys(CHECK_CLASS).join(", ")}`); return; }
|
|
160
|
+
classes[cls].push(c.id);
|
|
161
|
+
if (cls === "must_not_happen") mustNotHappen += 1;
|
|
162
|
+
|
|
163
|
+
// Collector inputs
|
|
164
|
+
if (c.kind === "locator" && (typeof c.selector !== "string" || !c.selector.trim())) add("check_selector", cw, "a locator check needs a selector");
|
|
165
|
+
if (c.kind === "probe") {
|
|
166
|
+
if (typeof c.name !== "string" || !c.name) add("check_probe", cw, "a probe check needs name (a key of fixture.probes)");
|
|
167
|
+
else if (!Object.prototype.hasOwnProperty.call(probes, c.name)) add("check_probe", cw, `probe ${JSON.stringify(c.name)} is not in fixture.probes`);
|
|
168
|
+
if (c.parse !== undefined && !["json", "jsonl", "sqlite-json", "text"].includes(c.parse)) add("check_probe", cw, `parse ${JSON.stringify(c.parse)} is not json | jsonl | sqlite-json | text`);
|
|
169
|
+
}
|
|
170
|
+
if (c.kind === "artifact") {
|
|
171
|
+
const p = c.path;
|
|
172
|
+
if (typeof p !== "string" || !p) add("check_artifact", cw, "an artifact check needs path (relative to DATA_DIR)");
|
|
173
|
+
else if (path.isAbsolute(p) || hasDotDot(p)) add("check_artifact", cw, `artifact path ${JSON.stringify(p)} escapes DATA_DIR`);
|
|
174
|
+
}
|
|
175
|
+
if (c.kind === "join") {
|
|
176
|
+
const src = Array.isArray(c.sources) ? c.sources : [];
|
|
177
|
+
if (src.length < 2) add("check_join", cw, "a join needs sources[] naming at least two earlier checks");
|
|
178
|
+
for (const s of src) if (!byId.has(s)) add("check_join", cw, `source ${JSON.stringify(s)} is not an earlier check of this chapter`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Verifier + negative samples (S3): the verifier must FAIL on every sample.
|
|
182
|
+
const verifier = c.verify === undefined ? defaultVerifier(c.kind) : c.verify;
|
|
183
|
+
const expr = verifier?.params?.expr;
|
|
184
|
+
if (!verifier || verifier.kind !== "jq_pred" || typeof expr !== "string" || !expr.trim()) {
|
|
185
|
+
add("check_verify", cw, "verify must be { kind: \"jq_pred\", params: { expr } } — the one verifier the record tool hands the vault");
|
|
186
|
+
} else {
|
|
187
|
+
const negs = c.negative;
|
|
188
|
+
if (!Array.isArray(negs) || negs.length === 0) {
|
|
189
|
+
add("negative_missing", cw, "every check needs at least one negative sample (a capture its verifier must FAIL)");
|
|
190
|
+
} else {
|
|
191
|
+
negs.forEach((sample, k) => {
|
|
192
|
+
const r = spawnSync(jq, ["-e", expr], { input: JSON.stringify(sample), encoding: "utf8", timeout: 10_000 });
|
|
193
|
+
if (r.error) add("negative_unverified", `${cw}.negative[${k}]`, `jq could not run (${r.error.code ?? r.error.message}); the lint fails closed`);
|
|
194
|
+
else if (r.status === 0) add("negative_passes", `${cw}.negative[${k}]`, `the verifier \`${expr}\` PASSES this sample, so the check cannot fail (a tautology)`);
|
|
195
|
+
else if (r.status !== 1 && r.status !== 4) add("check_verify", cw, `jq rejected \`${expr}\` (exit ${r.status}): ${(r.stderr || "").trim().slice(0, 200)}`);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
byId.set(c.id, c);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (classes.on_screen.length === 0) add("chapter_on_screen", where, "a chapter needs at least one on_screen check (kind: locator)");
|
|
203
|
+
if (classes.state.length === 0) add("chapter_state", where, "a chapter needs at least one saved_state/events/side_effects/output check (kind: probe or artifact)");
|
|
204
|
+
if (classes.on_screen.length && classes.state.length) {
|
|
205
|
+
const joined = checks.some((c) => c?.kind === "join" && Array.isArray(c.sources)
|
|
206
|
+
&& c.sources.some((s) => classes.on_screen.includes(s)) && c.sources.some((s) => classes.state.includes(s)));
|
|
207
|
+
if (!joined) add("chapter_cross_check", where, "a chapter with on_screen and state checks needs a join (cross_check) whose sources include one of each");
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (chapters.length && mustNotHappen === 0) add("must_not_happen", "segments", "the walkthrough needs at least one must_not_happen check (kind: guard)");
|
|
212
|
+
|
|
213
|
+
return { findings, chapters: chapters.length, checks: checkCount };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The `lint` action: resolve, import and lint the storyline; print the report; return the exit code. */
|
|
217
|
+
export async function lintAction({ root, storyline, tree, steps }) {
|
|
218
|
+
const storyPath = storyline ? path.resolve(storyline) : root ? path.join(path.resolve(root), "storyline.mjs") : null;
|
|
219
|
+
const report = (findings, extra = {}) => {
|
|
220
|
+
const out = { ok: findings.length === 0, storyline: storyPath, chapters: 0, checks: 0, ...extra, findings };
|
|
221
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
222
|
+
for (const f of findings) process.stderr.write(`lint: ${f.rule} at ${f.where}: ${f.detail}\n`);
|
|
223
|
+
return findings.length === 0 ? 0 : 1;
|
|
224
|
+
};
|
|
225
|
+
if (!storyPath) {
|
|
226
|
+
process.stderr.write("usage: walkthrough.mjs lint --root <author dir> [--storyline <path>] [--tree <dir>] [--steps <id,...>]\n");
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
let st;
|
|
230
|
+
try { st = fs.statSync(storyPath); } catch {
|
|
231
|
+
return report([{ rule: "storyline_missing", where: storyPath, detail: "no storyline here: the walkthrough_plan author writes <author dir>/storyline.mjs" }]);
|
|
232
|
+
}
|
|
233
|
+
if (!st.isFile()) return report([{ rule: "storyline_missing", where: storyPath, detail: "not a regular file" }]);
|
|
234
|
+
let story;
|
|
235
|
+
try {
|
|
236
|
+
story = (await import(pathToFileURL(storyPath).href)).default;
|
|
237
|
+
} catch (e) {
|
|
238
|
+
return report([{ rule: "storyline_unloadable", where: storyPath, detail: why(e) }]);
|
|
239
|
+
}
|
|
240
|
+
let result;
|
|
241
|
+
try {
|
|
242
|
+
result = lintStoryline(story, { tree: tree ? path.resolve(tree) : null, steps });
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// A storyline that throws while being read (a getter, a proxy) is a finding, never a crash without a report.
|
|
245
|
+
return report([{ rule: "storyline_unreadable", where: storyPath, detail: why(e) }]);
|
|
246
|
+
}
|
|
247
|
+
return report(result.findings, { chapters: result.chapters, checks: result.checks });
|
|
248
|
+
}
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
//
|
|
4
4
|
// wicked-garden run scripts/demo/walkthrough.mjs record [--storyline <path>]
|
|
5
5
|
// wicked-garden run scripts/demo/walkthrough.mjs seal --root <dir>
|
|
6
|
+
// wicked-garden run scripts/demo/walkthrough.mjs lint --root <author dir> [--storyline <path>] [--tree <dir>] [--steps <id,...>]
|
|
6
7
|
//
|
|
7
8
|
// Required env for record: WICKED_EVIDENCE_ROOT, WICKED_RUN_ID, WICKED_RUN_UNIT, WICKED_TREE
|
|
9
|
+
// The storyline: --storyline <path>, else $WICKED_WALKTHROUGH_AUTHOR/storyline.mjs (the walkthrough_plan step's
|
|
10
|
+
// author dir, which the engine hands the record tool); with neither, record refuses (storyline_refused).
|
|
11
|
+
// lint (walkthrough-lint.mjs, DES §4.5) is the walkthrough_plan step's pinned validator: exit 0 = no finding.
|
|
8
12
|
// Safety: WICKED_WALKTHROUGH_JAIL must be set; without it writes INCONCLUSIVE(unjailed_host) and exits 0.
|
|
9
13
|
//
|
|
10
14
|
// What the proof root holds is ONE contract with crew's walkthrough view and acceptance
|
|
@@ -39,6 +43,7 @@ import { tmpdir } from "node:os";
|
|
|
39
43
|
import { makeSegmentRecorder, failedAtInVideo, stitchParts, extractFrame, SEGMENT_TRIM_START } from "./record.mjs";
|
|
40
44
|
import { fixtureOrigin } from "./readonly.mjs";
|
|
41
45
|
import { crewRunStamp } from "../qe/lib/crew-run.mjs";
|
|
46
|
+
import { lintAction } from "./walkthrough-lint.mjs";
|
|
42
47
|
|
|
43
48
|
// ---- arg parsing ---------------------------------------------------------------
|
|
44
49
|
const [action, ...rawArgs] = process.argv.slice(2);
|
|
@@ -48,11 +53,22 @@ function argVal(flag) {
|
|
|
48
53
|
return i >= 0 ? rawArgs[i + 1] ?? null : null;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
if (action !== "record" && action !== "seal") {
|
|
52
|
-
process.stderr.write("usage: walkthrough.mjs <record|seal> [--storyline <path>] [--root <dir>]\n");
|
|
56
|
+
if (action !== "record" && action !== "seal" && action !== "lint") {
|
|
57
|
+
process.stderr.write("usage: walkthrough.mjs <record|seal|lint> [--storyline <path>] [--root <dir>]\n");
|
|
53
58
|
process.exit(2);
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
// ---- lint action (standalone; the walkthrough_plan step's pinned validator) ------
|
|
62
|
+
if (action === "lint") {
|
|
63
|
+
const steps = argVal("--steps");
|
|
64
|
+
process.exit(await lintAction({
|
|
65
|
+
root: argVal("--root") ?? process.env.WICKED_EVIDENCE_ROOT ?? null,
|
|
66
|
+
storyline: argVal("--storyline"),
|
|
67
|
+
tree: argVal("--tree"),
|
|
68
|
+
steps: steps ? steps.split(",").map((x) => x.trim()).filter(Boolean) : null,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
// ---- seal action (standalone, no env required) ---------------------------------
|
|
57
73
|
if (action === "seal") {
|
|
58
74
|
const sealRoot = argVal("--root") ?? process.env.WICKED_EVIDENCE_ROOT;
|
|
@@ -92,10 +108,13 @@ function tryStaticSegmentKeys(filePath) {
|
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
// Resolve storyline path early so the jail check can enumerate chapters.
|
|
111
|
+
// --storyline wins; else the author dir the engine hands the record tool (wicked-core `record_launch`). Not the
|
|
112
|
+
// unit ordinal (WICKED_RUN_UNIT): the author writes under its own plan step's id (wicked-garden#1231).
|
|
113
|
+
const AUTHOR_DIR = process.env.WICKED_WALKTHROUGH_AUTHOR ? path.resolve(process.env.WICKED_WALKTHROUGH_AUTHOR) : null;
|
|
95
114
|
const _earlyStoryPath = (() => {
|
|
96
115
|
const arg = argVal("--storyline");
|
|
97
116
|
if (arg) return path.resolve(arg);
|
|
98
|
-
return path.join(
|
|
117
|
+
return AUTHOR_DIR ? path.join(AUTHOR_DIR, "storyline.mjs") : null;
|
|
99
118
|
})();
|
|
100
119
|
|
|
101
120
|
// ---- jail check ----------------------------------------------------------------
|
|
@@ -560,11 +579,14 @@ function judgeChapter(seg, captures, failedAtSec, { crossCheck = null, vaultEntr
|
|
|
560
579
|
// Claim not recorded in vault (never reached)
|
|
561
580
|
claimVerdict = "FAIL";
|
|
562
581
|
} else {
|
|
563
|
-
//
|
|
564
|
-
//
|
|
582
|
+
// Precedence: (1) hash_ok === false — a tampered / mismatched payload is a FAIL whatever
|
|
583
|
+
// the verifier said; (2) verifier_status "error" (jq absent, verifier crashed) — the vault
|
|
584
|
+
// folds it into `result: "FAIL"`, but DES §4.5 says an environment fault is INCONCLUSIVE,
|
|
585
|
+
// never a product failure; (3) the folded `result` ("PASS"|"MISSING"|"FAIL"|"ERROR",
|
|
586
|
+
// uppercase), falling back to verifier_status ("pass"|"fail"|"error") for an older vault.
|
|
565
587
|
const res = vc.result?.toUpperCase();
|
|
566
588
|
const vs = vc.verifier_status?.toUpperCase();
|
|
567
|
-
const r = res ?? vs;
|
|
589
|
+
const r = vc.hash_ok === false ? "FAIL" : vs === "ERROR" ? "ERROR" : (res ?? vs);
|
|
568
590
|
claimVerdict = r === "PASS" ? "PASS"
|
|
569
591
|
: r === "MISSING" || r === "FAIL" ? "FAIL"
|
|
570
592
|
: "INCONCLUSIVE";
|
|
@@ -607,13 +629,17 @@ function judgeChapter(seg, captures, failedAtSec, { crossCheck = null, vaultEntr
|
|
|
607
629
|
function claimDetail(claimVerdict, cap, vaultClaim, vaultAvailable, vaultError) {
|
|
608
630
|
if (!vaultAvailable) return "vault unavailable: no cross-check could run";
|
|
609
631
|
if (vaultError) return "vault cross-check returned ERROR";
|
|
632
|
+
const why = vaultClaim && [vaultClaim.reason, vaultClaim.message, vaultClaim.detail].find((v) => typeof v === "string" && v);
|
|
633
|
+
const vaultSays = (status) => (why ? `vault ${status}: ${why}` : `vault ${status}`);
|
|
634
|
+
// The vault verified an artifact for this claim: its finding names the cause, whatever the capture
|
|
635
|
+
// looked like (mirrors the verdict precedence in judgeChapter).
|
|
636
|
+
if (vaultClaim?.hash_ok === false) return vaultSays("hash mismatch");
|
|
637
|
+
if (vaultClaim && String(vaultClaim.verifier_status ?? "").toUpperCase() === "ERROR") return vaultSays("verifier error");
|
|
610
638
|
if (cap?._verifier_error) return `collector error: ${cap._verifier_error}`;
|
|
611
639
|
if (!cap) return "never reached: ctx.check was not called for this id";
|
|
612
640
|
if (!vaultClaim) return "not recorded in the vault";
|
|
613
641
|
if (claimVerdict === "PASS") return null;
|
|
614
|
-
|
|
615
|
-
const why = [vaultClaim.reason, vaultClaim.message, vaultClaim.detail].find((v) => typeof v === "string" && v);
|
|
616
|
-
return why ? `vault ${status}: ${why}` : `vault ${status}`;
|
|
642
|
+
return vaultSays(vaultClaim.result ?? vaultClaim.verifier_status ?? claimVerdict);
|
|
617
643
|
}
|
|
618
644
|
|
|
619
645
|
/** The root-relative files behind a capture, as crew serves them (`/walkthrough/file?path=`). */
|
|
@@ -845,18 +871,12 @@ async function main() {
|
|
|
845
871
|
? parseInt(process.env.WICKED_WALKTHROUGH_DISK_CAP, 10) : 0;
|
|
846
872
|
|
|
847
873
|
// Locate storyline
|
|
848
|
-
const
|
|
849
|
-
let storyPath;
|
|
850
|
-
if (storyArgPath) {
|
|
851
|
-
storyPath = path.resolve(storyArgPath);
|
|
852
|
-
} else {
|
|
853
|
-
const authorDir = path.join(path.dirname(ROOT), "author", STEP_ID);
|
|
854
|
-
const autoPath = path.join(authorDir, "storyline.mjs");
|
|
855
|
-
storyPath = autoPath;
|
|
856
|
-
}
|
|
874
|
+
const storyPath = _earlyStoryPath;
|
|
857
875
|
|
|
858
|
-
if (!fs.existsSync(storyPath)) {
|
|
859
|
-
|
|
876
|
+
if (!storyPath || !fs.existsSync(storyPath)) {
|
|
877
|
+
const reason = storyPath ? `storyline not found: ${storyPath}`
|
|
878
|
+
: "no storyline: pass --storyline <path> or set WICKED_WALKTHROUGH_AUTHOR (the walkthrough_plan author dir)";
|
|
879
|
+
writeResult(ROOT, { overall: "INCONCLUSIVE", cause: "storyline_refused", reason, chapters: [] });
|
|
860
880
|
writeProgress(ROOT, { overall: "INCONCLUSIVE", cause: "storyline_refused" });
|
|
861
881
|
finishWithSeal(ROOT, { tree: TREE, storyline_sha: null, contract_shas: {}, overall: "INCONCLUSIVE", cause: "storyline_refused", chapters: [] });
|
|
862
882
|
process.exit(0);
|