@metaobjectsdev/cli 1.0.0-rc.4 → 1.0.0-rc.6
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/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +41 -9
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/gen.d.ts +8 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +17 -4
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +181 -35
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +6 -0
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +11 -5
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +2 -1
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +12 -3
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts.map +1 -1
- package/dist/src/commands/upgrade.js +38 -4
- package/dist/src/commands/upgrade.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +108 -30
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +55 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +6 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +13 -0
- package/dist/src/lib/advisory.js.map +1 -1
- package/dist/src/lib/anti-patterns.d.ts.map +1 -1
- package/dist/src/lib/anti-patterns.js +2 -4
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/args.d.ts +251 -12
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +119 -92
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/base-url-advisory.d.ts +37 -0
- package/dist/src/lib/base-url-advisory.d.ts.map +1 -0
- package/dist/src/lib/base-url-advisory.js +162 -0
- package/dist/src/lib/base-url-advisory.js.map +1 -0
- package/dist/src/lib/codegen-drift.d.ts +8 -0
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +43 -10
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts +17 -0
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +64 -2
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/docs-drift.d.ts +12 -1
- package/dist/src/lib/docs-drift.d.ts.map +1 -1
- package/dist/src/lib/docs-drift.js +85 -16
- package/dist/src/lib/docs-drift.js.map +1 -1
- package/dist/src/lib/git-ignore.d.ts +59 -0
- package/dist/src/lib/git-ignore.d.ts.map +1 -0
- package/dist/src/lib/git-ignore.js +120 -0
- package/dist/src/lib/git-ignore.js.map +1 -0
- package/dist/src/lib/ignored-scaffold-check.d.ts +12 -0
- package/dist/src/lib/ignored-scaffold-check.d.ts.map +1 -0
- package/dist/src/lib/ignored-scaffold-check.js +75 -0
- package/dist/src/lib/ignored-scaffold-check.js.map +1 -0
- package/dist/src/lib/load-error.d.ts +46 -0
- package/dist/src/lib/load-error.d.ts.map +1 -0
- package/dist/src/lib/load-error.js +51 -0
- package/dist/src/lib/load-error.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +65 -1
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
- package/dist/src/lib/manifest-ignored-check.js +23 -36
- package/dist/src/lib/manifest-ignored-check.js.map +1 -1
- package/dist/src/lib/output.d.ts +22 -1
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +39 -12
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/rel-posix.d.ts +5 -0
- package/dist/src/lib/rel-posix.d.ts.map +1 -0
- package/dist/src/lib/rel-posix.js +23 -0
- package/dist/src/lib/rel-posix.js.map +1 -0
- package/dist/src/lib/replay-remedy.d.ts +2 -0
- package/dist/src/lib/replay-remedy.d.ts.map +1 -0
- package/dist/src/lib/replay-remedy.js +69 -0
- package/dist/src/lib/replay-remedy.js.map +1 -0
- package/dist/src/lib/requirement-lint.d.ts.map +1 -1
- package/dist/src/lib/requirement-lint.js +25 -5
- package/dist/src/lib/requirement-lint.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/docs.ts +51 -12
- package/src/commands/gen.ts +19 -4
- package/src/commands/init.ts +188 -39
- package/src/commands/migrate.ts +13 -5
- package/src/commands/prompt-snapshot.ts +2 -1
- package/src/commands/types.ts +12 -3
- package/src/commands/upgrade.ts +44 -4
- package/src/commands/verify.ts +120 -32
- package/src/index.ts +55 -8
- package/src/lib/advisory.ts +15 -0
- package/src/lib/anti-patterns.ts +2 -5
- package/src/lib/args.ts +154 -109
- package/src/lib/base-url-advisory.ts +173 -0
- package/src/lib/codegen-drift.ts +52 -9
- package/src/lib/detect-stack.ts +73 -2
- package/src/lib/docs-drift.ts +107 -17
- package/src/lib/git-ignore.ts +159 -0
- package/src/lib/ignored-scaffold-check.ts +78 -0
- package/src/lib/load-error.ts +89 -0
- package/src/lib/load-metaobjects-config.ts +58 -1
- package/src/lib/manifest-ignored-check.ts +25 -33
- package/src/lib/output.ts +64 -11
- package/src/lib/rel-posix.ts +25 -0
- package/src/lib/replay-remedy.ts +78 -0
- package/src/lib/requirement-lint.ts +26 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -351,6 +351,63 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
351
351
|
} catch { /* no ts-poet adjacent to codegen-ts — leave project resolution in place */ }
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
// EVERY DECLARED SUBPATH EXPORT NEEDS ITS OWN ENTRY, because jiti's `alias` matches by
|
|
355
|
+
// PREFIX and then concatenates the remainder onto the mapped value. The mapped value is
|
|
356
|
+
// a FILE, so `@metaobjectsdev/codegen-ts/templates/inferred-types` became
|
|
357
|
+
// `…/codegen-ts/dist/index.js/templates/inferred-types` — a path that has never existed,
|
|
358
|
+
// named in an error message that mentions neither jiti nor the alias map:
|
|
359
|
+
//
|
|
360
|
+
// Cannot find module '…/@metaobjectsdev/codegen-ts/dist/index.js/templates/inferred-types'
|
|
361
|
+
//
|
|
362
|
+
// So every subpath the package deliberately exports was unloadable from an owned
|
|
363
|
+
// generator, and the documented escape (a dynamic `import()`, which this file's own
|
|
364
|
+
// comment offers) failed identically, because it falls back to the same broken matcher.
|
|
365
|
+
// A cold adoption probe hit this retargeting the entity tier off Drizzle and got past it
|
|
366
|
+
// only by reading this file's compiled output — the subpath holds `fieldTsTypeString`,
|
|
367
|
+
// the one neutral MetaField→TypeScript mapper, which anyone replacing `renderDrizzleSchema`
|
|
368
|
+
// needs. `rewriteImportSpecifiers` above was always correct here (it anchors on the
|
|
369
|
+
// closing quote, so a subpath is left alone); only the alias fallback was wrong, and the
|
|
370
|
+
// fallback is what an OWNED GENERATOR gets, since only the config file is pre-processed.
|
|
371
|
+
//
|
|
372
|
+
// DERIVED from each package's own `exports` map rather than listed. A hand-kept list of
|
|
373
|
+
// subpaths is the same defect one level up: the package adds an export, nobody edits this
|
|
374
|
+
// file, and it is unloadable again with the same unrecognisable error. Resolution goes
|
|
375
|
+
// through `createRequire().resolve`, so the exports map — including its conditions — is
|
|
376
|
+
// the authority, and a subpath that does not resolve is skipped rather than guessed at.
|
|
377
|
+
for (const [specifier, resolvedPath] of [...Object.entries(aliasMap)]) {
|
|
378
|
+
if (!specifier.startsWith("@metaobjectsdev/")) continue;
|
|
379
|
+
// The package root is the specifier itself; a specifier that is already a subpath
|
|
380
|
+
// (e.g. ".../generators") contributes nothing further.
|
|
381
|
+
if (specifier.slice("@metaobjectsdev/".length).includes("/")) continue;
|
|
382
|
+
// Read the manifest off the FILE SYSTEM by walking up from the resolved entry, not by
|
|
383
|
+
// requiring `<specifier>/package.json` — these packages do not list `./package.json` in
|
|
384
|
+
// their own exports map, so requiring it throws ERR_PACKAGE_PATH_NOT_EXPORTED and the
|
|
385
|
+
// derivation silently produces nothing. (Which it did, on the first cut of this fix:
|
|
386
|
+
// the gate in test/owned-generator-subpath-import.test.ts stayed red and named the
|
|
387
|
+
// same impossible path, which is exactly what that assertion is for.)
|
|
388
|
+
let manifest: { exports?: Record<string, unknown> } | undefined;
|
|
389
|
+
for (let dir = dirname(resolvedPath); ; dir = dirname(dir)) {
|
|
390
|
+
try {
|
|
391
|
+
const parsed = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8")) as {
|
|
392
|
+
name?: string;
|
|
393
|
+
exports?: Record<string, unknown>;
|
|
394
|
+
};
|
|
395
|
+
if (parsed.name === specifier) { manifest = parsed; break; }
|
|
396
|
+
} catch { /* not this directory — keep walking */ }
|
|
397
|
+
const parent = dirname(dir);
|
|
398
|
+
if (parent === dir) break; // filesystem root
|
|
399
|
+
}
|
|
400
|
+
if (manifest === undefined) continue; // no readable manifest — leave normal resolution
|
|
401
|
+
for (const key of Object.keys(manifest?.exports ?? {})) {
|
|
402
|
+
if (!key.startsWith("./") || key.includes("*")) continue;
|
|
403
|
+
const sub = `${specifier}/${key.slice(2)}`;
|
|
404
|
+
if (aliasMap[sub] !== undefined) continue; // an explicit CLI_PKG_PATHS entry wins
|
|
405
|
+
try {
|
|
406
|
+
aliasMap[sub] = createRequire(resolvedPath).resolve(sub);
|
|
407
|
+
} catch { /* not resolvable under these conditions — skip rather than invent a path */ }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
354
411
|
// Pre-process the config file content to rewrite @metaobjectsdev/* import
|
|
355
412
|
// specifiers to resolved absolute paths. This is the primary mechanism that
|
|
356
413
|
// ensures the CLI's own copies are used: jiti's `alias` map is kept below as
|
|
@@ -16,40 +16,14 @@
|
|
|
16
16
|
// unused artifact would be wrong.
|
|
17
17
|
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
|
-
import { spawnSync } from "node:child_process";
|
|
20
19
|
import { join } from "node:path";
|
|
21
20
|
import { log } from "./log.js";
|
|
21
|
+
import { gitIgnoreSource, isGitIgnored } from "./git-ignore.js";
|
|
22
22
|
|
|
23
23
|
/** Project-relative path of the manifest — the one artifact of `.gen-state/` that is
|
|
24
24
|
* meant to be committed. */
|
|
25
25
|
export const HASH_MANIFEST_REL = join(".metaobjects", ".gen-state", ".hashes.json");
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Ask git whether `relPath` is ignored, or undefined when the question cannot be
|
|
29
|
-
* answered (not a repository, git missing).
|
|
30
|
-
*
|
|
31
|
-
* `check-ignore` is the only reliable answer: parsing `.gitignore` by hand would have
|
|
32
|
-
* to reimplement precedence, negation and nested ignore files — and the specific trap
|
|
33
|
-
* this check exists to catch (`.gen-state/` excluding a directory so a `!` negation
|
|
34
|
-
* inside it can never apply) is exactly the rule a hand-rolled parser gets wrong.
|
|
35
|
-
*/
|
|
36
|
-
function isGitIgnored(cwd: string, relPath: string): boolean | undefined {
|
|
37
|
-
const gitBin = process.env.META_GEN_GIT ?? "git";
|
|
38
|
-
let res;
|
|
39
|
-
try {
|
|
40
|
-
res = spawnSync(gitBin, ["-C", cwd, "check-ignore", "-q", "--", relPath], {
|
|
41
|
-
encoding: "utf-8",
|
|
42
|
-
});
|
|
43
|
-
} catch {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
if (res.error !== undefined) return undefined;
|
|
47
|
-
// 0 = ignored, 1 = not ignored, 128 = not a git repo / other git error.
|
|
48
|
-
if (res.status === 0) return true;
|
|
49
|
-
if (res.status === 1) return false;
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
27
|
/**
|
|
54
28
|
* Warn once when the hash manifest is git-ignored, naming the fix.
|
|
55
29
|
*
|
|
@@ -61,15 +35,33 @@ export function warnIfManifestIgnored(cwd: string): void {
|
|
|
61
35
|
// Nothing generated yet ⇒ nothing at risk ⇒ nothing worth saying.
|
|
62
36
|
if (!existsSync(join(cwd, HASH_MANIFEST_REL))) return;
|
|
63
37
|
|
|
64
|
-
|
|
38
|
+
// honourGlobalExcludes: TRUE here, and that is the opposite call to the docs gate.
|
|
39
|
+
// The question this warning asks is "will this file reach another machine?", and a
|
|
40
|
+
// per-user ignore file answers it just as truly as a committed .gitignore does.
|
|
41
|
+
if (isGitIgnored(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true }) !== true) return;
|
|
42
|
+
|
|
43
|
+
// WHERE the rule actually lives, asked of git rather than assumed. This used to name
|
|
44
|
+
// `.metaobjects/.gitignore` and the literal pattern `.gen-state/`, which is only one of
|
|
45
|
+
// the places the exclusion can sit: an adopter estate had it in the ROOT `.gitignore` as
|
|
46
|
+
// `.metaobjects/.gen-state/`, so following this advisory to the letter edited a file that
|
|
47
|
+
// did not hold the rule, changed nothing, and printed this same line again — the loop
|
|
48
|
+
// shape. `git check-ignore -v` knows the answer, so nothing is guessed. When git cannot
|
|
49
|
+
// say, the wording degrades to the generic form rather than asserting a location.
|
|
50
|
+
const src = gitIgnoreSource(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true });
|
|
51
|
+
const where =
|
|
52
|
+
src !== undefined
|
|
53
|
+
? `The rule is '${src.pattern}' at ${src.file}:${src.line} — replace it there with ` +
|
|
54
|
+
`a '/*' form plus a '!' negation for the manifest ` +
|
|
55
|
+
`(e.g. '${src.pattern.replace(/\/$/, "")}/*' and '!${src.pattern.replace(/\/$/, "")}/.hashes.json')`
|
|
56
|
+
: `Find the rule with 'git check-ignore -v ${HASH_MANIFEST_REL}' and replace it with ` +
|
|
57
|
+
`a '/*' form plus a '!' negation for the manifest`;
|
|
65
58
|
|
|
66
59
|
log.warn(
|
|
67
60
|
`${HASH_MANIFEST_REL} is git-ignored, so it never reaches another machine — ` +
|
|
68
61
|
`on a fresh clone or CI runner 'meta gen' cannot tell your hand edits from its ` +
|
|
69
|
-
`own stale output, and will refuse to overwrite rather than guess.
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
`
|
|
73
|
-
`commit the manifest. If you deliberately do not commit generated output, ignore this.`,
|
|
62
|
+
`own stale output, and will refuse to overwrite rather than guess. ${where} ` +
|
|
63
|
+
`(the glob matters — git will not descend into an excluded directory, so a negation ` +
|
|
64
|
+
`INSIDE an excluded directory can never apply), then commit the manifest. ` +
|
|
65
|
+
`If you deliberately do not commit generated output, ignore this.`,
|
|
74
66
|
);
|
|
75
67
|
}
|
package/src/lib/output.ts
CHANGED
|
@@ -15,7 +15,27 @@ export interface FormatOptions {
|
|
|
15
15
|
// gen
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* `overwrite` is distinct from `new` on purpose. Reviewing a run, "an entity
|
|
20
|
+
* appeared" and "an existing artifact was rewritten" are different facts, and the
|
|
21
|
+
* CLI used to report both as `new` — the engine has always distinguished them
|
|
22
|
+
* (`WriteStatus.overwrite`), and the reporting layer collapsed the two. Found on
|
|
23
|
+
* two adopter estates independently, which is what makes it a reporting defect
|
|
24
|
+
* rather than a preference: the summary line was right and the per-file status
|
|
25
|
+
* was not.
|
|
26
|
+
*/
|
|
27
|
+
export type GenFileStatus =
|
|
28
|
+
| "new"
|
|
29
|
+
| "overwrite"
|
|
30
|
+
| "merged"
|
|
31
|
+
| "conflict"
|
|
32
|
+
| "unchanged"
|
|
33
|
+
| "refused"
|
|
34
|
+
/** `--baseline=adopt`: the file's existing content became the merge base and
|
|
35
|
+
* nothing was written. Never folded into "unchanged" — the run's product is the
|
|
36
|
+
* manifest, and a summary that showed these as unchanged would hide it. */
|
|
37
|
+
| "adopted"
|
|
38
|
+
| "removed";
|
|
19
39
|
|
|
20
40
|
export interface GenFileEntry {
|
|
21
41
|
path: string;
|
|
@@ -41,23 +61,35 @@ export interface GenResultShape {
|
|
|
41
61
|
* structured payload carries every finding.
|
|
42
62
|
*/
|
|
43
63
|
antiPatterns?: AdvisorySection<AdvisoryFindingRow>;
|
|
64
|
+
/**
|
|
65
|
+
* How many generators the config wired. Required, because the alternative is a
|
|
66
|
+
* message that GUESSES: with no generators wired, a run over a project with five
|
|
67
|
+
* entities emitted nothing and reported "no entities to generate … author entities
|
|
68
|
+
* in this project's metadata sources", sending the reader to edit a file that was
|
|
69
|
+
* already correct. The output's own first line (`gen: []`) already knew.
|
|
70
|
+
*/
|
|
71
|
+
generatorCount: number;
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
const GEN_GLYPHS: Record<GenFileStatus, string> = {
|
|
47
75
|
new: "✓",
|
|
76
|
+
overwrite: "↻",
|
|
48
77
|
merged: "↺",
|
|
49
78
|
conflict: "✗",
|
|
50
79
|
unchanged: "=",
|
|
51
80
|
refused: "⚠",
|
|
81
|
+
adopted: "⊕",
|
|
52
82
|
removed: "−",
|
|
53
83
|
};
|
|
54
84
|
|
|
55
85
|
const GEN_WORDS: Record<GenFileStatus, string> = {
|
|
56
86
|
new: "NEW",
|
|
87
|
+
overwrite: "OVERWRITE",
|
|
57
88
|
merged: "MERGED",
|
|
58
89
|
conflict: "CONFLICT",
|
|
59
90
|
unchanged: "UNCHANGED",
|
|
60
91
|
refused: "REFUSED",
|
|
92
|
+
adopted: "ADOPTED",
|
|
61
93
|
removed: "REMOVED",
|
|
62
94
|
};
|
|
63
95
|
|
|
@@ -88,14 +120,22 @@ export function formatGenResult(result: GenResultShape, opts: FormatOptions): st
|
|
|
88
120
|
acc[f.status] = (acc[f.status] ?? 0) + 1;
|
|
89
121
|
return acc;
|
|
90
122
|
},
|
|
91
|
-
{ new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
|
|
123
|
+
{ new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, adopted: 0, removed: 0 },
|
|
92
124
|
);
|
|
93
125
|
const parts: string[] = [];
|
|
94
|
-
|
|
126
|
+
// "written" stays the sum of new + overwrite: that number was always correct and
|
|
127
|
+
// adopters read it. Splitting the SUMMARY as well would change a line nothing was
|
|
128
|
+
// wrong with; the per-file status is the one that was lying.
|
|
129
|
+
const written = counts.new + counts.overwrite;
|
|
130
|
+
if (written > 0) parts.push(`${written} written`);
|
|
95
131
|
if (counts.merged > 0) parts.push(`${counts.merged} merged`);
|
|
96
132
|
if (counts.conflict > 0) parts.push(`${counts.conflict} conflict`);
|
|
97
133
|
if (counts.unchanged > 0) parts.push(`${counts.unchanged} unchanged`);
|
|
98
134
|
if (counts.refused > 0) parts.push(`${counts.refused} refused`);
|
|
135
|
+
// `--baseline=adopt`: recorded as the merge base, nothing written. Counted separately
|
|
136
|
+
// from `unchanged` because the run's product is the manifest, and an adopter who cannot
|
|
137
|
+
// see how many files it now covers has no way to review what they are about to commit.
|
|
138
|
+
if (counts.adopted > 0) parts.push(`${counts.adopted} adopted`);
|
|
99
139
|
if (counts.removed > 0) parts.push(`${counts.removed} removed`);
|
|
100
140
|
lines.push("", ` ${parts.join(", ")}`, "");
|
|
101
141
|
|
|
@@ -198,21 +238,34 @@ export function genResultToData(result: GenResultShape): {
|
|
|
198
238
|
} {
|
|
199
239
|
const counts = result.files.reduce<Record<GenFileStatus, number>>(
|
|
200
240
|
(a, f) => ((a[f.status] = (a[f.status] ?? 0) + 1), a),
|
|
201
|
-
{ new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
|
|
241
|
+
{ new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, adopted: 0, removed: 0 },
|
|
202
242
|
);
|
|
203
243
|
const parts: string[] = [];
|
|
204
|
-
|
|
244
|
+
// Same split as the text formatter: `written` is new + overwrite, and the
|
|
245
|
+
// per-file rows carry which one each was.
|
|
246
|
+
const written = counts.new + counts.overwrite;
|
|
247
|
+
if (written) parts.push(`${written} written`);
|
|
205
248
|
if (counts.merged) parts.push(`${counts.merged} merged`);
|
|
206
249
|
if (counts.conflict) parts.push(`${counts.conflict} conflict`);
|
|
207
250
|
if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
|
|
208
251
|
if (counts.refused) parts.push(`${counts.refused} refused`);
|
|
252
|
+
if (counts.adopted) parts.push(`${counts.adopted} adopted`);
|
|
209
253
|
if (counts.removed) parts.push(`${counts.removed} removed`);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
254
|
+
// Two ways to emit nothing, and they send the reader to different files. Name the
|
|
255
|
+
// one that is actually true: a config wiring no generators cannot produce output
|
|
256
|
+
// however good the metadata is, and telling its author to go write entities is a
|
|
257
|
+
// false statement about a project that already has them.
|
|
258
|
+
const noGenerators = result.generatorCount === 0;
|
|
259
|
+
const summary = result.files.length !== 0
|
|
260
|
+
? parts.join(", ")
|
|
261
|
+
: noGenerators
|
|
262
|
+
? "no generators are wired in metaobjects.config.ts, so nothing was generated"
|
|
263
|
+
: `no entities to generate in ${result.outDir}`;
|
|
264
|
+
const help = result.files.length !== 0
|
|
265
|
+
? ["typecheck the generated code with `npx tsc`", "create your database tables with `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`"]
|
|
266
|
+
: noGenerators
|
|
267
|
+
? ["add generators to the `generators: []` array in metaobjects.config.ts — `meta eject --list` names every generator you can own, and `meta init` scaffolds the usual set"]
|
|
268
|
+
: ["author entities in this project's metadata sources then re-run `meta gen`"];
|
|
216
269
|
// An absent section is STATED, never omitted: a reader must be able to tell
|
|
217
270
|
// "the scan found nothing" from "the scan never ran".
|
|
218
271
|
const antiPatterns = result.antiPatterns
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// One spelling of "a path, with forward slashes".
|
|
2
|
+
//
|
|
3
|
+
// Six modules under lib/ carried this rule inline — `git-ignore`, `docs-drift` (twice),
|
|
4
|
+
// `anti-patterns`, `base-url-advisory` and `ignored-scaffold-check` — and one of the six
|
|
5
|
+
// was spelled differently: `.split("\\").join("/")` rather than `.split(sep).join("/")`.
|
|
6
|
+
// On Windows those agree, because `sep` IS a backslash. On POSIX they do not: `sep` is
|
|
7
|
+
// already "/" so the correct form is a no-op, while splitting on a literal backslash
|
|
8
|
+
// rewrites a filename that legitimately CONTAINS one. That is the drift six copies of a
|
|
9
|
+
// one-line rule invite, and it had already happened.
|
|
10
|
+
//
|
|
11
|
+
// Comparisons against git's output are the reason the rule exists at all: `git check-ignore`
|
|
12
|
+
// and `git ls-files` speak forward slashes on every platform, so a Windows path has to be
|
|
13
|
+
// normalized before it can be looked up in a set built from git's answer.
|
|
14
|
+
|
|
15
|
+
import { relative, sep } from "node:path";
|
|
16
|
+
|
|
17
|
+
/** A path with the platform separator replaced by "/" — a no-op on POSIX. */
|
|
18
|
+
export function toPosix(p: string): string {
|
|
19
|
+
return p.split(sep).join("/");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `abs` relative to `root`, forward-slashed. */
|
|
23
|
+
export function relPosix(root: string, abs: string): string {
|
|
24
|
+
return toPosix(relative(root, abs));
|
|
25
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MigrationApplyError } from "@metaobjectsdev/migrate-ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What to tell someone whose committed chain will not apply to an empty database.
|
|
5
|
+
*
|
|
6
|
+
* The message this replaces prescribed ONE fix — *"fix this with a NEW migration that
|
|
7
|
+
* creates the missing object"* — and it cannot work, in any case. Migrations apply in
|
|
8
|
+
* timestamp order, so anything authored now sorts AFTER the migration that failed; the
|
|
9
|
+
* object is still missing when that file runs. Two estates found it from opposite
|
|
10
|
+
* directions and it is the same defect both times:
|
|
11
|
+
*
|
|
12
|
+
* - a chain whose FIRST migration needs a schema an out-of-band script creates (the third
|
|
13
|
+
* migration emits `CREATE SCHEMA IF NOT EXISTS`, the first predates it). A new migration
|
|
14
|
+
* sorts last, after the one that needs the schema.
|
|
15
|
+
* - a project where **another tool owns schema creation** — drizzle-kit's chain builds the
|
|
16
|
+
* tables, `.metaobjects/migrations/` holds later incremental patches. Following the
|
|
17
|
+
* printed remedy literally means hand-authoring every `CREATE TABLE` into the MetaObjects
|
|
18
|
+
* chain, i.e. duplicating a schema this project does not own, which is the opposite of
|
|
19
|
+
* what the agent context teaches two paragraphs into its own Principles.
|
|
20
|
+
*
|
|
21
|
+
* The old message assumed MetaObjects is the only schema authority and offered no third
|
|
22
|
+
* possibility. These name the fixes that exist, and the head-of-chain case gets the extra
|
|
23
|
+
* sentence that is only true there: nothing in this chain could have created what the
|
|
24
|
+
* first migration needs, so the base schema comes from outside it.
|
|
25
|
+
*
|
|
26
|
+
* "Applied migrations are immutable" is kept, because it is true and it is the reason the
|
|
27
|
+
* obvious fix is not offered first — but it is stated as the CONDITION it actually is
|
|
28
|
+
* (a checksum guard on databases that already ran the file), not as a blanket ban, because
|
|
29
|
+
* a migration no database has applied yet is edited in place every day.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Is this the migrate engine's own apply failure?
|
|
33
|
+
*
|
|
34
|
+
* By NAME, not `instanceof`. Two physical copies of `@metaobjectsdev/migrate-ts` in one
|
|
35
|
+
* process — a globally-installed or linked `meta` alongside a project-local dependency —
|
|
36
|
+
* give the class object and the instance different identities, so `instanceof` returns
|
|
37
|
+
* false for a real error and the remedy silently degrades to its un-positioned form. That
|
|
38
|
+
* is the same class-identity defect that split ts-poet's `Code` in 0.21.6 and drove the
|
|
39
|
+
* exported node guards in `metadata`. Unreachable through today's single import; costs one
|
|
40
|
+
* line to make unreachable by construction.
|
|
41
|
+
*/
|
|
42
|
+
function isApplyError(err: unknown): err is MigrationApplyError {
|
|
43
|
+
return err instanceof MigrationApplyError
|
|
44
|
+
|| (err instanceof Error && err.name === "MigrationApplyError"
|
|
45
|
+
&& typeof (err as MigrationApplyError).migration === "string"
|
|
46
|
+
&& typeof (err as MigrationApplyError).index === "number");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function replayRemedy(err: unknown): string[] {
|
|
50
|
+
const applyErr = isApplyError(err) ? err : undefined;
|
|
51
|
+
const head = applyErr !== undefined && applyErr.index === 0;
|
|
52
|
+
const which =
|
|
53
|
+
applyErr === undefined
|
|
54
|
+
? "the committed chain does not apply to an empty database."
|
|
55
|
+
: `'${applyErr.migration}' (migration ${applyErr.index + 1} of ${applyErr.pendingCount}) ` +
|
|
56
|
+
`does not apply to an empty database.`;
|
|
57
|
+
|
|
58
|
+
const out = [which];
|
|
59
|
+
if (head) {
|
|
60
|
+
out.push(
|
|
61
|
+
"It is the FIRST migration in the chain, so nothing here could have created what it " +
|
|
62
|
+
"needs — this chain has never built a database from empty, and its base schema comes " +
|
|
63
|
+
"from outside it.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
out.push(
|
|
67
|
+
"A NEW migration cannot fix this: migrations apply in timestamp order, so anything you " +
|
|
68
|
+
"author now sorts after the file that failed.",
|
|
69
|
+
);
|
|
70
|
+
out.push(
|
|
71
|
+
"The fixes that do work: if another tool or an out-of-band script owns schema creation " +
|
|
72
|
+
"for this project, --replay asserts a property this chain does not have — don't wire " +
|
|
73
|
+
"it. Otherwise correct the migration in place if no database has applied it yet " +
|
|
74
|
+
"(the ledger's checksum guard is what stops you once one has), or re-baseline from a " +
|
|
75
|
+
"live database with `meta migrate baseline --from-db --db <url>`.",
|
|
76
|
+
);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -155,6 +155,26 @@ const INERT_SLOTS = [DOC_ATTR_SUMMARY] as const;
|
|
|
155
155
|
*/
|
|
156
156
|
const TITLE_IS_AN_ID = /^[A-Z]{2,}[- ]?\d+/;
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* What is LEFT of a title once a leading catalogue id and its separator are removed.
|
|
160
|
+
*
|
|
161
|
+
* The rule fires only when that remainder is empty — when the label IS the id and
|
|
162
|
+
* nothing else. A title that carries an id AND a noun phrase is not "an id"; it is a
|
|
163
|
+
* label with a citation prefix, which is a house style, and the warning's own name is
|
|
164
|
+
* false for it.
|
|
165
|
+
*
|
|
166
|
+
* Measured on a 323-node adopter ledger that uses `@title` deliberately as its bridge
|
|
167
|
+
* to a spec catalogue: the un-narrowed rule fired **203 times**, on every run, telling
|
|
168
|
+
* that author their entire convention was wrong and offering a 203-entry edit. That is
|
|
169
|
+
* the "355 findings, all false" shape the 0.24.2 narrowing exists to avoid, reproduced
|
|
170
|
+
* by the check that narrowing shipped alongside. A lint people argue with is a lint
|
|
171
|
+
* people mute, and muting takes the six useful checks with it.
|
|
172
|
+
*
|
|
173
|
+
* `@title: "REQ-1234"` — a slot holding only a citation, with no label anywhere — is
|
|
174
|
+
* still caught, and that is the case the rule was written for.
|
|
175
|
+
*/
|
|
176
|
+
const TITLE_ID_ONLY = /^[A-Z]{2,}[- ]?\d+[\s\-–—:.]*$/;
|
|
177
|
+
|
|
158
178
|
/** Lowercase, drop everything that is not alphanumeric, collapse the gaps.
|
|
159
179
|
* Two slots "say the same thing" only if they survive this identically — no
|
|
160
180
|
* similarity score, no threshold. A fuzzy match on prose produces findings the
|
|
@@ -343,12 +363,13 @@ export function lintRequirements(
|
|
|
343
363
|
|
|
344
364
|
// -- an id is not a label -------------------------------------------------
|
|
345
365
|
const title = readOwnSlot(node, DOC_ATTR_TITLE);
|
|
346
|
-
if (title !== undefined &&
|
|
366
|
+
if (title !== undefined && TITLE_ID_ONLY.test(title.trim())) {
|
|
347
367
|
out.push(warn(path, WARN_REQUIREMENT_TITLE_IS_AN_ID,
|
|
348
|
-
`@title
|
|
349
|
-
`an id is not a name, so this
|
|
350
|
-
`@${REQUIREMENT_ATTR_TRACKED_BY}, which is the free-form reference slot and IS read, and
|
|
351
|
-
`
|
|
368
|
+
`@title holds only a catalogue or ticket id: ${excerpt(title)}. A title is the entry's ` +
|
|
369
|
+
`LABEL and an id is not a name, so this slot says nothing a reader can use. Put the id in ` +
|
|
370
|
+
`@${REQUIREMENT_ATTR_TRACKED_BY}, which is the free-form reference slot and IS read, and ` +
|
|
371
|
+
`give @${DOC_ATTR_TITLE} a noun phrase. A title that carries an id AND a phrase ` +
|
|
372
|
+
`("FR-448 — prompt construction as typed payloads") is a house style and is NOT flagged.`));
|
|
352
373
|
}
|
|
353
374
|
}
|
|
354
375
|
|