@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.
- 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,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
|
+
}
|