@motujs/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/bundled/github/agents/archipelago-build.agent.md +298 -0
  3. package/bundled/github/agents/island-create.agent.md +261 -0
  4. package/bundled/github/agents/island-extract.agent.md +360 -0
  5. package/bundled/github/agents/island-locate.agent.md +208 -0
  6. package/bundled/github/agents/motu-checks.agent.md +158 -0
  7. package/bundled/github/agents/motu-evidence.agent.md +267 -0
  8. package/bundled/github/agents/motu-regions.agent.md +304 -0
  9. package/bundled/github/host-rules.md +43 -0
  10. package/bundled/github/prompts/archipelago-build.prompt.md +19 -0
  11. package/bundled/github/prompts/island-create.prompt.md +37 -0
  12. package/bundled/github/prompts/island-extract.prompt.md +59 -0
  13. package/bundled/github/prompts/island-locate.prompt.md +43 -0
  14. package/bundled/packages/adapters/angularjs/vite.mjs +43 -0
  15. package/bundled/packages/adapters/next/lagoon/next-stubs.tsx +108 -0
  16. package/bundled/packages/adapters/next/vite.mjs +121 -0
  17. package/bundled/packages/adapters/vite/vite.mjs +179 -0
  18. package/package.json +64 -3
  19. package/src/alias-loader.mjs +63 -0
  20. package/src/alias-register.mjs +28 -0
  21. package/src/asset-stub.cjs +3 -0
  22. package/src/asset-stub.mjs +5 -0
  23. package/src/cli.mjs +78 -0
  24. package/src/commands/adopt-root.mjs +194 -0
  25. package/src/commands/archipelago-init.mjs +314 -0
  26. package/src/commands/archipelago.mjs +138 -0
  27. package/src/commands/assurance.mjs +40 -0
  28. package/src/commands/check.mjs +346 -0
  29. package/src/commands/codegen.mjs +29 -0
  30. package/src/commands/coverage.mjs +358 -0
  31. package/src/commands/create.mjs +277 -0
  32. package/src/commands/defaults.mjs +129 -0
  33. package/src/commands/diff.mjs +595 -0
  34. package/src/commands/fixtures.mjs +153 -0
  35. package/src/commands/init.mjs +473 -0
  36. package/src/commands/integrate.mjs +208 -0
  37. package/src/commands/integration.mjs +1349 -0
  38. package/src/commands/lagoon-try.mjs +272 -0
  39. package/src/commands/lagoon.mjs +1419 -0
  40. package/src/commands/link.mjs +110 -0
  41. package/src/commands/region-coverage.mjs +604 -0
  42. package/src/commands/removal-check.mjs +878 -0
  43. package/src/commands/skills.mjs +156 -0
  44. package/src/commands/snapshot.mjs +456 -0
  45. package/src/commands/sync.mjs +38 -0
  46. package/src/commands/verify/island-runtime.mjs +581 -0
  47. package/src/commands/verify/island-static.mjs +586 -0
  48. package/src/commands/verify/island.mjs +135 -0
  49. package/src/commands/verify/reach.mjs +426 -0
  50. package/src/commands/verify/region-composition.mjs +391 -0
  51. package/src/commands/verify/region-config.mjs +330 -0
  52. package/src/commands/verify/region-flows.mjs +760 -0
  53. package/src/commands/verify/region-sources.mjs +516 -0
  54. package/src/commands/verify/region.mjs +271 -0
  55. package/src/commands/verify/report.mjs +338 -0
  56. package/src/commands/verify/screens-runtime.mjs +123 -0
  57. package/src/commands/verify.mjs +18 -0
  58. package/src/lagoon-build.mjs +25 -0
  59. package/src/lagoon-dev.mjs +32 -0
  60. package/src/lib/alias-core.mjs +49 -0
  61. package/src/lib/archipelagos.mjs +111 -0
  62. package/src/lib/assurance.mjs +144 -0
  63. package/src/lib/baselines.mjs +107 -0
  64. package/src/lib/bundlability.mjs +123 -0
  65. package/src/lib/changed.mjs +157 -0
  66. package/src/lib/component-props.mjs +145 -0
  67. package/src/lib/config.mjs +451 -0
  68. package/src/lib/contracts.mjs +244 -0
  69. package/src/lib/data-flow.mjs +47 -0
  70. package/src/lib/declared-sources.mjs +101 -0
  71. package/src/lib/dock-inject.mjs +47 -0
  72. package/src/lib/effects.mjs +97 -0
  73. package/src/lib/eject.mjs +715 -0
  74. package/src/lib/evidence-json.mjs +74 -0
  75. package/src/lib/expect-calls.mjs +53 -0
  76. package/src/lib/finding-memory.mjs +0 -0
  77. package/src/lib/flow-calls.mjs +53 -0
  78. package/src/lib/framework.mjs +98 -0
  79. package/src/lib/host-rules.mjs +55 -0
  80. package/src/lib/host-sources.mjs +121 -0
  81. package/src/lib/island-placement.mjs +64 -0
  82. package/src/lib/island-render.mjs +105 -0
  83. package/src/lib/islands.mjs +112 -0
  84. package/src/lib/lagoon-bundle.mjs +138 -0
  85. package/src/lib/lagoon-declares.mjs +553 -0
  86. package/src/lib/lagoon-materialize.mjs +217 -0
  87. package/src/lib/lagoon-vite.mjs +788 -0
  88. package/src/lib/live-url.mjs +53 -0
  89. package/src/lib/node-aliases.mjs +73 -0
  90. package/src/lib/operations.mjs +145 -0
  91. package/src/lib/provenance-plugin.mjs +74 -0
  92. package/src/lib/region-refs.mjs +40 -0
  93. package/src/lib/remote.mjs +171 -0
  94. package/src/lib/scaffold.mjs +606 -0
  95. package/src/lib/screens.mjs +266 -0
  96. package/src/lib/snapshots.mjs +85 -0
  97. package/src/lib/stubs.mjs +114 -0
  98. package/src/lib/ts-project.mjs +42 -0
  99. package/src/lib/util.mjs +626 -0
  100. package/src/lib/worlds.mjs +236 -0
  101. package/src/lib/write-if-changed.mjs +35 -0
  102. package/src/lib/writes-coverage.mjs +32 -0
  103. package/src/playwright-lagoon.mjs +2305 -0
  104. package/src/run.mjs +385 -0
  105. package/src/runtime-harness.mjs +225 -0
  106. package/README.md +0 -3
@@ -0,0 +1,40 @@
1
+ // `motu assurance [id] [--json]` — one row per declared thing, with the strongest evidence that holds
2
+ // it. See lib/assurance.mjs for the levels and docs/proposals/constraint-loop.md (item 3) for why.
3
+ import { readRegions } from '../lib/eject.mjs';
4
+ import { assuranceRows, summarize, LEVELS } from '../lib/assurance.mjs';
5
+ import { color, paths } from '../lib/util.mjs';
6
+ import { readScenariosFor } from './verify/region-flows.mjs';
7
+
8
+ const tint = (level) =>
9
+ level === 'unchecked' ? color.red(level) : level === 'static' ? color.yellow(level) : level === 'runtime·emit' ? color.yellow(level) : color.green(level);
10
+
11
+ export function assuranceCommand(argv) {
12
+ const only = argv._?.[0];
13
+ const regions = readRegions(paths.archipelagosDir).filter((r) => !only || r.id === only);
14
+ if (only && !regions.length) {
15
+ console.error(color.red(`✗ no region "${only}"`));
16
+ return 2;
17
+ }
18
+ const out = regions.map((r) => {
19
+ const rows = assuranceRows(r, readScenariosFor(r.id));
20
+ return { region: r.id, rows, summary: summarize(rows) };
21
+ });
22
+ if (argv.json) {
23
+ console.log(JSON.stringify({ levels: LEVELS, note: 'levels are what the evidence CLAIMS; motu check --runtime confirms them', regions: out }, null, 2));
24
+ return 0;
25
+ }
26
+ console.log(color.bold('\nmotu assurance — what each declaration is held by\n'));
27
+ for (const { region, rows, summary } of out) {
28
+ console.log(color.bold(` ${region}`) + color.dim(` ${summary.total} declared thing(s) · weakest: ${summary.weakest}`));
29
+ for (const r of rows) {
30
+ console.log(` ${tint(r.level).padEnd(22)} ${color.dim(r.kind.padEnd(5))} ${r.subject}`);
31
+ console.log(` ${color.dim(r.evidence)}`);
32
+ if (r.missing) console.log(` ${color.yellow('missing:')} ${r.missing}`);
33
+ }
34
+ console.log('');
35
+ }
36
+ console.log(color.dim(' Levels are what the evidence CLAIMS; `motu check --runtime` is what confirms them.'));
37
+ console.log(color.dim(` ${LEVELS.join(' < ')}`));
38
+ // Reporting, not gating: a level is information for a person to decide about, not a failure.
39
+ return 0;
40
+ }
@@ -0,0 +1,346 @@
1
+ // `motu check` — one gate, one verdict: is the project's motu wiring still sound?
2
+ //
3
+ // Deliberately scoped to what MOTU owns. It does not run the host's typecheck or linter: those are the
4
+ // host's tools, configured by the host, and a framework that shells out to them has taken an opinion
5
+ // about a build it knows nothing about (the reference ocean is not even a node app). The host composes:
6
+ //
7
+ // <host build> && motu check
8
+ //
9
+ // The one apparent exception is removal-check, which does run the host's typecheck — but that IS its
10
+ // subject: "the app still compiles without motu" cannot be answered any other way.
11
+ //
12
+ // Static by default, because this is meant to run on every change. `--runtime` adds the lagoon mounts
13
+ // (a browser per island), which belongs in CI or before a release, not in a tight loop.
14
+ import { screenFindings } from '../lib/screens.mjs';
15
+ import { worldFindings } from '../lib/worlds.mjs';
16
+ import { screenRuntimeFindings } from './verify/screens-runtime.mjs';
17
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
18
+ import { readLiveUrl } from '../lib/live-url.mjs';
19
+ import { resolve } from 'node:path';
20
+ import { color, paths, hostStrictBoundaries } from '../lib/util.mjs';
21
+ import { listIslands } from '../lib/islands.mjs';
22
+ import { changedScope } from '../lib/changed.mjs';
23
+ import { profiledMs, runIslandVerify, runArchipelagoVerify, summaryOf, printSweep } from './verify.mjs';
24
+ import { ejectedEntryGaps } from '../lib/lagoon-materialize.mjs';
25
+ import { runRemovalCheck } from './removal-check.mjs';
26
+ import { contractsDrift } from '../lib/contracts.mjs';
27
+ import { renderCoverageModule, COVERAGE_MODULE } from '../lib/archipelagos.mjs';
28
+ import { integrationResults } from './integration.mjs';
29
+ import { unchangedSinceLastRun } from '../lib/finding-memory.mjs';
30
+ import { names, islandComponentPath, islandComponentExport } from '../lib/util.mjs';
31
+
32
+ /** Is `contracts.generated.ts` what the components say it should be? */
33
+ function islands0Drift() {
34
+ const util = { names, islandComponentPath, islandComponentExport };
35
+ const d = contractsDrift(paths.islandsDir, util);
36
+ if (d.missing) return { stale: true, reason: 'contracts.generated.ts is missing' };
37
+ if (d.drifted) return { stale: true, reason: 'a component changed since the contracts were generated' };
38
+ return { stale: false, reason: null };
39
+ }
40
+
41
+ /**
42
+ * Is `coverage.generated.ts` what motu.config.json says it should be?
43
+ *
44
+ * The same question `islands0Drift` asks, for the region side. It matters more here than it looks:
45
+ * the file's whole job is a SIDE EFFECT, so a stale one does not fail to compile, does not fail a
46
+ * type check, and does not fail to import. It quietly configures the wrong thing — or nothing — and
47
+ * the only symptom is coverage that never records, which is indistinguishable from a region nobody
48
+ * has visited.
49
+ */
50
+ function coverageDrift() {
51
+ const file = resolve(paths.archipelagosDir, COVERAGE_MODULE);
52
+ const want = renderCoverageModule(paths.coverage);
53
+ // ABSENT IS FINE WHEN COVERAGE IS OFF, and demanding it otherwise was wrong. A project that never
54
+ // enabled coverage has no generated module and no import of one — a perfectly consistent state, and
55
+ // failing it would make every existing project red until somebody ran a command that changes
56
+ // nothing. The file becomes required the moment it is switched on, or the moment one exists.
57
+ if (!existsSync(file)) {
58
+ return paths.coverage?.enabled
59
+ ? { stale: true, reason: `${COVERAGE_MODULE} is missing — run \`motu sync\`` }
60
+ : { stale: false, reason: null };
61
+ }
62
+ if (readFileSync(file, 'utf8') !== want)
63
+ return { stale: true, reason: `${COVERAGE_MODULE} does not match motu.config.json — run \`motu sync\`` };
64
+ return { stale: false, reason: null };
65
+ }
66
+
67
+ export async function checkCommand(argv) {
68
+ const startedAt = Date.now();
69
+ // `--runtime` opts IN, the same way it does for a single `verify` — the sub-checks read the flag
70
+ // directly, so this only has to pass it through.
71
+ const runtime = argv.runtime === true;
72
+ // `--audit` implies `--runtime`: asking whether the UI is usable at every viewport, for everyone,
73
+ // only means anything against something that rendered.
74
+ const audit = argv.audit === true;
75
+ const sub = { ...argv, runtime: runtime || audit, audit, fast: argv.fast };
76
+
77
+ // The generated half, first and cheaply: every island's contract is READ from its component, so the
78
+ // only way it can be wrong is by being stale. One comparison answers that for the whole project.
79
+ const drift = islands0Drift();
80
+ const regionDrift = coverageDrift();
81
+ const allIslands = listIslands(paths.islandsDir);
82
+
83
+ // `--changed`: verify what this branch touched. Widens back to everything the moment a change
84
+ // cannot be attributed to one island or region, and says so — a run that is fast because it skipped
85
+ // your change is worse than a slow one.
86
+ const scope = argv.changed ? changedScope(typeof argv.changed === 'string' ? argv.changed : undefined) : null;
87
+ if (scope && !scope.scoped) {
88
+ console.log(color.dim(` --changed: running everything — ${scope.reason}`));
89
+ }
90
+ const islands = scope?.scoped ? allIslands.filter((i) => scope.islands.includes(i.kebab)) : allIslands;
91
+
92
+ const islandResults = [];
93
+ for (const island of islands) islandResults.push(await runIslandVerify(sub, island.kebab));
94
+
95
+ const regionIds = existsSync(paths.archipelagosDir)
96
+ ? readdirSync(paths.archipelagosDir, { withFileTypes: true })
97
+ .filter((e) => e.isDirectory() && existsSync(paths.archipelagoFile(e.name)))
98
+ .map((e) => e.name)
99
+ : [];
100
+ // A touched ISLAND is a touched region too: its contract is what the region declares against.
101
+ const regionsToRun = scope?.scoped ? regionIds.filter((id) => scope.regions.includes(id)) : regionIds;
102
+ const regionResults = [];
103
+ for (const id of regionsToRun) regionResults.push(await runArchipelagoVerify(sub, id));
104
+
105
+ if (scope?.scoped) {
106
+ console.log(
107
+ color.dim(
108
+ ` --changed: ${islands.length}/${allIslands.length} island(s), ` +
109
+ `${regionsToRun.length}/${regionIds.length} region(s) — from ${scope.files} changed file(s)`,
110
+ ),
111
+ );
112
+ // NOTHING CHECKED IS NOT A PASS. `--changed` with no changes verified zero islands and printed
113
+ // green — the same "a check that examined nothing has not passed" rule this project keeps
114
+ // relearning, reproduced in the flag whose whole purpose is to examine less.
115
+ if (!islands.length && !regionsToRun.length) {
116
+ console.log('');
117
+ console.log(
118
+ color.yellow(color.bold('NOTHING TO CHECK')) +
119
+ color.dim(' no island or region changed — this run proves nothing about the project'),
120
+ );
121
+ process.exit(2);
122
+ }
123
+ }
124
+
125
+ // AN EJECTED ENTRY DOES NOT FOLLOW THE FRAMEWORK, and nothing said so until now. `lagoon eject`
126
+ // hands the project its entry and stops regenerating it, which is the point — and it means every
127
+ // option added to the scaffold afterwards reaches that project only if somebody copies it down.
128
+ // Twice in this repo's own demo-app the answer was "nobody did": `evidence` (every declared state
129
+ // unreachable in a browser) and `lens.onPicked` (the crosshair could not scope). Both silent.
130
+ const gaps = ejectedEntryGaps(paths);
131
+ if (gaps) {
132
+ console.log(color.bold('\nmotu check — ejected entry\n'));
133
+ if (!gaps.missing.length) {
134
+ console.log(` ${color.green('✓')} ${color.dim('entry-current'.padEnd(20))}${color.dim(`${gaps.entry} passes every option the scaffold does`)}`);
135
+ } else {
136
+ // A WARNING: an entry may legitimately drop an option (a project with no frames, say), and a
137
+ // check that turns an ejected project red for exercising the escape hatch it was given is one
138
+ // people switch off. What it cannot do is stay quiet.
139
+ console.log(
140
+ ` ${color.yellow('!')} ${color.dim('entry-current'.padEnd(20))}` +
141
+ `${gaps.entry} never mentions ${gaps.missing.map((m) => `\`${m}\``).join(', ')} — ` +
142
+ color.dim('the scaffold passes these; an ejected entry only gets them by hand'),
143
+ );
144
+ }
145
+ }
146
+
147
+ // The LAST MILE. Everything above looks at motu's own files, so all of it can be green while the
148
+ // application composes none of it — which is exactly how a region can verify for a day without a
149
+ // browser ever rendering it. Static, cheap, and it belongs in the same verdict.
150
+ const integration = integrationResults();
151
+ // EXPERIMENTAL: the declared screens, and whether every navigation a region asks for goes somewhere.
152
+ // Null when the project declares no screens — then the section is not printed at all.
153
+ const screens = screenFindings();
154
+ // AT RUNTIME, the screens are opened too: the app's own layout makes calls no region's flow does
155
+ // (Peps' badge counts and feature grants), and screen flows click its links and navigate.
156
+ if (screens && (runtime || audit)) screens.findings.push(...((await screenRuntimeFindings()) ?? []));
157
+ const screensOk = !screens || screens.findings.every((f) => f.level !== 'error');
158
+ // WORLDS: every name resolves, every world has a caller, nothing pins a source's key. Null (and not
159
+ // printed) when the project declares none.
160
+ const worlds = worldFindings();
161
+ const worldsOk = !worlds || worlds.findings.every((f) => f.level !== 'error');
162
+
163
+ // Last, and only when the rest holds up: it rewrites the host on disk (and restores it), so running
164
+ // it after a failure that is already fatal buys nothing and costs a typecheck of the whole app.
165
+ const integrationOk = integration.every((r) => r.findings.every((f) => f.level !== 'error'));
166
+ // Integration is deliberately NOT part of this: removal-check asks whether the app compiles WITHOUT
167
+ // motu, and a region the host has not adopted yet is the easiest case for that surgery, not a reason
168
+ // to skip it. Only broken wiring makes the rewrite pointless.
169
+ const structureOk =
170
+ !drift.stale &&
171
+ !regionDrift.stale &&
172
+ islandResults.every((r) => r.errors.length === 0) &&
173
+ regionResults.every((r) => r.errors.length === 0);
174
+ const removal = structureOk ? await runRemovalCheck(argv, { quiet: true }) : null;
175
+
176
+ const pass = structureOk && integrationOk && screensOk && worldsOk && (removal?.pass ?? false);
177
+
178
+ if (argv.json) {
179
+ console.log(
180
+ JSON.stringify(
181
+ {
182
+ pass,
183
+ runtime,
184
+ contracts: drift,
185
+ islands: islandResults.map(summaryOf),
186
+ archipelagos: regionResults.map(summaryOf),
187
+ integration,
188
+ screens: screens?.findings ?? null,
189
+ worlds: worlds?.findings ?? null,
190
+ removal: removal ?? { skipped: 'structure checks failed first' },
191
+ },
192
+ null,
193
+ 2,
194
+ ),
195
+ );
196
+ process.exit(pass ? 0 : 1);
197
+ }
198
+
199
+ console.log(color.bold('\nmotu check — contracts\n'));
200
+ {
201
+ // The mechanical half of `default-props`: an island must render from defaults alone, which means
202
+ // the EMPTY case too, and `strict` alone lets `list[0].x` compile. Audited, never enforced — the
203
+ // host owns its build. Unknown is silent: a project with no tsconfig gets no finding.
204
+ const b = hostStrictBoundaries();
205
+ if (b.known && !b.enabled)
206
+ console.log(
207
+ ` ${color.yellow('!')} ${color.dim('strict-boundaries'.padEnd(20))}` +
208
+ `${color.yellow('noUncheckedIndexedAccess is off')} in ${paths.rel(b.file)} — \`list[0].x\` on a list that can be ` +
209
+ `empty compiles under \`strict\` and throws in the browser, which is a bug this project has already shipped once. ` +
210
+ `Turning it on makes forgetting a compile error; scope it to a tsconfig over the island + ui files if the ` +
211
+ `app-wide flip is too large today.`,
212
+ );
213
+ else if (b.known)
214
+ console.log(
215
+ ` ${color.green('✓')} ${color.dim('strict-boundaries'.padEnd(20))}${color.dim('noUncheckedIndexedAccess is on — an unguarded index is a compile error')}`,
216
+ );
217
+ }
218
+ console.log(
219
+ drift.stale
220
+ ? ` ${color.red('✗')} ${color.dim('generated'.padEnd(20))} ${color.red(drift.reason)} — run \`motu sync\``
221
+ : ` ${color.green('✓')} ${color.dim('generated'.padEnd(20))}${color.dim('every island contract matches its component')}`,
222
+ );
223
+ // THE REGION SIDE, which fails silently in a way the island side does not. `coverage.generated.ts`
224
+ // exists for a side effect, so a stale one still compiles, still type-checks and still imports —
225
+ // it just configures the wrong thing, and the symptom is coverage that never records, which looks
226
+ // exactly like a region nobody has visited.
227
+ console.log(
228
+ regionDrift.stale
229
+ ? ` ${color.red('✗')} ${color.dim('region-generated'.padEnd(20))} ${color.red(regionDrift.reason)}`
230
+ : ` ${color.green('✓')} ${color.dim('region-generated'.padEnd(20))}${color.dim(
231
+ paths.coverage?.enabled
232
+ ? 'coverage config matches motu.config.json'
233
+ : existsSync(resolve(paths.archipelagosDir, COVERAGE_MODULE))
234
+ ? 'coverage off, and nothing names @motujs/coverage'
235
+ : 'coverage off, and no generated module to keep in step',
236
+ )}`,
237
+ );
238
+
239
+ printSweep('motu check — islands', islandResults);
240
+ printSweep('motu check — regions', regionResults);
241
+ console.log(color.bold('\nmotu check — integration\n'));
242
+ for (const r of integration) {
243
+ const errs = r.findings.filter((f) => f.level === 'error');
244
+ const warns = r.findings.filter((f) => f.level === 'warn');
245
+ const mark = errs.length ? color.red('✗') : warns.length ? color.yellow('!') : color.green('✓');
246
+ console.log(
247
+ ` ${mark} ${color.dim(r.id.padEnd(20))}` +
248
+ color.dim(errs.length ? `${errs.length} error(s)` : warns.length ? `${warns.length} warning(s)` : 'composed, mounted, placed, read'),
249
+ );
250
+ for (const f of [...errs, ...warns]) {
251
+ const again = unchangedSinceLastRun(f) ? color.dim(' · unchanged') : '';
252
+ console.log(` ${f.level === 'error' ? color.red('✗') : color.yellow('!')} ${color.dim(f.check.padEnd(12))} ${f.msg}${again}`);
253
+ }
254
+ }
255
+
256
+ if (screens) {
257
+ console.log(color.bold('\nmotu check — screens') + color.dim(' (experimental)') + '\n');
258
+ for (const f of screens.findings) {
259
+ const mark = f.level === 'error' ? color.red('✗') : f.level === 'warn' ? color.yellow('!') : f.level === 'ok' ? color.green('✓') : color.dim('–');
260
+ console.log(` ${mark} ${color.dim(f.check.padEnd(20))}${f.level === 'ok' ? color.dim(f.msg) : f.msg}`);
261
+ }
262
+ }
263
+
264
+ if (worlds) {
265
+ console.log(color.bold('\nmotu check — worlds') + color.dim(` ${worlds.worlds.length} declared in ${paths.rel(worlds.file)}`) + '\n');
266
+ for (const f of worlds.findings) {
267
+ const mark = f.level === 'error' ? color.red('✗') : f.level === 'warn' ? color.yellow('!') : f.level === 'ok' ? color.green('✓') : color.dim('–');
268
+ console.log(` ${mark} ${color.dim(f.check.padEnd(20))}${f.level === 'ok' ? color.dim(f.msg) : f.msg}`);
269
+ }
270
+ }
271
+
272
+ console.log(color.bold('\nmotu check — removal\n'));
273
+ if (!removal) {
274
+ console.log(` ${color.dim('–')} ${color.dim('skipped'.padEnd(20))} ${color.dim('structure checks failed first')}`);
275
+ } else if (removal.skipped) {
276
+ // A SKIP, NOT A TICK. The project declared `removable: false`, so nothing was examined — and
277
+ // rendering that as `✓ removable 0 deleted, 0 unwrapped` is a green light from an empty search,
278
+ // which is the exact failure this command was rewritten to stop reporting about itself.
279
+ console.log(
280
+ ` ${color.dim('–')} ${color.dim('removable'.padEnd(20))} ` +
281
+ color.dim('not claimed — the project declares `removable: false`, so motu is meant to be load-bearing here'),
282
+ );
283
+ } else if (removal.pass) {
284
+ const ejected = removal.ejected.reduce((n, e) => n + e.notes.length, 0);
285
+ console.log(
286
+ ` ${color.green('✓')} ${color.dim('removable'.padEnd(20))}` +
287
+ color.dim(
288
+ `${removal.deleted.length} deleted, ${removal.stripped.length} unwrapped, ${ejected} ejected` +
289
+ // Say when it was not re-proved. A cached verdict is still a verdict, but a reader has to
290
+ // know which one they are looking at.
291
+ (removal.cached ? ' · unchanged since the last proof' : ''),
292
+ ),
293
+ );
294
+ } else {
295
+ console.log(` ${color.red('✗')} ${color.dim('load-bearing'.padEnd(20))} ${color.red('the host does not compile without motu')}`);
296
+ for (const line of removal.errors.slice(0, 5)) console.log(` ${color.dim(line)}`);
297
+ }
298
+
299
+ console.log('');
300
+ // WHAT THE PROFILE MISSED, said out loud. Timed steps are not a partition of the run — boot, the
301
+ // static passes and anything nobody wrapped live in the gap — and a parts list that hides the
302
+ // remainder gets you optimising the wrong half. This printed 13.8s of 31.8 once, every line precise.
303
+ if (argv.verbose) {
304
+ const wall = Date.now() - startedAt;
305
+ const profiled = profiledMs();
306
+ const rest = Math.max(0, wall - profiled);
307
+ console.log(
308
+ color.dim(
309
+ ` profiled ${(profiled / 1000).toFixed(1)}s of ${(wall / 1000).toFixed(1)}s · ` +
310
+ `${(rest / 1000).toFixed(1)}s (${Math.round((rest / wall) * 100)}%) outside any timed step`,
311
+ ),
312
+ );
313
+ }
314
+ console.log(
315
+ pass
316
+ ? color.green(color.bold('PASS')) +
317
+ color.dim(
318
+ ` ${islandResults.length} island(s), ${regionResults.length} region(s)` +
319
+ // Only claim removability when it was actually proved. The verdict line is the one thing
320
+ // a reader takes away, and "removable" on a run that never examined the host is the same
321
+ // empty-search claim the line above it was just fixed to stop making.
322
+ (removal?.skipped ? '' : ', removable'),
323
+ )
324
+ : color.red(color.bold('FAIL')) + color.dim(' see above'),
325
+ );
326
+ // WHAT THIS VERDICT DOES NOT COVER, said on the line a reader actually takes away.
327
+ //
328
+ // motu checks composition: that a region's islands are placed, that a declared coupling carries,
329
+ // that every state renders. It runs no typecheck and no test runner, deliberately — and an agent
330
+ // reading a green PASS has no way to know that from the output. Measured: a defect-detection
331
+ // experiment injected eight regressions, six of them ordinary logic errors inside a component body,
332
+ // and motu was scored as having "missed" them. It had not missed them; it had never been asked.
333
+ //
334
+ // A flow also emits an island's DECLARED OUTPUT rather than acting on the UI, so a bug between a
335
+ // user's action and that declaration — a handler that ignores its argument — passes every flow. The
336
+ // instrument for that is a unit test, and saying so here is cheaper than the confusion is.
337
+ console.log(
338
+ color.dim(' motu checks composition. Your typecheck and unit tests are yours and are not run here:'),
339
+ );
340
+ console.log(color.dim(' a logic bug inside a component, or a handler that drops its argument, passes every check above.'));
341
+ // AND WHERE TO LOOK AT IT. `check` is the verdict an agent reads and quotes; without this line the
342
+ // handover is a summary of files and nobody has opened the region the work is for.
343
+ const live = readLiveUrl();
344
+ console.log(live ? color.dim(' look at it: ') + live : color.dim(' no live lagoon — start one with: motu lagoon serve --watch --detach'));
345
+ process.exit(pass ? 0 : 1);
346
+ }
@@ -0,0 +1,29 @@
1
+ // `motu codegen [manifest] [outDir]` — regenerates the typed @motujs/contract from a motu-manifest.json
2
+ // emitted by dev.motu:apt. Thin wrapper over packages/codegen/src/cli.mjs so contract regeneration is
3
+ // reachable from the one `motu` entry point, with the same defaults as `pnpm gen:contract`.
4
+ import { spawnSync } from 'node:child_process';
5
+ import { existsSync } from 'node:fs';
6
+ import { paths, color } from '../lib/util.mjs';
7
+
8
+ export async function codegenCommand(argv) {
9
+ const manifest = argv._[0] || paths.defaultManifest;
10
+ const outDir = argv._[1] || paths.contractSrcDir;
11
+
12
+ if (!existsSync(manifest)) {
13
+ console.error(color.red(`✗ manifest not found: ${manifest}`));
14
+ console.error(color.dim(' build the backend so dev.motu:apt emits motu-manifest.json, or pass an explicit path.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ // @motujs/codegen is NOT published: it serves the reference ocean's Java manifest and lives only in
19
+ // the checkout. Installed from npm, spawning the path below would die with node's own
20
+ // MODULE_NOT_FOUND naming `node_modules/@motujs/codegen/src/cli.mjs`, which reads as a broken
21
+ // install rather than as a command that needs a checkout.
22
+ if (!existsSync(paths.codegenCli)) {
23
+ console.error(color.red('✗ motu codegen needs a motu checkout — @motujs/codegen is not published to npm.'));
24
+ console.error(color.dim(' clone the motu repository and run it from there, or set MOTU_ROOT=<checkout>.'));
25
+ process.exit(2);
26
+ }
27
+ const res = spawnSync(process.execPath, [paths.codegenCli, manifest, outDir], { stdio: 'inherit' });
28
+ process.exit(res.status ?? 0);
29
+ }