@metaobjectsdev/cli 0.24.5 → 0.25.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/README.md +2 -1
- package/dist/src/commands/docs.d.ts +14 -1
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +153 -8
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +1 -1
- package/dist/src/commands/eject.js +3 -3
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +43 -20
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +4 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +31 -6
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/types.d.ts +2 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +165 -28
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/verify.d.ts +9 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +291 -50
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +82 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +77 -0
- package/dist/src/lib/advisory.d.ts.map +1 -0
- package/dist/src/lib/advisory.js +97 -0
- package/dist/src/lib/advisory.js.map +1 -0
- package/dist/src/lib/anti-patterns.d.ts +27 -3
- package/dist/src/lib/anti-patterns.d.ts.map +1 -1
- package/dist/src/lib/anti-patterns.js +145 -8
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/args.d.ts +27 -1
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +13 -1
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/docs-drift.d.ts +31 -0
- package/dist/src/lib/docs-drift.d.ts.map +1 -0
- package/dist/src/lib/docs-drift.js +195 -0
- package/dist/src/lib/docs-drift.js.map +1 -0
- package/dist/src/lib/format.d.ts +10 -0
- package/dist/src/lib/format.d.ts.map +1 -1
- package/dist/src/lib/format.js +15 -0
- package/dist/src/lib/format.js.map +1 -1
- package/dist/src/lib/output.d.ts +13 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +16 -1
- package/dist/src/lib/output.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/docs.ts +194 -8
- package/src/commands/eject.ts +3 -3
- package/src/commands/gen.ts +54 -19
- package/src/commands/init.ts +35 -6
- package/src/commands/types.ts +185 -34
- package/src/commands/verify.ts +350 -46
- package/src/index.ts +91 -6
- package/src/lib/advisory.ts +150 -0
- package/src/lib/anti-patterns.ts +163 -8
- package/src/lib/args.ts +40 -2
- package/src/lib/docs-drift.ts +222 -0
- package/src/lib/format.ts +14 -0
- package/src/lib/output.ts +33 -2
package/src/commands/docs.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
makeRenderContext,
|
|
23
23
|
buildPkMap,
|
|
24
24
|
buildRelationMap,
|
|
25
|
+
buildProjectionViews,
|
|
25
26
|
resolveDocsConfig,
|
|
26
27
|
apiLabel,
|
|
27
28
|
} from "@metaobjectsdev/codegen-ts";
|
|
@@ -30,11 +31,28 @@ import type {
|
|
|
30
31
|
EmittedFile,
|
|
31
32
|
ResolvedDocsConfig,
|
|
32
33
|
DocsSurface,
|
|
34
|
+
Dialect,
|
|
35
|
+
ColumnNamingStrategy,
|
|
33
36
|
} from "@metaobjectsdev/codegen-ts";
|
|
34
|
-
import { docsFile, apiDocsFile, requirementsFile } from "@metaobjectsdev/codegen-ts/generators";
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
+
import { docsFile, apiDocsFile, requirementsFile, agentDocsFile } from "@metaobjectsdev/codegen-ts/generators";
|
|
38
|
+
import {
|
|
39
|
+
composeRegistry,
|
|
40
|
+
coreProviders,
|
|
41
|
+
DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
42
|
+
renderCoreMetamodelDocs,
|
|
43
|
+
} from "@metaobjectsdev/metadata";
|
|
44
|
+
import { dbEmittingObjects, DEFAULT_DIALECT, missingDialectMessage } from "@metaobjectsdev/codegen-ts";
|
|
45
|
+
import type { MetaDataTypeProvider, MetaRoot } from "@metaobjectsdev/metadata";
|
|
37
46
|
import { generateSite, SITE_TEMPLATE_NAMES, SITE_ASSET_NAMES, readSiteFile } from "@metaobjectsdev/docs-site";
|
|
47
|
+
// The `agent` schema surface takes the physical schema as an ARGUMENT with its resolvers
|
|
48
|
+
// injected — codegen-ts deliberately owns none of it (see agent-schema-input.ts). THIS is
|
|
49
|
+
// where the two packages meet: `meta docs` already depends on both.
|
|
50
|
+
import {
|
|
51
|
+
buildExpectedSchemaWithProvenance,
|
|
52
|
+
columnTypeSql,
|
|
53
|
+
qualifiedDbName,
|
|
54
|
+
} from "@metaobjectsdev/migrate-ts";
|
|
55
|
+
import type { AgentSchemaInput, SchemaColumnLike } from "@metaobjectsdev/codegen-ts";
|
|
38
56
|
|
|
39
57
|
type DocsLayout = "flat" | "package";
|
|
40
58
|
|
|
@@ -106,6 +124,7 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
106
124
|
let wantModel = false;
|
|
107
125
|
let wantApi = false;
|
|
108
126
|
let wantRequirements = false;
|
|
127
|
+
let wantAgent = false;
|
|
109
128
|
let wantMetamodel = false;
|
|
110
129
|
let wantSite = false;
|
|
111
130
|
let wantScaffoldSite = false;
|
|
@@ -133,6 +152,8 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
133
152
|
wantApi = true;
|
|
134
153
|
} else if (a === "--requirements") {
|
|
135
154
|
wantRequirements = true;
|
|
155
|
+
} else if (a === "--agent") {
|
|
156
|
+
wantAgent = true;
|
|
136
157
|
} else if (a === "--metamodel") {
|
|
137
158
|
wantMetamodel = true;
|
|
138
159
|
} else if (a === "--site") {
|
|
@@ -175,6 +196,7 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
175
196
|
if (wantModel) surfaces.push("model");
|
|
176
197
|
if (wantApi) surfaces.push("api");
|
|
177
198
|
if (wantRequirements) surfaces.push("requirements");
|
|
199
|
+
if (wantAgent) surfaces.push("agent");
|
|
178
200
|
return {
|
|
179
201
|
// `<project-root>` is the project root that CONTAINS the metadata; default
|
|
180
202
|
// cwd (mirrors how migrate/gen treat the working directory as the root).
|
|
@@ -197,7 +219,99 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
197
219
|
};
|
|
198
220
|
}
|
|
199
221
|
|
|
200
|
-
|
|
222
|
+
/**
|
|
223
|
+
* How a caller other than the CLI wants `meta docs` to behave.
|
|
224
|
+
*
|
|
225
|
+
* `verify --docs` runs this exact command into a temp directory and diffs the result, so
|
|
226
|
+
* that the gate and the door can never be two implementations of "what the docs are". The
|
|
227
|
+
* only thing it needs differently is silence: a verify run announcing "meta docs — wrote 8
|
|
228
|
+
* entity pages" in the middle of its own report describes work the user is not getting.
|
|
229
|
+
*/
|
|
230
|
+
export interface DocsCommandOptions {
|
|
231
|
+
/** Suppress the informational output. Warnings and errors still print — a docs run that
|
|
232
|
+
* degraded is something a verify caller must see, not something to swallow. */
|
|
233
|
+
silent?: boolean;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The physical schema `agent/schema.md` renders from, built by the package that OWNS it
|
|
238
|
+
* (`migrate-ts`) with its own resolvers handed across — see codegen-ts's
|
|
239
|
+
* agent-schema-input.ts for why the docs generator refuses to compute any of this itself.
|
|
240
|
+
*
|
|
241
|
+
* Returns undefined, after warning, when the page is to be SKIPPED — and a skipped page is
|
|
242
|
+
* not a silent pass: `verify --docs` convicts a committed `agent/<page>.md` that a fresh
|
|
243
|
+
* run no longer emits (see lib/docs-drift.ts), so a page describing the previous schema
|
|
244
|
+
* fails the gate rather than surviving it on exactly the change it most needs to flag.
|
|
245
|
+
*/
|
|
246
|
+
function buildAgentSchemaInput(
|
|
247
|
+
root: MetaRoot,
|
|
248
|
+
configured: Dialect | undefined,
|
|
249
|
+
strategy: ColumnNamingStrategy,
|
|
250
|
+
): AgentSchemaInput | undefined {
|
|
251
|
+
// THE RUNNER'S OWN GUARD DECIDES THIS, not a default applied here.
|
|
252
|
+
//
|
|
253
|
+
// `DEFAULT_DIALECT` is INERT: `runGen` throws when a model emits database code and the
|
|
254
|
+
// config declares no dialect, and it throws BEFORE `normalizeConfig` fills that default
|
|
255
|
+
// in, precisely so a DB project that forgot one gets a named error instead of
|
|
256
|
+
// "a Postgres project quietly emitting sqlite". So a persisted model with no dialect is
|
|
257
|
+
// not a sqlite project — it is a project `meta gen` REFUSES. Documenting it as sqlite
|
|
258
|
+
// would state an answer the toolchain never gave, about a schema it will not build.
|
|
259
|
+
//
|
|
260
|
+
// Two wrong answers were tried here before this one, and both were ASSERTIONS. First a
|
|
261
|
+
// hardcoded `?? "sqlite"`; then a skip whenever `dialect` was absent, on the theory that
|
|
262
|
+
// an undeclared dialect is an unknown one. The compute answer is to ask the predicate
|
|
263
|
+
// `runGen` asks — `dbEmittingObjects` — and skip only when that guard would fire. A model
|
|
264
|
+
// with no persisted object needs no dialect and renders an empty schema page anyway, so
|
|
265
|
+
// the inert default is correct for exactly the projects the guard lets through.
|
|
266
|
+
const dbEmitting = dbEmittingObjects(root.objects());
|
|
267
|
+
if (dbEmitting.length > 0 && configured === undefined) {
|
|
268
|
+
log.warn(
|
|
269
|
+
`docs: agent/schema.md skipped — ${missingDialectMessage(dbEmitting)} ` +
|
|
270
|
+
`('meta gen' refuses this model for the same reason.)`,
|
|
271
|
+
);
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const dialect = configured ?? DEFAULT_DIALECT;
|
|
275
|
+
try {
|
|
276
|
+
const built = buildExpectedSchemaWithProvenance(root, {
|
|
277
|
+
dialect,
|
|
278
|
+
columnNamingStrategy: strategy,
|
|
279
|
+
// Views come from codegen-ts (migrate-ts never generates view DDL), exactly as
|
|
280
|
+
// `verify --db` threads them.
|
|
281
|
+
views: buildProjectionViews(root, { dialect, columnNamingStrategy: strategy }),
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
dialect,
|
|
285
|
+
tables: built.snapshot.tables,
|
|
286
|
+
views: built.snapshot.views,
|
|
287
|
+
provenance: built.provenance,
|
|
288
|
+
// The structural `SchemaColumnLike` is migrate-ts's own ColumnDescriptor,
|
|
289
|
+
// narrowed to what the page reads; the cast hands the full descriptor back to
|
|
290
|
+
// the renderer that produced it.
|
|
291
|
+
columnType: (c: SchemaColumnLike) => columnTypeSql(c as never, dialect),
|
|
292
|
+
qualify: qualifiedDbName,
|
|
293
|
+
};
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// A model the schema builder refuses is a real condition (a primary-key move, a
|
|
296
|
+
// duplicate physical name) that `meta migrate` will report properly. Docs must not
|
|
297
|
+
// be the command that fails on it, so the schema page is skipped and the other two
|
|
298
|
+
// agent pages still emit.
|
|
299
|
+
log.warn(
|
|
300
|
+
`docs: agent/schema.md skipped — the expected schema could not be built ` +
|
|
301
|
+
`(${(err as Error).message}). Run 'meta migrate' for the full diagnosis.`,
|
|
302
|
+
);
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function docsCommand(
|
|
308
|
+
args: string[],
|
|
309
|
+
cwd: string,
|
|
310
|
+
opts?: DocsCommandOptions,
|
|
311
|
+
): Promise<number> {
|
|
312
|
+
// Informational output only. `log.warn` / `log.error` are deliberately NOT routed
|
|
313
|
+
// through this: a skipped surface or a failed render is a finding either way.
|
|
314
|
+
const info = opts?.silent === true ? (_m: string): void => {} : log.info;
|
|
201
315
|
let flags: DocsFlags;
|
|
202
316
|
try {
|
|
203
317
|
flags = parseDocsArgs(args, cwd);
|
|
@@ -211,6 +325,33 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
211
325
|
// NEITHER a user's metadata NOR a config — there is nothing to load. It writes
|
|
212
326
|
// the renderer's files under <out>/metamodel/ (default ./docs/metamodel).
|
|
213
327
|
if (flags.metamodel) {
|
|
328
|
+
// `--site` builds HTML from a MODEL — docs-site's own loader and templates over the
|
|
329
|
+
// user's metadata. The metamodel surface is a different renderer entirely, over the
|
|
330
|
+
// registry, and it emits markdown; there is no renderer here to bridge them. The flag
|
|
331
|
+
// used to be parsed, accepted and then dropped by this very return: the command wrote
|
|
332
|
+
// sixteen markdown files, printed a success line and exited 0, so asking for a site
|
|
333
|
+
// produced no site and no complaint.
|
|
334
|
+
//
|
|
335
|
+
// It refuses rather than growing an HTML renderer, which would put a markdown-
|
|
336
|
+
// rendering dependency into a published package for one surface. The website renders
|
|
337
|
+
// it instead, keeping that dependency dev-only and giving the pages the metaobjects.dev
|
|
338
|
+
// look rather than the docs-site adopter theme.
|
|
339
|
+
// `--scaffold-site` is here for the same reason and was missed the first time: it is
|
|
340
|
+
// the OTHER flag that asks for files to be written, and this same early return dropped
|
|
341
|
+
// it identically — `--metamodel --scaffold-site` wrote 16 markdown pages, scaffolded no
|
|
342
|
+
// theme anywhere, and exited 0. Fixing one of a matched pair leaves the defect wearing
|
|
343
|
+
// a different flag name.
|
|
344
|
+
const dropped = [
|
|
345
|
+
...(flags.site ? ["--site"] : []),
|
|
346
|
+
...(flags.scaffoldSite ? ["--scaffold-site"] : []),
|
|
347
|
+
];
|
|
348
|
+
if (dropped.length > 0) {
|
|
349
|
+
log.error(
|
|
350
|
+
`docs: ${dropped.join(" and ")} ${dropped.length > 1 ? "are" : "is"} not supported ` +
|
|
351
|
+
"with --metamodel. The metamodel reference is markdown; the rendered form is " +
|
|
352
|
+
"published at https://metaobjects.dev/reference");
|
|
353
|
+
return 2;
|
|
354
|
+
}
|
|
214
355
|
return metamodelDocsCommand(cwd, flags.out);
|
|
215
356
|
}
|
|
216
357
|
|
|
@@ -376,6 +517,13 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
376
517
|
loadedRoot: root,
|
|
377
518
|
outDir,
|
|
378
519
|
dbImport: "",
|
|
520
|
+
// The project's own prefix, not the "" default. `agent/ui.md` documents an ENDPOINT,
|
|
521
|
+
// and the routes generator mounts every one of them inside
|
|
522
|
+
// `fastify.register(…, { prefix: apiPrefix })` — so a project configuring "/api" is
|
|
523
|
+
// served at /api/authors and was being told, by the page whose whole job is to be
|
|
524
|
+
// right about addresses, that it was /authors. `meta gen` threads this (runner.ts);
|
|
525
|
+
// `meta docs` is the OTHER door onto the same page and did not.
|
|
526
|
+
apiPrefix: loadedConfig?.apiPrefix ?? "",
|
|
379
527
|
pkMap: buildPkMap(root),
|
|
380
528
|
relationMap: buildRelationMap(root),
|
|
381
529
|
});
|
|
@@ -498,16 +646,46 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
498
646
|
// Surfaces owned by other ports: link only, with a pointer to where they
|
|
499
647
|
// get produced.
|
|
500
648
|
for (const s of labeled.filter((s) => s.lang !== "ts")) {
|
|
501
|
-
|
|
649
|
+
info(
|
|
502
650
|
`meta docs: api surface '${s.lang}' (${s.subDir}) is produced by that port's docs command — run it to populate those pages.`,
|
|
503
651
|
);
|
|
504
652
|
}
|
|
505
653
|
} else if (hasConfig) {
|
|
506
654
|
// Config present but failed to load — already warned above; don't claim an
|
|
507
655
|
// api surface we couldn't build.
|
|
508
|
-
|
|
656
|
+
info("meta docs: api surface skipped — metaobjects.config.ts failed to load.");
|
|
657
|
+
} else {
|
|
658
|
+
info("meta docs: api surface skipped — no metaobjects.config.ts (nothing generated to document).");
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// AGENT surface — three pages an agent reads BEFORE touching a tier (`agent/schema.md`
|
|
663
|
+
// before persistence, `agent/ui.md` before a form or grid, `agent/requirements.md`
|
|
664
|
+
// before adding a capability). The fourth file the always-on pointer names,
|
|
665
|
+
// `api/AGENT-API.md`, belongs to the api surface above.
|
|
666
|
+
//
|
|
667
|
+
// Gated on a loadable gen config exactly as `api` is, and for a stronger reason: the
|
|
668
|
+
// physical schema depends on the project's DIALECT and column-naming strategy, and the
|
|
669
|
+
// neutral model surface above runs on a placeholder because it documents no SQL at all.
|
|
670
|
+
// The dialect here is the project's own, resolved by the same default `meta gen` and
|
|
671
|
+
// `meta migrate` apply — see `buildAgentSchemaInput`.
|
|
672
|
+
if (docsCfg.surfaces.includes("agent")) {
|
|
673
|
+
if (loadedConfig !== undefined) {
|
|
674
|
+
const strategy = loadedConfig.columnNamingStrategy ?? DEFAULT_COLUMN_NAMING_STRATEGY;
|
|
675
|
+
const schema = buildAgentSchemaInput(root, loadedConfig.dialect, strategy);
|
|
676
|
+
emit.push(
|
|
677
|
+
...(await agentDocsFile({
|
|
678
|
+
...(schema !== undefined && { schema }),
|
|
679
|
+
columnNamingStrategy: strategy,
|
|
680
|
+
}).generate(ctx)),
|
|
681
|
+
);
|
|
682
|
+
} else if (hasConfig) {
|
|
683
|
+
info("meta docs: agent surface skipped — metaobjects.config.ts failed to load.");
|
|
509
684
|
} else {
|
|
510
|
-
|
|
685
|
+
info(
|
|
686
|
+
"meta docs: agent surface skipped — no metaobjects.config.ts (the physical schema " +
|
|
687
|
+
"and the generated UI are what it describes).",
|
|
688
|
+
);
|
|
511
689
|
}
|
|
512
690
|
}
|
|
513
691
|
|
|
@@ -548,7 +726,15 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
548
726
|
// did not run — the opposite of the silence the empty-ledger guard exists to produce.
|
|
549
727
|
const requirementFiles = emit.filter((f) => f.path.startsWith("requirements.")).length;
|
|
550
728
|
const reqSummary = requirementFiles > 0 ? `; ${requirementFiles} requirement file(s)` : "";
|
|
551
|
-
|
|
729
|
+
// Same rule as the requirements line: NAMED rather than counted, and only when written.
|
|
730
|
+
// Each agent page is skipped when its tier has nothing to describe, so "3 agent pages"
|
|
731
|
+
// would leave a reader unable to tell which three — and the pages are the thing the
|
|
732
|
+
// always-on agent context points at by name.
|
|
733
|
+
const agentPages = emit
|
|
734
|
+
.filter((f) => f.path.startsWith("agent/"))
|
|
735
|
+
.map((f) => basename(f.path));
|
|
736
|
+
const agentSummary = agentPages.length > 0 ? `; agent/${agentPages.sort().join(" + agent/")}` : "";
|
|
737
|
+
info(`meta docs — wrote ${modelSummary}; ${apiSummary}${reqSummary}${agentSummary} → ${outDir}`);
|
|
552
738
|
return 0;
|
|
553
739
|
}
|
|
554
740
|
|
package/src/commands/eject.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FR-040 §4.2(a) — `meta eject <generator>` takes ownership of any reference-template
|
|
2
2
|
// generator, in any package, at any time after `meta init`. ADR-0034 scaffold-and-own
|
|
3
|
-
// has `init` copy
|
|
3
|
+
// has `init` copy five of them eagerly (entity, queries, routes, barrel, names); this is the
|
|
4
4
|
// SAME copy operation, generalised to every ejectable name and callable on demand — for
|
|
5
5
|
// a generator you skipped at init time, or one a package gained since.
|
|
6
6
|
import { mkdir, writeFile, stat, readFile } from "node:fs/promises";
|
|
@@ -147,7 +147,7 @@ function requiredPackages(templateSource: string): string[] {
|
|
|
147
147
|
* declared dependencies or their `tsc` reports TS2307 on the file we just told them
|
|
148
148
|
* they own — and under a strict (pnpm/npm) node_modules layout `meta gen` cannot
|
|
149
149
|
* resolve it either. `meta init` already calls this out by ADDING the two packages its
|
|
150
|
-
*
|
|
150
|
+
* five scaffolded generators need; the on-demand templates import two more
|
|
151
151
|
* (codegen-ts-react, codegen-ts-tanstack) that nothing declares.
|
|
152
152
|
*
|
|
153
153
|
* This REPORTS rather than edits: init is a scaffolder writing a whole project and has
|
|
@@ -255,7 +255,7 @@ export async function ejectCommand(args: string[], cwd: string): Promise<number>
|
|
|
255
255
|
//
|
|
256
256
|
// But eject reads no config, so it cannot know WHICH of the three states this project
|
|
257
257
|
// is in, and stating one of them as fact is wrong in the other two — including for the
|
|
258
|
-
//
|
|
258
|
+
// five `meta init` scaffolds, whose config already imports from ./codegen/generators/,
|
|
259
259
|
// which is precisely the `meta eject <name> --force` re-sync case. Name the goal, then
|
|
260
260
|
// the three branches; the reader knows which one they are looking at.
|
|
261
261
|
log.info(`In metaobjects.config.ts, "${result.exportName}" must resolve to this file:`);
|
package/src/commands/gen.ts
CHANGED
|
@@ -9,6 +9,10 @@ import { log } from "../lib/log.js";
|
|
|
9
9
|
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
10
10
|
import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
|
|
11
11
|
import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
|
|
12
|
+
import {
|
|
13
|
+
antiPatternRows, ranSection, skippedSection, warnCapped,
|
|
14
|
+
type AdvisoryFindingRow, type AdvisorySection,
|
|
15
|
+
} from "../lib/advisory.js";
|
|
12
16
|
import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
|
|
13
17
|
import { runGen, listGenerators } from "@metaobjectsdev/codegen-ts";
|
|
14
18
|
import type { WriteStatus } from "@metaobjectsdev/codegen-ts";
|
|
@@ -149,12 +153,23 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
149
153
|
(forgeConfig.targets ? Object.values(forgeConfig.targets).map((t) => t.outDir) : [])
|
|
150
154
|
.concat([forgeConfig.outDir]),
|
|
151
155
|
));
|
|
156
|
+
|
|
157
|
+
// The advisory verify-as-teacher pass now runs BEFORE the result is rendered,
|
|
158
|
+
// because its findings ride IN the result. It used to run after the payload was
|
|
159
|
+
// printed and write only to stderr as text, so `meta gen --format json` on a run
|
|
160
|
+
// with hundreds of findings emitted a document that mentioned none of them — the
|
|
161
|
+
// structured output being the documented default for an agent on a pipe.
|
|
162
|
+
// Warnings ONLY — nothing here reaches the exit code (bias to under-flagging).
|
|
163
|
+
const antiPatterns = runAntiPatternScan(
|
|
164
|
+
projectRoot, cliConfig.dryRun, flags.noAntipatterns, forgeConfig.verify?.antiPatternIgnore);
|
|
165
|
+
|
|
152
166
|
const genResult = {
|
|
153
167
|
files,
|
|
154
168
|
outDir: targetDirs.length > 1 ? targetDirs.join(", ") : forgeConfig.outDir,
|
|
155
169
|
dialect: forgeConfig.dialect,
|
|
156
170
|
dryRun: cliConfig.dryRun,
|
|
157
171
|
warnings: [],
|
|
172
|
+
antiPatterns,
|
|
158
173
|
};
|
|
159
174
|
const output =
|
|
160
175
|
fmt === "toon" ? formatGenResultToon(genResult)
|
|
@@ -175,31 +190,51 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
175
190
|
);
|
|
176
191
|
}
|
|
177
192
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (findings.length > 0) {
|
|
188
|
-
const CAP = 10;
|
|
189
|
-
log.warn(
|
|
190
|
-
`\nmeta gen — ${findings.length} place(s) hand-roll what MetaObjects can model ` +
|
|
191
|
-
`(advisory — declaring the construct lets codegen own it):`,
|
|
192
|
-
);
|
|
193
|
-
for (const f of findings.slice(0, CAP)) log.warn(` ${f.message}`);
|
|
194
|
-
if (findings.length > CAP) log.warn(` …and ${findings.length - CAP} more.`);
|
|
195
|
-
}
|
|
196
|
-
} catch { /* never let an advisory scan break gen */ }
|
|
193
|
+
// The human-readable half of the advisory pass, printed after the file listing
|
|
194
|
+
// where a reader expects it. Capped for a terminal; the structured payload above
|
|
195
|
+
// already carried every finding, and the tail line says so.
|
|
196
|
+
if (antiPatterns.total > 0) {
|
|
197
|
+
log.warn(
|
|
198
|
+
`\nmeta gen — ${antiPatterns.total} place(s) hand-roll what MetaObjects can model ` +
|
|
199
|
+
`(advisory — declaring the construct lets codegen own it):`,
|
|
200
|
+
);
|
|
201
|
+
warnCapped(antiPatterns.rows.map((r) => ` ${r.message}`), flags.limit, { structured: fmt !== "text" });
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
const hasFailure = files.some((f) => f.status === "conflict" || f.status === "refused");
|
|
200
205
|
return hasFailure ? 1 : 0;
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Run the advisory verify-as-teacher scan (same pass `meta verify` runs): surface
|
|
210
|
+
* authored source that hand-rolls what the metadata could model. `gen` is the
|
|
211
|
+
* command an agent always runs, so this is where the teaching actually reaches it.
|
|
212
|
+
*
|
|
213
|
+
* Returns the section EITHER WAY — a skip is reported with its reason rather than
|
|
214
|
+
* dropped, so a reader can tell "found nothing" from "never looked". Warnings only;
|
|
215
|
+
* it can never affect the exit code. Suppress with --no-antipatterns or
|
|
216
|
+
* META_NO_ANTIPATTERNS=1 (both opt-outs work on `meta gen` and `meta verify`).
|
|
217
|
+
*/
|
|
218
|
+
function runAntiPatternScan(
|
|
219
|
+
projectRoot: string,
|
|
220
|
+
dryRun: boolean,
|
|
221
|
+
noAntipatterns: boolean,
|
|
222
|
+
ignore: readonly string[] | undefined,
|
|
223
|
+
): AdvisorySection<AdvisoryFindingRow> {
|
|
224
|
+
// A --dry-run writes nothing, so it teaches nothing; the scan is skipped, as it
|
|
225
|
+
// always has been. The payload now says that instead of looking clean.
|
|
226
|
+
if (dryRun) return skippedSection("skipped on --dry-run (the advisory pass runs on a real write run)");
|
|
227
|
+
if (noAntipatterns) return skippedSection("suppressed by --no-antipatterns");
|
|
228
|
+
if (process.env.META_NO_ANTIPATTERNS === "1") return skippedSection("suppressed by META_NO_ANTIPATTERNS=1");
|
|
229
|
+
try {
|
|
230
|
+
return ranSection(antiPatternRows(scanSourceForAntiPatterns(
|
|
231
|
+
projectRoot, ignore !== undefined ? { ignore } : undefined)));
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Never let an advisory scan break gen — but never claim it found nothing either.
|
|
234
|
+
return skippedSection(`the scan failed: ${(err as Error).message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
/**
|
|
204
239
|
* `meta gen --list` — print the stable-name generator registry (ADR-0021 D3).
|
|
205
240
|
*
|
package/src/commands/init.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile, readFile, readdir, stat } from "node:fs/promises";
|
|
1
|
+
import { mkdir, writeFile, readFile, readdir, stat, rm } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { basename, dirname } from "node:path";
|
|
4
4
|
import { existsSync as existsSyncWrap, readFileSync as readFileSyncWrap } from "node:fs";
|
|
@@ -19,7 +19,7 @@ import { readReferenceTemplate, type ReferenceGeneratorName } from "@metaobjects
|
|
|
19
19
|
// the consumer's repo so they OWN them; metaobjects.config.ts imports them locally.
|
|
20
20
|
const OWNED_GENERATORS_DIR = "codegen/generators";
|
|
21
21
|
|
|
22
|
-
// The
|
|
22
|
+
// The FIVE reference generators `meta init` copies EAGERLY — deliberately an explicit
|
|
23
23
|
// literal, not derived from @metaobjectsdev/codegen-ts's REFERENCE_GENERATOR_NAMES (the
|
|
24
24
|
// full list of everything `meta eject` can copy). Looping over that array unconditionally
|
|
25
25
|
// used to mean init scaffolded whatever it contained: when a later task registered
|
|
@@ -28,7 +28,13 @@ const OWNED_GENERATORS_DIR = "codegen/generators";
|
|
|
28
28
|
// scaffolded metaobjects.config.ts's import list (buildMetaobjectsConfigBody below) made
|
|
29
29
|
// explicit and checkable — anything else is eject-on-demand via `meta eject <name>`, which
|
|
30
30
|
// exists exactly so eager copying isn't the only way to take ownership of a template.
|
|
31
|
-
|
|
31
|
+
//
|
|
32
|
+
// "names" joined this list by spec §A5's ruling, not by falling out of the general rule
|
|
33
|
+
// above: TypeScript is opt-in by construction under ADR-0034 (meta gen runs the adopter's
|
|
34
|
+
// copy, so a packaged change to a generator can never reach anyone who has ejected), so
|
|
35
|
+
// the honest maximum for the names artifact is every NEW `meta init` getting it and every
|
|
36
|
+
// existing project adding one config line by hand.
|
|
37
|
+
export const SCAFFOLDED_GENERATOR_NAMES: readonly ReferenceGeneratorName[] = ["entity", "queries", "routes", "barrel", "names"];
|
|
32
38
|
|
|
33
39
|
// The scaffolded config's outDir + dbImport, as named constants so the throwing-stub
|
|
34
40
|
// path below is DERIVED from the same values the config template embeds rather than
|
|
@@ -105,6 +111,7 @@ function buildMetaobjectsConfigBody(dialect: "sqlite" | "postgres" | "d1" = "sql
|
|
|
105
111
|
import { entityFile } from "./codegen/generators/entity.js";
|
|
106
112
|
import { queriesFile } from "./codegen/generators/queries.js";
|
|
107
113
|
import { routesFile } from "./codegen/generators/routes.js";
|
|
114
|
+
import { namesFile } from "./codegen/generators/names.js";
|
|
108
115
|
import { barrel } from "./codegen/generators/barrel.js";
|
|
109
116
|
|
|
110
117
|
export default defineConfig({
|
|
@@ -126,6 +133,7 @@ export default defineConfig({
|
|
|
126
133
|
entityFile(),
|
|
127
134
|
queriesFile(),
|
|
128
135
|
routesFile(),
|
|
136
|
+
namesFile(), // <Entity>Names — physical table/column constants (spec §A1/§A5)
|
|
129
137
|
barrel(),
|
|
130
138
|
],
|
|
131
139
|
docs: {
|
|
@@ -246,6 +254,8 @@ export interface InitOptions {
|
|
|
246
254
|
export interface InitResult {
|
|
247
255
|
created: string[];
|
|
248
256
|
preserved: string[];
|
|
257
|
+
/** agent-context files deleted because this stack no longer assembles them. */
|
|
258
|
+
removed: string[];
|
|
249
259
|
warnings: string[];
|
|
250
260
|
}
|
|
251
261
|
|
|
@@ -324,6 +334,12 @@ async function writeUnlessDryRun(cwd: string, dryRun: boolean, path: string, con
|
|
|
324
334
|
await writeFile(abs, contents, "utf8");
|
|
325
335
|
}
|
|
326
336
|
|
|
337
|
+
/** Delete twin of writeUnlessDryRun — a dry run must not remove anything either. */
|
|
338
|
+
async function removeUnlessDryRun(cwd: string, dryRun: boolean, path: string): Promise<void> {
|
|
339
|
+
if (dryRun) return;
|
|
340
|
+
await rm(join(cwd, path), { force: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
327
343
|
/** "would be VERBED" during a dry run, plain VERBED otherwise — the one tense
|
|
328
344
|
* marker every reported write shares, so each call site states only its own
|
|
329
345
|
* past participle instead of writing out both tenses of the whole sentence. */
|
|
@@ -387,8 +403,21 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise
|
|
|
387
403
|
);
|
|
388
404
|
result.created.push(AGENT_CONTEXT_MANIFEST_PATH);
|
|
389
405
|
|
|
406
|
+
// A fragment this stack no longer assembles. `prunes` are ones we wrote that nobody has
|
|
407
|
+
// touched — deleting is exactly as safe as the overwrite the same hash predicate already
|
|
408
|
+
// authorises above, and leaving them contradicts every SKILL.md footer, which tells the
|
|
409
|
+
// reader to read every references/*.md "one per server language in this project's stack".
|
|
410
|
+
for (const orphan of decision.prunes) {
|
|
411
|
+
await removeUnlessDryRun(opts.cwd, dryRun, orphan);
|
|
412
|
+
result.removed.push(orphan);
|
|
413
|
+
}
|
|
414
|
+
// Hand-edited ones are never deleted — losing an adopter's writing is worse than leaving
|
|
415
|
+
// a stale file behind — so they are named, with the reason and the remedy.
|
|
390
416
|
for (const orphan of decision.removed) {
|
|
391
|
-
result.warnings.push(
|
|
417
|
+
result.warnings.push(
|
|
418
|
+
`${orphan} is no longer part of this stack but appears hand-edited, so it was kept; ` +
|
|
419
|
+
"delete it yourself once you have salvaged anything you want from it.",
|
|
420
|
+
);
|
|
392
421
|
}
|
|
393
422
|
|
|
394
423
|
if (opts.wireRoot) await wireRootMemory(opts.cwd, result, dryRun);
|
|
@@ -426,7 +455,7 @@ async function wireRootMemory(cwd: string, result: InitResult, dryRun = false):
|
|
|
426
455
|
* so a re-run with --force never clobbers a hand-edited generator. The scaffolded
|
|
427
456
|
* metaobjects.config.ts imports these local copies (not the package `/generators` export).
|
|
428
457
|
*
|
|
429
|
-
* Copies SCAFFOLDED_GENERATOR_NAMES only — the
|
|
458
|
+
* Copies SCAFFOLDED_GENERATOR_NAMES only — the five the scaffolded config actually
|
|
430
459
|
* wires — not every name @metaobjectsdev/codegen-ts happens to register. Anything else
|
|
431
460
|
* (routes-hono, and any UI-tier template from codegen-ts-react/-tanstack) is reached with
|
|
432
461
|
* `meta eject <name>`, not by eager copying.
|
|
@@ -513,7 +542,7 @@ async function writeConfigFile(opts: InitOptions, result: InitResult, agentDir:
|
|
|
513
542
|
}
|
|
514
543
|
|
|
515
544
|
export async function init(opts: InitOptions): Promise<InitResult> {
|
|
516
|
-
const result: InitResult = { created: [], preserved: [], warnings: [] };
|
|
545
|
+
const result: InitResult = { created: [], preserved: [], removed: [], warnings: [] };
|
|
517
546
|
const agentDir = join(opts.cwd, DEFAULT_METAOBJECTS_DIR);
|
|
518
547
|
const metaobjectsDir = join(opts.cwd, DEFAULT_METADATA_DIR);
|
|
519
548
|
|