@ultimat3/cli 11.2.0 → 12.0.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.
@@ -19,6 +19,7 @@ import { checkAppBoundaries } from './app-boundaries';
19
19
  import { envExampleFindings } from './app-env';
20
20
  import { appManifest, readAppManifest } from './app-manifest';
21
21
  import { OPENAPI_FILE, openApiJson } from './app-openapi';
22
+ import { policyFindings } from './app-permissions';
22
23
  import { APP_CONFIG_FILE } from './app-root';
23
24
  import { checkBudgets, readBuildStats } from './budgets';
24
25
  import { checkDestructiveMigrations } from './db-destructive';
@@ -265,6 +266,15 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
265
266
  applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
266
267
  run: async (ctx) => fromFindings(await catalogFindings(ctx.root)),
267
268
  },
269
+ {
270
+ name: 'policy',
271
+ summary: 'every permission this app grants or requires is one it declares',
272
+ // A repo with no `app.config.ts` is the framework monorepo, which declares no roles and
273
+ // registers no routes — SKIPPED there, never passed, for the reason `i18n` gives: a step that
274
+ // answers `ok` about nothing is the vacuous green these checks exist to refuse.
275
+ applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
276
+ run: async (ctx) => fromFindings(await policyFindings(ctx.root)),
277
+ },
268
278
  {
269
279
  name: 'manifest',
270
280
  summary: 'the files an agent reads: generated facts, hand-written conventions, the env example',
@@ -283,6 +293,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
283
293
  async run(ctx) {
284
294
  const agents = await checkAgentsMd(ctx.root);
285
295
  const findings = [
296
+ ...manifestMissingFindings(ctx.root),
286
297
  ...(await driftFindings(ctx.root)),
287
298
  ...(await envExampleFindings(ctx.root)),
288
299
  ...floorProblemFindings(await readVerifyFloor(ctx.root)),
@@ -312,6 +323,31 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
312
323
  },
313
324
  ];
314
325
 
326
+ /**
327
+ * The half that had never been asked: is the file there at all? `driftFindings` returns nothing
328
+ * when it is absent — correctly, it has nothing to compare — and `AGENTS.md` line 3 tells an agent
329
+ * that facts live in `x.manifest.json`, `x dev` prints its path, and `x manifest` is the only
330
+ * thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
331
+ * `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
332
+ *
333
+ * An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
334
+ * `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
335
+ * `drift`, `budgets`, `seo`, `i18n` and `policy` each use.
336
+ */
337
+ function manifestMissingFindings(root: string): readonly Finding[] {
338
+ if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
339
+ if (existsSync(join(root, MANIFEST_FILENAME))) return [];
340
+ return [
341
+ {
342
+ code: 'X_MANIFEST_MISSING',
343
+ cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
344
+ fix: 'x manifest',
345
+ docs: ERROR_DOCS_URL,
346
+ at: MANIFEST_FILENAME,
347
+ },
348
+ ];
349
+ }
350
+
315
351
  /** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
316
352
  async function driftFindings(root: string): Promise<readonly Finding[]> {
317
353
  const path = join(root, MANIFEST_FILENAME);
@@ -41,6 +41,14 @@ export const VERIFY_STEP_NAMES = [
41
41
  // registries that load filled. Until it existed, an app could ship every user-facing string as
42
42
  // `⟦key⟧` with `x verify` green, because nothing in the gate ever asked (issue #249).
43
43
  'i18n',
44
+ // Twentieth, by the same test `seo` and `i18n` each passed: a rider must ask the SAME question
45
+ // off the same data, and "was this import legal?" is not "does the permission this app grants
46
+ // and requires exist?". Reported under `budgets` it would hand the reader a byte budget for an
47
+ // authz defect (axiom 4). Until it existed, `x new` shipped an app that answered HTTP 500 with
48
+ // X_PERMISSION_UNKNOWN on two of its three routes under a green gate: `defineRoles()` accepts an
49
+ // undeclared grant in silence and `RouteGuard.permission` is a bare string. It costs no second
50
+ // app load — `budgets` already imported every module, and this reads the registries that filled.
51
+ 'policy',
44
52
  'manifest',
45
53
  'roadmap',
46
54
  ] as const;
@@ -1,129 +0,0 @@
1
- // Every `solid-js` import in an island chunk resolves to Solid's PRODUCTION browser build.
2
- // `Bun.build({ target: 'browser' })` always adds the `development` export condition and offers no
3
- // option that removes it — `conditions`, `production`, `env` and `define` were each measured under
4
- // Bun 1.4 and none of them does — so without this seam an island ships the dev bundle silently.
5
-
6
- // `node:path` by necessity: Bun ships no path API, and this file resolves a package entry back
7
- // to the directory its `exports` map is relative to.
8
- import { dirname, join } from 'node:path';
9
- import type { BunPlugin } from 'bun';
10
- import { IslandBuildFailedError } from './errors';
11
-
12
- /**
13
- * The conditions an island's `solid-js` subpath is resolved under. `development` is the one NOT in
14
- * the set, which is the whole point of the file; `production` is in it because an island chunk is
15
- * only ever built to be shipped — `x dev` serves the same chunk the container does, so a second
16
- * answer here would be a bundle the byte budget never measured.
17
- */
18
- const ISLAND_CONDITIONS: ReadonlySet<string> = new Set([
19
- 'production',
20
- 'browser',
21
- 'module',
22
- 'import',
23
- 'default',
24
- ]);
25
-
26
- /** `solid-js` and its subpaths, and nothing else: Solid is the runtime an island is compiled for. */
27
- const SOLID_SPECIFIER = /^solid-js(?:\/|$)/;
28
-
29
- /**
30
- * Node's conditional-exports walk, restricted to what this file needs: the first key of the object
31
- * that the build's condition set contains, depth-first, with an array as an ordered fallback list.
32
- * Written out rather than delegated to `Bun.resolveSync` because the ONE thing it has to do
33
- * differently from Bun's resolver is refuse `development` — and `types` with it, which would
34
- * otherwise win on Solid's map and hand the bundler a `.d.ts`.
35
- */
36
- export function selectCondition(node: unknown, conditions: ReadonlySet<string>): string | null {
37
- if (typeof node === 'string') return node;
38
- if (Array.isArray(node)) {
39
- for (const alternative of node as readonly unknown[]) {
40
- const picked = selectCondition(alternative, conditions);
41
- if (picked !== null) return picked;
42
- }
43
- return null;
44
- }
45
- if (typeof node !== 'object' || node === null) return null;
46
- for (const [condition, value] of Object.entries(node)) {
47
- if (!conditions.has(condition)) continue;
48
- const picked = selectCondition(value, conditions);
49
- if (picked !== null) return picked;
50
- }
51
- return null;
52
- }
53
-
54
- /** One parse per manifest: an island imports Solid from several files, and every file asks again. */
55
- const exportsCache = new Map<string, unknown>();
56
-
57
- async function exportsOf(manifest: string): Promise<unknown> {
58
- const hit = exportsCache.get(manifest);
59
- if (hit !== undefined || exportsCache.has(manifest)) return hit;
60
- const parsed: unknown = JSON.parse(await Bun.file(manifest).text());
61
- const field =
62
- typeof parsed === 'object' && parsed !== null && 'exports' in parsed
63
- ? (parsed as { readonly exports?: unknown }).exports
64
- : undefined;
65
- exportsCache.set(manifest, field);
66
- return field;
67
- }
68
-
69
- /** Test seam: the cache is process-global because `x dev` rebuilds in one process. */
70
- export function clearSolidExportsCache(): void {
71
- exportsCache.clear();
72
- }
73
-
74
- /**
75
- * The absolute file `specifier` must resolve to, or `null` for "Bun's own answer is already the
76
- * right one" — which is every subpath Solid declares as a plain string or a pattern, since a
77
- * declaration with no conditions on it cannot select the development build.
78
- */
79
- export async function solidProductionEntry(
80
- specifier: string,
81
- resolveDir: string,
82
- importer: string,
83
- ): Promise<string | null> {
84
- let manifest: string;
85
- try {
86
- // `solid-js/package.json` is an `exports` entry of Solid's own map, so this reaches the exact
87
- // copy the island would have imported — not a hoisted sibling at a different version.
88
- manifest = Bun.resolveSync('solid-js/package.json', resolveDir);
89
- } catch {
90
- // Solid is not installed here. Bun's resolver says so, in its own words, naming the importer.
91
- return null;
92
- }
93
- const field = await exportsOf(manifest);
94
- const subpath = specifier === 'solid-js' ? '.' : `.${specifier.slice('solid-js'.length)}`;
95
- if (typeof field !== 'object' || field === null || !(subpath in field)) return null;
96
-
97
- const entry = selectCondition((field as Record<string, unknown>)[subpath], ISLAND_CONDITIONS);
98
- const file = entry === null ? null : join(dirname(manifest), entry);
99
- if (file === null || !(await Bun.file(file).exists())) {
100
- throw new IslandBuildFailedError({
101
- file: importer.length > 0 ? importer : specifier,
102
- logs:
103
- `${specifier} has no production browser entry: ${manifest} answers ` +
104
- `${entry === null ? 'nothing' : JSON.stringify(entry)} under ` +
105
- `[${[...ISLAND_CONDITIONS].join(', ')}], and an island may not ship Solid's ` +
106
- 'development build',
107
- });
108
- }
109
- return file;
110
- }
111
-
112
- /**
113
- * The plugin `island-bundle.ts` hands `Bun.build`, beside `solidJsxPlugin`. Stateless apart from
114
- * the manifest cache, so one frozen descriptor serves every concurrent island build.
115
- *
116
- * The absolute path it answers with no longer matches `SOLID_SPECIFIER`, so Solid's own internal
117
- * `import … from 'solid-js'` is the only re-entry — and that one is wanted: it is how `web.js`
118
- * reaches `solid.js` rather than `dev.js`.
119
- */
120
- export const solidProductionPlugin: BunPlugin = {
121
- name: 'ultimate-island-solid-production',
122
- setup(build): void {
123
- build.onResolve({ filter: SOLID_SPECIFIER }, async ({ path, importer, resolveDir }) => {
124
- const from = resolveDir.length > 0 ? resolveDir : dirname(importer);
125
- const entry = await solidProductionEntry(path, from, importer);
126
- return entry === null ? undefined : { path: entry };
127
- });
128
- },
129
- };