@metaobjectsdev/cli 1.0.0-rc.3 → 1.0.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/src/commands/gen.d.ts +3 -0
  2. package/dist/src/commands/gen.d.ts.map +1 -1
  3. package/dist/src/commands/gen.js +5 -3
  4. package/dist/src/commands/gen.js.map +1 -1
  5. package/dist/src/commands/init.d.ts.map +1 -1
  6. package/dist/src/commands/init.js +237 -29
  7. package/dist/src/commands/init.js.map +1 -1
  8. package/dist/src/commands/migrate.d.ts +1 -0
  9. package/dist/src/commands/migrate.d.ts.map +1 -1
  10. package/dist/src/commands/migrate.js +1 -1
  11. package/dist/src/commands/migrate.js.map +1 -1
  12. package/dist/src/commands/verify.d.ts.map +1 -1
  13. package/dist/src/commands/verify.js +39 -9
  14. package/dist/src/commands/verify.js.map +1 -1
  15. package/dist/src/index.d.ts +2 -0
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +30 -2
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/lib/args.d.ts +228 -0
  20. package/dist/src/lib/args.d.ts.map +1 -1
  21. package/dist/src/lib/args.js +88 -72
  22. package/dist/src/lib/args.js.map +1 -1
  23. package/dist/src/lib/docs-drift.d.ts +12 -1
  24. package/dist/src/lib/docs-drift.d.ts.map +1 -1
  25. package/dist/src/lib/docs-drift.js +71 -11
  26. package/dist/src/lib/docs-drift.js.map +1 -1
  27. package/dist/src/lib/git-ignore.d.ts +40 -0
  28. package/dist/src/lib/git-ignore.d.ts.map +1 -0
  29. package/dist/src/lib/git-ignore.js +77 -0
  30. package/dist/src/lib/git-ignore.js.map +1 -0
  31. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  32. package/dist/src/lib/load-metaobjects-config.js +65 -1
  33. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  34. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  35. package/dist/src/lib/manifest-ignored-check.js +5 -31
  36. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  37. package/dist/src/lib/output.d.ts +10 -1
  38. package/dist/src/lib/output.d.ts.map +1 -1
  39. package/dist/src/lib/output.js +15 -6
  40. package/dist/src/lib/output.js.map +1 -1
  41. package/dist/src/lib/requirement-lint.d.ts.map +1 -1
  42. package/dist/src/lib/requirement-lint.js +25 -5
  43. package/dist/src/lib/requirement-lint.js.map +1 -1
  44. package/package.json +10 -10
  45. package/src/commands/gen.ts +5 -3
  46. package/src/commands/init.ts +247 -33
  47. package/src/commands/migrate.ts +1 -1
  48. package/src/commands/verify.ts +46 -9
  49. package/src/index.ts +30 -2
  50. package/src/lib/args.ts +96 -72
  51. package/src/lib/docs-drift.ts +92 -12
  52. package/src/lib/git-ignore.ts +115 -0
  53. package/src/lib/load-metaobjects-config.ts +58 -1
  54. package/src/lib/manifest-ignored-check.ts +5 -28
  55. package/src/lib/output.ts +30 -5
  56. package/src/lib/requirement-lint.ts +26 -5
@@ -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 { 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,7 +35,10 @@ 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
- if (isGitIgnored(cwd, HASH_MANIFEST_REL) !== true) return;
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;
65
42
 
66
43
  log.warn(
67
44
  `${HASH_MANIFEST_REL} is git-ignored, so it never reaches another machine — ` +
package/src/lib/output.ts CHANGED
@@ -15,7 +15,23 @@ export interface FormatOptions {
15
15
  // gen
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
- export type GenFileStatus = "new" | "merged" | "conflict" | "unchanged" | "refused" | "removed";
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
+ | "removed";
19
35
 
20
36
  export interface GenFileEntry {
21
37
  path: string;
@@ -45,6 +61,7 @@ export interface GenResultShape {
45
61
 
46
62
  const GEN_GLYPHS: Record<GenFileStatus, string> = {
47
63
  new: "✓",
64
+ overwrite: "↻",
48
65
  merged: "↺",
49
66
  conflict: "✗",
50
67
  unchanged: "=",
@@ -54,6 +71,7 @@ const GEN_GLYPHS: Record<GenFileStatus, string> = {
54
71
 
55
72
  const GEN_WORDS: Record<GenFileStatus, string> = {
56
73
  new: "NEW",
74
+ overwrite: "OVERWRITE",
57
75
  merged: "MERGED",
58
76
  conflict: "CONFLICT",
59
77
  unchanged: "UNCHANGED",
@@ -88,10 +106,14 @@ export function formatGenResult(result: GenResultShape, opts: FormatOptions): st
88
106
  acc[f.status] = (acc[f.status] ?? 0) + 1;
89
107
  return acc;
90
108
  },
91
- { new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
109
+ { new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
92
110
  );
93
111
  const parts: string[] = [];
94
- if (counts.new > 0) parts.push(`${counts.new} written`);
112
+ // "written" stays the sum of new + overwrite: that number was always correct and
113
+ // adopters read it. Splitting the SUMMARY as well would change a line nothing was
114
+ // wrong with; the per-file status is the one that was lying.
115
+ const written = counts.new + counts.overwrite;
116
+ if (written > 0) parts.push(`${written} written`);
95
117
  if (counts.merged > 0) parts.push(`${counts.merged} merged`);
96
118
  if (counts.conflict > 0) parts.push(`${counts.conflict} conflict`);
97
119
  if (counts.unchanged > 0) parts.push(`${counts.unchanged} unchanged`);
@@ -198,10 +220,13 @@ export function genResultToData(result: GenResultShape): {
198
220
  } {
199
221
  const counts = result.files.reduce<Record<GenFileStatus, number>>(
200
222
  (a, f) => ((a[f.status] = (a[f.status] ?? 0) + 1), a),
201
- { new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
223
+ { new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
202
224
  );
203
225
  const parts: string[] = [];
204
- if (counts.new) parts.push(`${counts.new} written`);
226
+ // Same split as the text formatter: `written` is new + overwrite, and the
227
+ // per-file rows carry which one each was.
228
+ const written = counts.new + counts.overwrite;
229
+ if (written) parts.push(`${written} written`);
205
230
  if (counts.merged) parts.push(`${counts.merged} merged`);
206
231
  if (counts.conflict) parts.push(`${counts.conflict} conflict`);
207
232
  if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
@@ -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 && TITLE_IS_AN_ID.test(title.trim())) {
366
+ if (title !== undefined && TITLE_ID_ONLY.test(title.trim())) {
347
367
  out.push(warn(path, WARN_REQUIREMENT_TITLE_IS_AN_ID,
348
- `@title opens with a catalogue or ticket id: ${excerpt(title)}. A title is a NOUN PHRASE and ` +
349
- `an id is not a name, so this is two things in one slot. SPLIT them — put the id in ` +
350
- `@${REQUIREMENT_ATTR_TRACKED_BY}, which is the free-form reference slot and IS read, and leave ` +
351
- `the phrase as the title. Moving the whole string would throw the label away.`));
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