@motujs/cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/bundled/github/agents/archipelago-build.agent.md +298 -0
- package/bundled/github/agents/island-create.agent.md +261 -0
- package/bundled/github/agents/island-extract.agent.md +360 -0
- package/bundled/github/agents/island-locate.agent.md +208 -0
- package/bundled/github/agents/motu-checks.agent.md +158 -0
- package/bundled/github/agents/motu-evidence.agent.md +267 -0
- package/bundled/github/agents/motu-regions.agent.md +304 -0
- package/bundled/github/host-rules.md +43 -0
- package/bundled/github/prompts/archipelago-build.prompt.md +19 -0
- package/bundled/github/prompts/island-create.prompt.md +37 -0
- package/bundled/github/prompts/island-extract.prompt.md +59 -0
- package/bundled/github/prompts/island-locate.prompt.md +43 -0
- package/bundled/packages/adapters/angularjs/vite.mjs +43 -0
- package/bundled/packages/adapters/next/lagoon/next-stubs.tsx +108 -0
- package/bundled/packages/adapters/next/vite.mjs +121 -0
- package/bundled/packages/adapters/vite/vite.mjs +179 -0
- package/package.json +64 -3
- package/src/alias-loader.mjs +63 -0
- package/src/alias-register.mjs +28 -0
- package/src/asset-stub.cjs +3 -0
- package/src/asset-stub.mjs +5 -0
- package/src/cli.mjs +78 -0
- package/src/commands/adopt-root.mjs +194 -0
- package/src/commands/archipelago-init.mjs +314 -0
- package/src/commands/archipelago.mjs +138 -0
- package/src/commands/assurance.mjs +40 -0
- package/src/commands/check.mjs +346 -0
- package/src/commands/codegen.mjs +29 -0
- package/src/commands/coverage.mjs +358 -0
- package/src/commands/create.mjs +277 -0
- package/src/commands/defaults.mjs +129 -0
- package/src/commands/diff.mjs +595 -0
- package/src/commands/fixtures.mjs +153 -0
- package/src/commands/init.mjs +473 -0
- package/src/commands/integrate.mjs +208 -0
- package/src/commands/integration.mjs +1349 -0
- package/src/commands/lagoon-try.mjs +272 -0
- package/src/commands/lagoon.mjs +1419 -0
- package/src/commands/link.mjs +110 -0
- package/src/commands/region-coverage.mjs +604 -0
- package/src/commands/removal-check.mjs +878 -0
- package/src/commands/skills.mjs +156 -0
- package/src/commands/snapshot.mjs +456 -0
- package/src/commands/sync.mjs +38 -0
- package/src/commands/verify/island-runtime.mjs +581 -0
- package/src/commands/verify/island-static.mjs +586 -0
- package/src/commands/verify/island.mjs +135 -0
- package/src/commands/verify/reach.mjs +426 -0
- package/src/commands/verify/region-composition.mjs +391 -0
- package/src/commands/verify/region-config.mjs +330 -0
- package/src/commands/verify/region-flows.mjs +760 -0
- package/src/commands/verify/region-sources.mjs +516 -0
- package/src/commands/verify/region.mjs +271 -0
- package/src/commands/verify/report.mjs +338 -0
- package/src/commands/verify/screens-runtime.mjs +123 -0
- package/src/commands/verify.mjs +18 -0
- package/src/lagoon-build.mjs +25 -0
- package/src/lagoon-dev.mjs +32 -0
- package/src/lib/alias-core.mjs +49 -0
- package/src/lib/archipelagos.mjs +111 -0
- package/src/lib/assurance.mjs +144 -0
- package/src/lib/baselines.mjs +107 -0
- package/src/lib/bundlability.mjs +123 -0
- package/src/lib/changed.mjs +157 -0
- package/src/lib/component-props.mjs +145 -0
- package/src/lib/config.mjs +451 -0
- package/src/lib/contracts.mjs +244 -0
- package/src/lib/data-flow.mjs +47 -0
- package/src/lib/declared-sources.mjs +101 -0
- package/src/lib/dock-inject.mjs +47 -0
- package/src/lib/effects.mjs +97 -0
- package/src/lib/eject.mjs +715 -0
- package/src/lib/evidence-json.mjs +74 -0
- package/src/lib/expect-calls.mjs +53 -0
- package/src/lib/finding-memory.mjs +0 -0
- package/src/lib/flow-calls.mjs +53 -0
- package/src/lib/framework.mjs +98 -0
- package/src/lib/host-rules.mjs +55 -0
- package/src/lib/host-sources.mjs +121 -0
- package/src/lib/island-placement.mjs +64 -0
- package/src/lib/island-render.mjs +105 -0
- package/src/lib/islands.mjs +112 -0
- package/src/lib/lagoon-bundle.mjs +138 -0
- package/src/lib/lagoon-declares.mjs +553 -0
- package/src/lib/lagoon-materialize.mjs +217 -0
- package/src/lib/lagoon-vite.mjs +788 -0
- package/src/lib/live-url.mjs +53 -0
- package/src/lib/node-aliases.mjs +73 -0
- package/src/lib/operations.mjs +145 -0
- package/src/lib/provenance-plugin.mjs +74 -0
- package/src/lib/region-refs.mjs +40 -0
- package/src/lib/remote.mjs +171 -0
- package/src/lib/scaffold.mjs +606 -0
- package/src/lib/screens.mjs +266 -0
- package/src/lib/snapshots.mjs +85 -0
- package/src/lib/stubs.mjs +114 -0
- package/src/lib/ts-project.mjs +42 -0
- package/src/lib/util.mjs +626 -0
- package/src/lib/worlds.mjs +236 -0
- package/src/lib/write-if-changed.mjs +35 -0
- package/src/lib/writes-coverage.mjs +32 -0
- package/src/playwright-lagoon.mjs +2305 -0
- package/src/run.mjs +385 -0
- package/src/runtime-harness.mjs +225 -0
- package/README.md +0 -3
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// `motu lagoon try <region> [step…]` — ask a region "what happens if…?" and see the answer, step by step.
|
|
2
|
+
//
|
|
3
|
+
// WHY. Every check motu has answers "does this hold?" — a question you can only ask once you know what
|
|
4
|
+
// to assert. Before that, an agent exploring a region had two moves, both bad: write a flow, run a
|
|
5
|
+
// runtime verification for minutes and read the failure (an assertion loop pressed into exploring),
|
|
6
|
+
// or open a state in a browser and hand-write `page.evaluate` against `window.__motuLagoon` (a private
|
|
7
|
+
// API, rediscovered by guessing, every time). Both happened, repeatedly, in the session that produced
|
|
8
|
+
// this command: a bug whose state the screen MASKED was only found by reading `snapshot()` by hand.
|
|
9
|
+
//
|
|
10
|
+
// WHAT IT DOES. Turns the steps given on the command line into a one-off flow, replays it with the
|
|
11
|
+
// same engine `motu diff` uses (`runRegionFlows`, traced), and prints what each step changed: the
|
|
12
|
+
// region's keys (old → new), what each slot perceivably shows, the backend calls that landed — and
|
|
13
|
+
// what failed, if anything. Nothing is written unless asked.
|
|
14
|
+
//
|
|
15
|
+
// THE SAME VERBS A FLOW HAS, and nothing else: `click` by accessible name, `emit` a declared output,
|
|
16
|
+
// `provide` a host key, `seed` the starting state, `in <world>`. No selectors, no eval — reaching for
|
|
17
|
+
// those is leaving the harness for a browser test, the line motu draws everywhere else.
|
|
18
|
+
//
|
|
19
|
+
// KEEPING IT. Assertions are the AGENT's to write (`shows`, `hides`, `holds`), never derived from what
|
|
20
|
+
// was observed: an assertion copied from the output agrees with itself by construction, which is the
|
|
21
|
+
// fake evidence motu's checks exist to refuse. `--keep "<name>"` appends the flow to the region's
|
|
22
|
+
// evidence only when every assertion held — and then `motu archipelago verify` judges it like any flow.
|
|
23
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
24
|
+
import { join, resolve } from 'node:path';
|
|
25
|
+
import { color, paths } from '../lib/util.mjs';
|
|
26
|
+
import { readRegions } from '../lib/eject.mjs';
|
|
27
|
+
import { runRegionFlows, closeLagoonPool } from '../playwright-lagoon.mjs';
|
|
28
|
+
|
|
29
|
+
const USAGE = `motu lagoon try <region> [step…] [--world <id>] [--shot <dir>] [--keep "<flow name>"] [--json]
|
|
30
|
+
|
|
31
|
+
steps (each one argument — quote it):
|
|
32
|
+
in <world> run in a declared world (first, once)
|
|
33
|
+
seed <key>=<json> the starting state, before anything renders
|
|
34
|
+
click <accessible name> what a person does
|
|
35
|
+
emit <slot>:<event> [json] an island's declared output, entered past its handler
|
|
36
|
+
provide <key>=<json> the application moving its own state
|
|
37
|
+
assertions (attach to the step before them; required for --keep):
|
|
38
|
+
shows <slot>: <text> holds <key>=<json>
|
|
39
|
+
hides <slot>: <text>`;
|
|
40
|
+
|
|
41
|
+
/** Parse the step arguments into one flow. Exported for tests. */
|
|
42
|
+
export function parseTry(args) {
|
|
43
|
+
const flow = { seed: {}, steps: [], world: null };
|
|
44
|
+
const json = (raw, what) => {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
// A bare word is a string — `provide viewMode=diff` should not need quotes inside quotes.
|
|
49
|
+
if (/^[\w.-]+$/.test(raw)) return raw;
|
|
50
|
+
throw new Error(`${what}: "${raw}" is not JSON (quote strings: '"text"')`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const assign = (rest, what) => {
|
|
54
|
+
const at = rest.indexOf('=');
|
|
55
|
+
if (at < 1) throw new Error(`${what} wants <key>=<json>, got "${rest}"`);
|
|
56
|
+
return [rest.slice(0, at).trim(), json(rest.slice(at + 1).trim(), what)];
|
|
57
|
+
};
|
|
58
|
+
const last = (verb) => {
|
|
59
|
+
if (!flow.steps.length) throw new Error(`"${verb}" asserts on the step before it — there is no step yet (start with click, emit, provide, or "look")`);
|
|
60
|
+
return flow.steps[flow.steps.length - 1];
|
|
61
|
+
};
|
|
62
|
+
for (const raw of args) {
|
|
63
|
+
const arg = String(raw).trim();
|
|
64
|
+
const sp = arg.indexOf(' ');
|
|
65
|
+
const verb = sp < 0 ? arg : arg.slice(0, sp);
|
|
66
|
+
const rest = sp < 0 ? '' : arg.slice(sp + 1).trim();
|
|
67
|
+
if (verb === 'in') { if (flow.steps.length) throw new Error('"in <world>" comes first'); flow.world = rest; continue; }
|
|
68
|
+
if (verb === 'seed') { const [k, v] = assign(rest, 'seed'); flow.seed[k] = v; continue; }
|
|
69
|
+
if (verb === 'look') { flow.steps.push({ look: true }); continue; }
|
|
70
|
+
if (verb === 'click') { if (!rest) throw new Error('click wants an accessible name'); flow.steps.push({ click: rest }); continue; }
|
|
71
|
+
if (verb === 'emit') {
|
|
72
|
+
const m = rest.match(/^([\w-]+):([\w-]+)(?:\s+(.+))?$/);
|
|
73
|
+
if (!m) throw new Error(`emit wants <slot>:<event> [json], got "${rest}"`);
|
|
74
|
+
flow.steps.push({ emit: { slot: m[1], event: m[2], ...(m[3] !== undefined ? { detail: json(m[3], 'emit detail') } : {}) } });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (verb === 'provide') { const [k, v] = assign(rest, 'provide'); flow.steps.push({ provide: { [k]: v } }); continue; }
|
|
78
|
+
if (verb === 'shows' || verb === 'hides') {
|
|
79
|
+
const m = rest.match(/^([\w-]+):\s*(.+)$/);
|
|
80
|
+
if (!m) throw new Error(`${verb} wants <slot>: <text>, got "${rest}"`);
|
|
81
|
+
const st = last(verb);
|
|
82
|
+
st.expectRender ??= {};
|
|
83
|
+
const prev = st.expectRender[m[1]];
|
|
84
|
+
const want = typeof prev === 'string' ? { text: prev } : (prev ?? {});
|
|
85
|
+
if (verb === 'shows') want.text = m[2]; else want.notText = m[2];
|
|
86
|
+
st.expectRender[m[1]] = want.notText === undefined ? want.text : want;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (verb === 'holds') { const [k, v] = assign(rest, 'holds'); (last('holds').expect ??= {})[k] = v; continue; }
|
|
90
|
+
throw new Error(`unknown step "${arg}" — one of: in, seed, look, click, emit, provide, shows, hides, holds`);
|
|
91
|
+
}
|
|
92
|
+
return flow;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What changed between two snapshots of the region's keys, as `key: old → new`. Exported for tests. */
|
|
96
|
+
export function keyDiff(before, after) {
|
|
97
|
+
const out = [];
|
|
98
|
+
const show = (v) => {
|
|
99
|
+
const s = JSON.stringify(v);
|
|
100
|
+
return s === undefined ? 'undefined' : s.length > 120 ? `${s.slice(0, 117)}…` : s;
|
|
101
|
+
};
|
|
102
|
+
for (const k of new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})])) {
|
|
103
|
+
const a = JSON.stringify(before?.[k]);
|
|
104
|
+
const b = JSON.stringify(after?.[k]);
|
|
105
|
+
if (a !== b) out.push({ key: k, from: show(before?.[k]), to: show(after?.[k]) });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** What a slot shows, read once: the surface repeats under nested slot elements (joined " | ") and the
|
|
111
|
+
* mountpoints view prefixes the slot's own caption — both are the harness, not the region. */
|
|
112
|
+
const tidy = (slot, text) => {
|
|
113
|
+
const parts = String(text ?? '').split(' | ').map((p) => p.trim().replace(new RegExp(`^${slot}\\s+`, 'i'), '')).filter(Boolean);
|
|
114
|
+
return [...new Set(parts)].join(' | ');
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const clip = (s, n = 160) => {
|
|
118
|
+
const t = String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
119
|
+
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export async function lagoonTryCommand(argv) {
|
|
123
|
+
const id = argv._[0];
|
|
124
|
+
if (!id || argv.help) {
|
|
125
|
+
console.log(USAGE);
|
|
126
|
+
process.exit(id ? 0 : 2);
|
|
127
|
+
}
|
|
128
|
+
const region = readRegions(paths.archipelagosDir).find((r) => r.id === id);
|
|
129
|
+
if (!region) {
|
|
130
|
+
const known = readRegions(paths.archipelagosDir).map((r) => r.id);
|
|
131
|
+
console.error(color.red(`✗ no region "${id}"`) + color.dim(known.length ? ` — regions here: ${known.join(', ')}` : ' — none found; run this from the project that holds motu.config.json'));
|
|
132
|
+
process.exit(2);
|
|
133
|
+
}
|
|
134
|
+
let flow;
|
|
135
|
+
try {
|
|
136
|
+
flow = parseTry(argv._.slice(1));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(color.red(`✗ ${e.message}`));
|
|
139
|
+
console.log(USAGE);
|
|
140
|
+
process.exit(2);
|
|
141
|
+
}
|
|
142
|
+
if (typeof argv.world === 'string') flow.world = argv.world;
|
|
143
|
+
|
|
144
|
+
// STEP 0 IS "HOW IT STARTS": an assertion that always holds on the region's first slot, so the
|
|
145
|
+
// runner records the state before anything is done — the baseline every later step is read against.
|
|
146
|
+
const firstSlot = region.islands[0]?.slot;
|
|
147
|
+
if (!firstSlot) {
|
|
148
|
+
console.error(color.red(`✗ region "${id}" declares no islands — nothing to try`));
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
// EXPLORING NEEDS NO ASSERTION, and the runner (rightly) refuses a step without one. So a step the
|
|
152
|
+
// agent did not assert gets an always-true one of ours — on the first slot, containing "" — which
|
|
153
|
+
// is never reported and never kept. Steps the agent DID assert run exactly as written.
|
|
154
|
+
const placeholder = { [firstSlot]: '' };
|
|
155
|
+
const steps = [
|
|
156
|
+
{ expectRender: placeholder },
|
|
157
|
+
...flow.steps.map(({ look, ...st }) => (st.expect || st.expectRender ? st : { ...st, expectRender: placeholder })),
|
|
158
|
+
];
|
|
159
|
+
const name = typeof argv.keep === 'string' ? argv.keep : 'motu lagoon try';
|
|
160
|
+
const scenario = { name, ...(Object.keys(flow.seed).length ? { seed: flow.seed } : {}), ...(flow.world ? { world: flow.world } : {}), steps };
|
|
161
|
+
|
|
162
|
+
const shotDir = typeof argv.shot === 'string' ? resolve(argv.shot) : null;
|
|
163
|
+
if (shotDir) mkdirSync(shotDir, { recursive: true });
|
|
164
|
+
const shots = [];
|
|
165
|
+
const t0 = Date.now();
|
|
166
|
+
let run;
|
|
167
|
+
try {
|
|
168
|
+
run = await runRegionFlows({
|
|
169
|
+
id,
|
|
170
|
+
// 7300–7599: clear of Chromium's unsafe ports (6665–6669 is IRC — a random pick there refused to load).
|
|
171
|
+
port: 7300 + Math.floor(Math.random() * 300),
|
|
172
|
+
scenarios: [scenario],
|
|
173
|
+
trace: true,
|
|
174
|
+
onStep: shotDir
|
|
175
|
+
? async (page, { step }) => {
|
|
176
|
+
const file = join(shotDir, `${id}-step-${step - 1}.png`);
|
|
177
|
+
await page.screenshot({ path: file, fullPage: true });
|
|
178
|
+
shots.push(file);
|
|
179
|
+
}
|
|
180
|
+
: null,
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error(color.red(`? could not run — ${String(e?.message ?? e).split('\n')[0]}`));
|
|
184
|
+
process.exit(2);
|
|
185
|
+
} finally {
|
|
186
|
+
await closeLagoonPool().catch(() => {});
|
|
187
|
+
}
|
|
188
|
+
const ms = Date.now() - t0;
|
|
189
|
+
const results = run?.flows ?? [];
|
|
190
|
+
// A LAGOON THAT NEVER MOUNTED is not a region where nothing happened — it is a run that did not
|
|
191
|
+
// happen. Same guard as `motu diff`: say so, exit 2, never print an empty exploration as an answer.
|
|
192
|
+
if (!results.length || results.every((r) => /no emit seam|no island mounted/.test(String(r.error ?? '')))) {
|
|
193
|
+
const why = (run?.diagnostics ?? []).map((d) => d.text ?? d.message ?? String(d)).find(Boolean);
|
|
194
|
+
console.error(color.red(`? could not run — the lagoon never mounted ${id}`) + (why ? color.dim(`: ${String(why).slice(0, 300)}`) : ''));
|
|
195
|
+
process.exit(2);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Read the trace into "what each step changed".
|
|
199
|
+
const described = ['how it starts', ...flow.steps.map((s) => (s.look ? 'look' : s.click ? `click ${s.click}` : s.emit ? `emit ${s.emit.slot}:${s.emit.event}${s.emit.detail !== undefined ? ` ${JSON.stringify(s.emit.detail)}` : ''}` : s.provide ? `provide ${Object.entries(s.provide).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')}` : 'step'))];
|
|
200
|
+
const report = [];
|
|
201
|
+
let prev = null;
|
|
202
|
+
for (const [i, r] of results.entries()) {
|
|
203
|
+
const t = r.trace ?? { keys: {}, surface: {}, calls: [] };
|
|
204
|
+
const surface = Object.entries(t.surface ?? {}).map(([slot, text]) => ({ slot, text: clip(tidy(slot, text), 400), changed: !prev || prev.surface?.[slot] !== text }));
|
|
205
|
+
report.push({
|
|
206
|
+
step: i,
|
|
207
|
+
did: described[i] ?? `step ${i}`,
|
|
208
|
+
keys: prev ? keyDiff(prev.keys, t.keys) : Object.keys(t.keys ?? {}).map((k) => ({ key: k, from: '', to: clip(JSON.stringify(t.keys[k]), 120) })),
|
|
209
|
+
slots: surface,
|
|
210
|
+
calls: t.calls ?? [],
|
|
211
|
+
// Step 0's always-true assertion is not the agent's; its mismatches are not either.
|
|
212
|
+
failed: i === 0 ? (r.error && !/render:/.test(r.error) ? r.error : null) : (r.error ?? null),
|
|
213
|
+
mismatches: i === 0 ? [] : (r.mismatches ?? []).map((m) => `${m.key}: expected ${typeof m.expected === 'string' ? m.expected : JSON.stringify(m.expected)}, got ${clip(typeof m.actual === 'string' ? m.actual : JSON.stringify(m.actual), 200)}`),
|
|
214
|
+
crashed: r.crashed ?? null,
|
|
215
|
+
undeclared: (r.undeclared ?? []).map((c) => `${c.method} ${c.label || c.url} ${c.status}`),
|
|
216
|
+
shot: shots[i] ?? null,
|
|
217
|
+
});
|
|
218
|
+
prev = t;
|
|
219
|
+
}
|
|
220
|
+
const asserted = flow.steps.some((s) => s.expect || s.expectRender);
|
|
221
|
+
const held = report.slice(1).every((r) => !r.failed && r.mismatches.length === 0);
|
|
222
|
+
|
|
223
|
+
if (argv.json) {
|
|
224
|
+
console.log(JSON.stringify({ region: id, world: flow.world ?? 'default', ms, held: asserted ? held : null, steps: report }, null, 2));
|
|
225
|
+
} else {
|
|
226
|
+
console.log(color.bold(`motu lagoon try ${id}`) + color.dim(`${flow.world ? ` · world ${flow.world}` : ''} · ${(ms / 1000).toFixed(1)}s`));
|
|
227
|
+
for (const r of report) {
|
|
228
|
+
console.log(`\n${color.bold(`${r.step}. ${r.did}`)}${r.failed ? color.red(` ✗ ${r.failed}`) : ''}${r.crashed ? color.red(` crashed: ${r.crashed}`) : ''}`);
|
|
229
|
+
if (r.step === 0) console.log(color.dim(` keys ${r.keys.length} in the region (${r.keys.map((k) => k.key).join(', ')})`));
|
|
230
|
+
else if (r.keys.length) for (const k of r.keys) console.log(` ${color.bold('key')} ${k.key}: ${color.dim(k.from)} → ${k.to}`);
|
|
231
|
+
else console.log(color.dim(' keys nothing moved'));
|
|
232
|
+
const shown = r.step === 0 ? r.slots : r.slots.filter((s) => s.changed);
|
|
233
|
+
for (const s of shown) console.log(` ${color.bold('slot')} ${s.slot}: ${clip(s.text, 160)}`);
|
|
234
|
+
if (r.step > 0 && !shown.length) console.log(color.dim(' slots nothing changed on screen'));
|
|
235
|
+
if (r.calls.length) console.log(` ${color.bold('calls')} ${r.calls.join(' · ')}`);
|
|
236
|
+
for (const u of r.undeclared) console.log(color.yellow(` ! a call nothing answered: ${u}`));
|
|
237
|
+
for (const m of r.mismatches) console.log(color.red(` ✗ ${m}`));
|
|
238
|
+
if (r.shot) console.log(color.dim(` shot ${r.shot}`));
|
|
239
|
+
}
|
|
240
|
+
if (asserted) console.log(`\n${held ? color.green('✓ every assertion held') : color.red('✗ an assertion did not hold')}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (typeof argv.keep === 'string') {
|
|
244
|
+
if (!asserted) {
|
|
245
|
+
console.error(color.red('\n✗ --keep needs assertions you wrote (shows / hides / holds) — a flow that asserts nothing is not evidence, and one copied from this output agrees with itself'));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (!held) {
|
|
249
|
+
console.error(color.red('\n✗ not kept: an assertion did not hold — fix the flow or the region first'));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
// motu refuses a kept step that asserts nothing (region-flow), so say it here, before writing.
|
|
253
|
+
const bare = flow.steps.filter((st) => !st.look && !st.expect && !st.expectRender).length;
|
|
254
|
+
if (bare) {
|
|
255
|
+
console.error(color.red(`\n✗ not kept: ${bare} step(s) assert nothing — follow each with what it visibly changes (shows / hides / holds); motu refuses a flow step that asserts nothing`));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
const kept = { name: argv.keep, ...(flow.world ? { world: flow.world } : {}), ...(Object.keys(flow.seed).length ? { seed: flow.seed } : {}), steps: flow.steps.map(({ look, ...st }) => st).filter((st) => Object.keys(st).length) };
|
|
259
|
+
const literal = ` ${JSON.stringify(kept)},\n`;
|
|
260
|
+
const file = paths.archipelagoEvidence(id);
|
|
261
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
262
|
+
const at = text ? text.lastIndexOf('];') : -1;
|
|
263
|
+
if (!text || !/export const scenarios/.test(text) || at < 0) {
|
|
264
|
+
console.log(color.yellow(`\n! could not find the scenarios array in ${paths.rel(file)} — add this flow by hand:`));
|
|
265
|
+
console.log(literal);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
writeFileSync(file, `${text.slice(0, at)}${literal}${text.slice(at)}`);
|
|
269
|
+
console.log(color.green(`\n✓ kept as "${argv.keep}" in ${paths.rel(file)}`) + color.dim(' — `motu archipelago verify ' + id + ' --runtime` now judges it like any flow (flow-mutation included)'));
|
|
270
|
+
}
|
|
271
|
+
process.exit(asserted && !held ? 1 : 0);
|
|
272
|
+
}
|