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

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 (121) hide show
  1. package/dist/src/commands/docs.d.ts.map +1 -1
  2. package/dist/src/commands/docs.js +41 -9
  3. package/dist/src/commands/docs.js.map +1 -1
  4. package/dist/src/commands/gen.d.ts +5 -0
  5. package/dist/src/commands/gen.d.ts.map +1 -1
  6. package/dist/src/commands/gen.js +12 -1
  7. package/dist/src/commands/gen.js.map +1 -1
  8. package/dist/src/commands/init.d.ts.map +1 -1
  9. package/dist/src/commands/init.js +27 -3
  10. package/dist/src/commands/init.js.map +1 -1
  11. package/dist/src/commands/migrate.d.ts +5 -0
  12. package/dist/src/commands/migrate.d.ts.map +1 -1
  13. package/dist/src/commands/migrate.js +10 -4
  14. package/dist/src/commands/migrate.js.map +1 -1
  15. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
  16. package/dist/src/commands/prompt-snapshot.js +2 -1
  17. package/dist/src/commands/prompt-snapshot.js.map +1 -1
  18. package/dist/src/commands/types.d.ts.map +1 -1
  19. package/dist/src/commands/types.js +12 -3
  20. package/dist/src/commands/types.js.map +1 -1
  21. package/dist/src/commands/upgrade.d.ts.map +1 -1
  22. package/dist/src/commands/upgrade.js +62 -4
  23. package/dist/src/commands/upgrade.js.map +1 -1
  24. package/dist/src/commands/verify.d.ts.map +1 -1
  25. package/dist/src/commands/verify.js +69 -21
  26. package/dist/src/commands/verify.js.map +1 -1
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +37 -18
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/src/lib/advisory.d.ts +6 -0
  31. package/dist/src/lib/advisory.d.ts.map +1 -1
  32. package/dist/src/lib/advisory.js +13 -0
  33. package/dist/src/lib/advisory.js.map +1 -1
  34. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  35. package/dist/src/lib/anti-patterns.js +8 -4
  36. package/dist/src/lib/anti-patterns.js.map +1 -1
  37. package/dist/src/lib/args.d.ts +23 -12
  38. package/dist/src/lib/args.d.ts.map +1 -1
  39. package/dist/src/lib/args.js +33 -22
  40. package/dist/src/lib/args.js.map +1 -1
  41. package/dist/src/lib/authored-source.d.ts +22 -0
  42. package/dist/src/lib/authored-source.d.ts.map +1 -0
  43. package/dist/src/lib/authored-source.js +46 -0
  44. package/dist/src/lib/authored-source.js.map +1 -0
  45. package/dist/src/lib/base-url-advisory.d.ts +37 -0
  46. package/dist/src/lib/base-url-advisory.d.ts.map +1 -0
  47. package/dist/src/lib/base-url-advisory.js +168 -0
  48. package/dist/src/lib/base-url-advisory.js.map +1 -0
  49. package/dist/src/lib/codegen-drift.d.ts +8 -0
  50. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  51. package/dist/src/lib/codegen-drift.js +43 -10
  52. package/dist/src/lib/codegen-drift.js.map +1 -1
  53. package/dist/src/lib/detect-stack.d.ts +17 -0
  54. package/dist/src/lib/detect-stack.d.ts.map +1 -1
  55. package/dist/src/lib/detect-stack.js +64 -2
  56. package/dist/src/lib/detect-stack.js.map +1 -1
  57. package/dist/src/lib/docs-drift.d.ts.map +1 -1
  58. package/dist/src/lib/docs-drift.js +16 -7
  59. package/dist/src/lib/docs-drift.js.map +1 -1
  60. package/dist/src/lib/git-ignore.d.ts +19 -0
  61. package/dist/src/lib/git-ignore.d.ts.map +1 -1
  62. package/dist/src/lib/git-ignore.js +46 -3
  63. package/dist/src/lib/git-ignore.js.map +1 -1
  64. package/dist/src/lib/ignored-scaffold-check.d.ts +12 -0
  65. package/dist/src/lib/ignored-scaffold-check.d.ts.map +1 -0
  66. package/dist/src/lib/ignored-scaffold-check.js +75 -0
  67. package/dist/src/lib/ignored-scaffold-check.js.map +1 -0
  68. package/dist/src/lib/load-error.d.ts +46 -0
  69. package/dist/src/lib/load-error.d.ts.map +1 -0
  70. package/dist/src/lib/load-error.js +51 -0
  71. package/dist/src/lib/load-error.js.map +1 -0
  72. package/dist/src/lib/load-metaobjects-config.d.ts +13 -0
  73. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  74. package/dist/src/lib/load-metaobjects-config.js +54 -5
  75. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  76. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  77. package/dist/src/lib/manifest-ignored-check.js +19 -6
  78. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  79. package/dist/src/lib/output.d.ts +13 -1
  80. package/dist/src/lib/output.d.ts.map +1 -1
  81. package/dist/src/lib/output.js +26 -8
  82. package/dist/src/lib/output.js.map +1 -1
  83. package/dist/src/lib/rel-posix.d.ts +5 -0
  84. package/dist/src/lib/rel-posix.d.ts.map +1 -0
  85. package/dist/src/lib/rel-posix.js +23 -0
  86. package/dist/src/lib/rel-posix.js.map +1 -0
  87. package/dist/src/lib/replay-remedy.d.ts +2 -0
  88. package/dist/src/lib/replay-remedy.d.ts.map +1 -0
  89. package/dist/src/lib/replay-remedy.js +69 -0
  90. package/dist/src/lib/replay-remedy.js.map +1 -0
  91. package/dist/src/lib/wrangler.d.ts +17 -2
  92. package/dist/src/lib/wrangler.d.ts.map +1 -1
  93. package/dist/src/lib/wrangler.js +51 -2
  94. package/dist/src/lib/wrangler.js.map +1 -1
  95. package/package.json +10 -10
  96. package/src/commands/docs.ts +51 -12
  97. package/src/commands/gen.ts +14 -1
  98. package/src/commands/init.ts +27 -3
  99. package/src/commands/migrate.ts +12 -4
  100. package/src/commands/prompt-snapshot.ts +2 -1
  101. package/src/commands/types.ts +12 -3
  102. package/src/commands/upgrade.ts +69 -4
  103. package/src/commands/verify.ts +74 -23
  104. package/src/index.ts +37 -18
  105. package/src/lib/advisory.ts +15 -0
  106. package/src/lib/anti-patterns.ts +7 -5
  107. package/src/lib/args.ts +60 -39
  108. package/src/lib/authored-source.ts +45 -0
  109. package/src/lib/base-url-advisory.ts +178 -0
  110. package/src/lib/codegen-drift.ts +52 -9
  111. package/src/lib/detect-stack.ts +73 -2
  112. package/src/lib/docs-drift.ts +17 -7
  113. package/src/lib/git-ignore.ts +47 -3
  114. package/src/lib/ignored-scaffold-check.ts +78 -0
  115. package/src/lib/load-error.ts +89 -0
  116. package/src/lib/load-metaobjects-config.ts +53 -5
  117. package/src/lib/manifest-ignored-check.ts +21 -6
  118. package/src/lib/output.ts +36 -8
  119. package/src/lib/rel-posix.ts +25 -0
  120. package/src/lib/replay-remedy.ts +78 -0
  121. package/src/lib/wrangler.ts +52 -4
@@ -12,12 +12,14 @@ import { parseVerifyArgs, type MigrateFlags } from "../lib/args.js";
12
12
  import { log } from "../lib/log.js";
13
13
  import { emitStructured, type OutputFormat } from "../lib/format.js";
14
14
  import {
15
- antiPatternRows, ranSection, skippedSection, warnCapped,
15
+ antiPatternRows, missingBaseUrlRows, ranSection, skippedSection, warnCapped,
16
16
  type AdvisoryDiagnosticRow, type AdvisoryFindingRow, type AdvisorySection,
17
17
  } from "../lib/advisory.js";
18
18
  import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
19
19
  import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
20
20
  import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
21
+ import { scanForMissingBaseUrl, type BaseUrlFinding } from "../lib/base-url-advisory.js";
22
+ import { replayRemedy } from "../lib/replay-remedy.js";
21
23
  import { FileProvider } from "../lib/file-provider.js";
22
24
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
23
25
  import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
@@ -85,6 +87,7 @@ import {
85
87
  REQUIREMENT_STATUSES,
86
88
  } from "@metaobjectsdev/metadata";
87
89
  import { verify, ERR_REQUIRED_SLOT_UNUSED, ERR_PARTIAL_UNRESOLVED } from "@metaobjectsdev/render";
90
+ import { describeLoadError, reportLoadError } from "../lib/load-error.js";
88
91
 
89
92
  const DEFAULT_PROMPTS_DIR = "prompts";
90
93
 
@@ -250,7 +253,10 @@ export async function verifyCommand(
250
253
  });
251
254
  } catch (err) {
252
255
  const msg = (err as Error).message;
253
- log.error(`failed to load metadata: ${msg}`);
256
+ // Everything the loader's envelope carried, not just the message: the stable code a
257
+ // CI job keys on, and the FILE + json path the message itself can never name.
258
+ const report = describeLoadError(err);
259
+ log.error(`failed to load metadata: ${report.text}`);
254
260
  // Strict-load rejection (ADR-0023). Two different failures reach here and they need
255
261
  // different advice:
256
262
  //
@@ -263,8 +269,11 @@ export async function verifyCommand(
263
269
  // retired attribute and the author gets a green `meta verify` over a value that now
264
270
  // reaches nothing: a correct, loud failure converted into a quiet, wrong pass. That is
265
271
  // strictly worse than the error it replaced.
266
- const code = (err as { code?: string }).code;
267
- const suggestions = (err as { suggestions?: string[] }).suggestions;
272
+ // Off the REPORT, not the raw error a second time: `describeLoadError` has already
273
+ // decided what the envelope carried (and filtered empty entries), so re-reading the
274
+ // throwable would be a second answer to a question already answered above.
275
+ const code = report.code;
276
+ const suggestions = report.suggestions;
268
277
  const strictHint =
269
278
  "meta verify is strict (ADR-0023): every authored @attr must be declared. " +
270
279
  "Fix: register the attr on a metadata provider, OR move arbitrary " +
@@ -280,6 +289,14 @@ export async function verifyCommand(
280
289
  // report — only why there is none.
281
290
  emitStructured({
282
291
  error: `failed to load metadata: ${msg}`,
292
+ // ADR-0009 promises a stable `code`, and the docs name codes a CI job is meant to
293
+ // branch on — but this payload used to carry the message string and nothing else,
294
+ // so there was nothing to branch on. Each field is present only when the loader
295
+ // actually supplied it; absence here means the loader had none, never that it was
296
+ // dropped in transit.
297
+ ...(report.code !== undefined ? { code: report.code } : {}),
298
+ ...(report.files !== undefined ? { files: [...report.files] } : {}),
299
+ ...(report.jsonPath !== undefined ? { jsonPath: report.jsonPath } : {}),
283
300
  hint: suggestions !== undefined && suggestions.length > 0
284
301
  ? suggestions.join(" ")
285
302
  : isStrictAttr ? strictHint : "fix the metadata error above and re-run",
@@ -449,11 +466,7 @@ export async function verifyCommand(
449
466
  applied = await applyPending(engine.db, dir, { dryRun: false, dialect });
450
467
  } catch (err) {
451
468
  log.error(`meta verify --replay: ${(err as Error).message}`);
452
- log.error(
453
- `meta verify --replay: the committed chain does not apply to an empty database. ` +
454
- `Applied migrations are immutable, so fix this with a NEW migration that creates the ` +
455
- `missing object — not by editing a committed up.sql.`,
456
- );
469
+ for (const line of replayRemedy(err)) log.error(`meta verify --replay: ${line}`);
457
470
  return 1;
458
471
  }
459
472
 
@@ -711,13 +724,33 @@ export async function verifyCommand(
711
724
  antiPatternSection = skippedSection(`the scan failed: ${(err as Error).message}`);
712
725
  return;
713
726
  }
714
- antiPatternSection = ranSection(antiPatternRows(findings));
715
- if (findings.length === 0) return;
716
- log.warn(
717
- `meta verify — ${findings.length} place(s) hand-roll what MetaObjects can model ` +
718
- `(advisory — does not fail the build):`,
719
- );
720
- warnCapped(findings.map((f) => ` ${f.message}`), flags.limit, { structured });
727
+ // F52 — the one gate that can see a provider silently missing its base. Folded into
728
+ // this pass rather than given its own, because it is the same KIND of finding (a
729
+ // teachable warning that never touches the exit code) and one --no-antipatterns
730
+ // should suppress the whole advisory tier, not half of it.
731
+ let baseUrl: BaseUrlFinding[] = [];
732
+ try {
733
+ baseUrl = scanForMissingBaseUrl(projectRoot, forgeConfig?.apiPrefix ?? "");
734
+ } catch {
735
+ // Same discipline as above: an advisory scan never breaks verify. The anti-pattern
736
+ // half already ran, so its rows are still reported rather than lost to this catch.
737
+ }
738
+
739
+ antiPatternSection = ranSection([...antiPatternRows(findings), ...missingBaseUrlRows(baseUrl)]);
740
+ if (findings.length > 0) {
741
+ log.warn(
742
+ `meta verify — ${findings.length} place(s) hand-roll what MetaObjects can model ` +
743
+ `(advisory — does not fail the build):`,
744
+ );
745
+ warnCapped(findings.map((f) => ` ${f.message}`), flags.limit, { structured });
746
+ }
747
+ if (baseUrl.length > 0) {
748
+ log.warn(
749
+ `meta verify — ${baseUrl.length} entity-fetcher provider(s) mounted with no baseUrl ` +
750
+ `while apiPrefix is "${forgeConfig?.apiPrefix ?? ""}" (advisory — does not fail the build):`,
751
+ );
752
+ warnCapped(baseUrl.map((f) => ` ${f.message}`), flags.limit, { structured });
753
+ }
721
754
  }
722
755
 
723
756
  // -- template (prompt / output) drift --------------------------------------
@@ -1200,11 +1233,11 @@ export async function verifyCommand(
1200
1233
  strict: !flags.lax,
1201
1234
  });
1202
1235
  } catch (err) {
1203
- log.error(`verify --codegen: failed to load this package's metadata: ${(err as Error).message}`);
1204
- // The second door onto the same strict load. It carried no remedy at all, which
1205
- // meant a retirement diagnosed here told the author what broke and nothing about
1206
- // how to fix it — the half-true rule this file's sibling comment warns about.
1207
- for (const s of (err as { suggestions?: string[] }).suggestions ?? []) log.error(` ${s}`);
1236
+ // The second door onto the same strict load, and it must carry the same provenance
1237
+ // as the first: the ADR-0009 code, the FILE and json path the message cannot name,
1238
+ // and the loader's own remedies. It printed a bare message plus a hand-rolled
1239
+ // suggestions read — the half-true rule this file's sibling comment warns about.
1240
+ reportLoadError(log, "verify --codegen: failed to load this package's metadata", err);
1208
1241
  return 2;
1209
1242
  }
1210
1243
  }
@@ -1223,7 +1256,11 @@ export async function verifyCommand(
1223
1256
  }
1224
1257
 
1225
1258
  if (result.clean) {
1226
- say("meta verify — generated output is in sync with the metadata (no codegen drift).");
1259
+ // Says what it CHECKED, not more. Since 0.24.3 this gate asks whether each file's
1260
+ // GENERATED contribution is current — a hand edit `meta gen` preserves is exempt —
1261
+ // so "output is in sync with the metadata" claimed a stronger property than it had
1262
+ // verified, and did so over a file whose committed content contradicted the model.
1263
+ say("meta verify — every file's generated contribution is current (no codegen drift).");
1227
1264
  return 0;
1228
1265
  }
1229
1266
 
@@ -1231,7 +1268,21 @@ export async function verifyCommand(
1231
1268
  `meta verify — codegen drift (${result.driftedFiles.length} file(s) differ from a fresh regen):`,
1232
1269
  );
1233
1270
  for (const line of result.lines) log.error(` ${line}`);
1234
- log.error("Run 'meta gen' to regenerate, then commit the result.");
1271
+ // The remedy is PER CONVICTION KIND, not one line for the whole gate. `meta gen`
1272
+ // fixes stale generated output; it cannot fix a hand-edited names artifact, because
1273
+ // it MERGES the edit and exits 0 — so prescribing it there was the unbreakable loop
1274
+ // the hand-edit exemption exists to end, reintroduced for the one file the exemption
1275
+ // deliberately excludes. Both lines print when the drift set holds both kinds.
1276
+ if (result.driftedFiles.length > result.handEditedNames.length) {
1277
+ log.error("Run 'meta gen' to regenerate, then commit the result.");
1278
+ }
1279
+ if (result.handEditedNames.length > 0) {
1280
+ log.error(
1281
+ "For the names artifact(s) above, 'meta gen' will NOT help — it preserves a hand " +
1282
+ "edit by design. Revert the edit (or delete the file and re-run 'meta gen' to " +
1283
+ "re-emit it), and keep constants of your own in a file of your own.",
1284
+ );
1285
+ }
1235
1286
  return 1;
1236
1287
  }
1237
1288
 
package/src/index.ts CHANGED
@@ -57,10 +57,12 @@ GEN FLAGS:
57
57
  --dry-run Compute and print, don't write
58
58
  --list Print the generator registry (name, tier, options) and exit —
59
59
  no config or metadata required
60
- --baseline <default|fresh>
61
- First-time-on-existing-file behavior. Default: write-if-different,
62
- adopting existing content as the canonical baseline. "fresh"
63
- overwrites and re-baselines.
60
+ --baseline <default|adopt|fresh>
61
+ First-time-on-existing-file behavior. Default: refuse a file that
62
+ cannot be proved to be generated output. "adopt" records the files
63
+ you have as the merge base and writes NOTHING — the first run a
64
+ project with no committed .hashes.json can perform. "fresh"
65
+ overwrites them with fresh output and re-baselines (discards edits).
64
66
  --no-antipatterns Suppress the advisory "hand-rolled what MetaObjects can model" pass
65
67
  --limit <n|all> How many advisory lines TEXT output prints before it truncates
66
68
  (default 20). Never applies to --format toon/json, which
@@ -75,7 +77,11 @@ DOCS FLAGS:
75
77
  [<project-root>] PROJECT ROOT to resolve metadata from — the directory that CONTAINS
76
78
  your metadata, NOT the metadata directory. Passing it SCOPES the run
77
79
  (default: cwd)
78
- --out <dir>, -o Output directory for the pages (default: ./docs)
80
+ --out <dir>, -o Output directory for THIS run (default: ./docs/generated). It does
81
+ NOT survive the run — set 'docs: { outDir }' in metaobjects.config.ts
82
+ for a directory that persists, which is what 'meta verify --docs'
83
+ measures. The default is a sub-directory on purpose: these pages are
84
+ regenerated and overwritten, and 'docs/' itself is usually yours.
79
85
  --templates <dir> Project root to resolve adopter templates/ overrides (default: <project-root>)
80
86
  --prompts <dir> Extra dir holding prompt .mustache sources for --site (e.g. data/templates/)
81
87
 
@@ -143,10 +149,12 @@ FLAGS:
143
149
  --dry-run Compute and print, don't write
144
150
  --list Print the generator registry (name, tier, options) and exit —
145
151
  no config or metadata required
146
- --baseline <default|fresh>
147
- First-time-on-existing-file behavior. Default: write-if-different,
148
- adopting existing content as the canonical baseline. "fresh"
149
- overwrites and re-baselines.
152
+ --baseline <default|adopt|fresh>
153
+ First-time-on-existing-file behavior. Default: refuse a file that
154
+ cannot be proved to be generated output. "adopt" records the files
155
+ you have as the merge base and writes NOTHING — the first run a
156
+ project with no committed .hashes.json can perform. "fresh"
157
+ overwrites them with fresh output and re-baselines (discards edits).
150
158
  --no-antipatterns Suppress the advisory "hand-rolled what MetaObjects can model" pass
151
159
  --limit <n|all> Advisory lines TEXT output prints before truncating (default 20)
152
160
  --format <toon|json|text> Output format (global flag; default toon off-TTY)
@@ -191,7 +199,10 @@ USAGE:
191
199
 
192
200
  FLAGS:
193
201
  --templates Template/prompt {{field}}↔payload drift (default when bare)
194
- --codegen Codegen drift — regenerate to temp dir and diff committed output
202
+ --codegen Codegen drift — is the GENERATED contribution current? A hand
203
+ edit 'meta gen' preserves is not drift; a stale generated part is.
204
+ An <Entity>.names.ts artifact is the exception: it must match a
205
+ fresh regen exactly — a hand edit in it is itself the defect.
195
206
  Needs metaobjects.config.ts; exit 2 if absent.
196
207
  --docs Docs drift — run 'meta docs' to a temp dir and diff docs.outDir
197
208
  Needs metaobjects.config.ts; exit 2 if absent. A page the
@@ -269,7 +280,11 @@ FLAGS:
269
280
  it SCOPES the run to that directory's own sources; no ancestor
270
281
  .metaobjects/config.json is consulted. Omitted (default), the project
271
282
  is discovered by walking up.
272
- --out <dir>, -o Output directory for the pages (default: ./docs)
283
+ --out <dir>, -o Output directory for THIS run (default: ./docs/generated). It does
284
+ NOT survive the run — set 'docs: { outDir }' in metaobjects.config.ts
285
+ for a directory that persists, which is what 'meta verify --docs'
286
+ measures. The default is a sub-directory on purpose: these pages are
287
+ regenerated and overwritten, and 'docs/' itself is usually yours.
273
288
  --model Emit the markdown model surface (entity + template pages)
274
289
  --api Emit the markdown api surface (generated SDK reference)
275
290
  --requirements Emit the declared requirement ledger (requirements.md + .toon).
@@ -278,7 +293,9 @@ FLAGS:
278
293
  agent/ui.md (the generated forms and grids) and
279
294
  agent/requirements.md (the ledger plus a claimed-node index).
280
295
  Needs metaobjects.config.ts (the dialect and physical names come
281
- from it); each page is skipped when its tier has nothing to describe.
296
+ from it); each page is skipped when its tier has nothing to describe
297
+ — agent/ui.md needs a UI generator (a form, hook or grid) in the
298
+ suite, not merely objects that could have one.
282
299
  --metamodel Document the built-in metamodel vocabulary (no metadata needed)
283
300
  --site Generate the browsable HTML documentation site (<out>/site/)
284
301
  --scaffold-site Copy the site's templates + assets into codegen/docs-site/ to own (theme) them
@@ -297,10 +314,12 @@ FLAGS:
297
314
  --docs-only Write only the agent-context files — no metadata sources, no
298
315
  codegen scaffold, no config. Pair with --refresh-docs to update
299
316
  an existing project after upgrading.
300
- --server <lang> Declare a server language for the agent context (repeatable;
301
- e.g. csharp, kotlin, python, node)
302
- --client <fw> Declare a client framework for the agent context (repeatable;
303
- e.g. react, vue)
317
+ --server <lang> Declare a server language for the agent context (repeatable):
318
+ typescript | java | kotlin | csharp | python
319
+ ('node' is accepted as an alias for typescript; an unknown
320
+ value is refused, never dropped)
321
+ --client <fw> Declare a client framework for the agent context (repeatable):
322
+ react | tanstack | angular
304
323
  --no-skills Skip the .claude/skills/ scaffold
305
324
  --force Overwrite existing files
306
325
  --quiet Suppress output
@@ -317,8 +336,8 @@ USAGE:
317
336
  meta agent-docs [--server <lang>]... [--client <fw>]... [--out <dir>] [flags]
318
337
 
319
338
  FLAGS:
320
- --server <lang> Server language (repeatable; e.g. csharp, kotlin, python, node)
321
- --client <fw> Client framework (repeatable; e.g. react, vue)
339
+ --server <lang> Server language (repeatable): typescript|java|kotlin|csharp|python
340
+ --client <fw> Client framework (repeatable): react|tanstack|angular
322
341
  --out <dir> Output directory (default: current directory)
323
342
  --no-skills Skip .claude/skills/ scaffold
324
343
  --no-wire-root Skip wiring root CLAUDE.md @import
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { log } from "./log.js";
24
24
  import type { AntiPatternFinding } from "./anti-patterns.js";
25
+ import type { BaseUrlFinding } from "./base-url-advisory.js";
25
26
 
26
27
  /**
27
28
  * How many advisory lines TEXT output prints before it truncates.
@@ -119,6 +120,20 @@ export function antiPatternRows(findings: readonly AntiPatternFinding[]): Adviso
119
120
  }));
120
121
  }
121
122
 
123
+ /**
124
+ * F52 findings in the same row shape, so the structured payload has ONE advisory list
125
+ * rather than a second one a consumer has to learn about. `rule` is the discriminator.
126
+ */
127
+ export function missingBaseUrlRows(findings: readonly BaseUrlFinding[]): AdvisoryFindingRow[] {
128
+ return findings.map((f) => ({
129
+ file: f.file,
130
+ line: f.line,
131
+ rule: "missing-base-url",
132
+ construct: f.construct,
133
+ message: f.message,
134
+ }));
135
+ }
136
+
122
137
  // ---------------------------------------------------------------------------
123
138
  // text output
124
139
  // ---------------------------------------------------------------------------
@@ -16,7 +16,9 @@
16
16
  // nag a legitimate one.
17
17
 
18
18
  import { readdirSync, readFileSync, statSync } from "node:fs";
19
- import { join, relative, sep } from "node:path";
19
+ import { join } from "node:path";
20
+ import { relPosix } from "./rel-posix.js";
21
+ import { looksBundled } from "./authored-source.js";
20
22
 
21
23
  export interface AntiPatternFinding {
22
24
  file: string; // path relative to the scan root (posix-ish, sep-normalized)
@@ -220,10 +222,6 @@ function findingFor(file: string, lineNo: number, raw: string, isSql: boolean):
220
222
  };
221
223
  }
222
224
 
223
- function relPosix(root: string, abs: string): string {
224
- return relative(root, abs).split(sep).join("/");
225
- }
226
-
227
225
  function walk(dir: string, root: string, ignore: readonly RegExp[], acc: AntiPatternFinding[]): void {
228
226
  let entries;
229
227
  try {
@@ -262,6 +260,10 @@ function walk(dir: string, root: string, ignore: readonly RegExp[], acc: AntiPat
262
260
  }
263
261
  // Skip MetaObjects-generated output — it legitimately contains AVG/CHECK etc.
264
262
  if (text.slice(0, 600).includes(GENERATED_MARKER)) continue;
263
+ // …and skip a BUNDLE, which is nobody's source. The 512KB gate above is about cost, not
264
+ // authorship: a 300KB minified file passes it while being just as unfixable. Same rule
265
+ // the base-URL advisory applies, from the same module. See lib/authored-source.ts.
266
+ if (looksBundled(text)) continue;
265
267
  const isSql = e.name.endsWith(".sql");
266
268
  const lines = text.split("\n");
267
269
  for (let i = 0; i < lines.length; i++) {
package/src/lib/args.ts CHANGED
@@ -100,10 +100,10 @@ export function parseAgentDocsArgs(argv: string[]): AgentDocsFlags {
100
100
  export interface GenFlags {
101
101
  dryRun: boolean;
102
102
  entities: string[];
103
- /** First-time-on-existing-file behavior. Default: write-if-different
104
- * (existing content becomes the canonical baseline). "fresh" → overwrite
105
- * and re-baseline. */
106
- baseline: "default" | "fresh";
103
+ /** First-time-on-existing-file behavior. Default: refuse a file that cannot be
104
+ * proved to be ours. "adopt" → record the file as the merge base and write
105
+ * nothing. "fresh" → overwrite from fresh output and re-baseline. */
106
+ baseline: "default" | "fresh" | "adopt";
107
107
  /** ADR-0021 D3 — print the stable-name generator registry and exit without
108
108
  * running codegen. */
109
109
  list: boolean;
@@ -134,15 +134,20 @@ export function parseGenArgs(argv: string[]): GenFlags {
134
134
  allowPositionals: true,
135
135
  });
136
136
  const baselineRaw = values.baseline as string | undefined;
137
- if (baselineRaw !== undefined && baselineRaw !== "default" && baselineRaw !== "fresh") {
137
+ if (
138
+ baselineRaw !== undefined &&
139
+ baselineRaw !== "default" &&
140
+ baselineRaw !== "fresh" &&
141
+ baselineRaw !== "adopt"
142
+ ) {
138
143
  throw new Error(
139
- `invalid --baseline '${baselineRaw}'; expected 'default' or 'fresh'`,
144
+ `invalid --baseline '${baselineRaw}'; expected 'default', 'adopt' or 'fresh'`,
140
145
  );
141
146
  }
142
147
  return {
143
148
  dryRun: !!values["dry-run"],
144
149
  entities: positionals,
145
- baseline: (baselineRaw as "default" | "fresh" | undefined) ?? "default",
150
+ baseline: (baselineRaw as "default" | "fresh" | "adopt" | undefined) ?? "default",
146
151
  list: !!values.list,
147
152
  noAntipatterns: !!values["no-antipatterns"],
148
153
  // Throws on a bad value; the command layer reports it and exits 2, exactly as
@@ -263,7 +268,7 @@ export interface VerifyFlags {
263
268
  // command layer applies the bare-verify default (= --templates) when none are.
264
269
  /** Run the template/prompt {{field}}↔payload drift gate. */
265
270
  templates: boolean;
266
- /** Run the codegen-drift gate (regenerate-to-temp and diff committed output). */
271
+ /** Run the codegen-drift gate: is the GENERATED contribution of each file current? */
267
272
  codegen: boolean;
268
273
  /**
269
274
  * Run the docs-drift gate: regenerate the `meta docs` surfaces into a temp dir and diff
@@ -272,8 +277,9 @@ export interface VerifyFlags {
272
277
  * A SEPARATE subverb rather than part of `--codegen`, because they check different
273
278
  * trees under different ownership rules: `--codegen` regenerates `outDir`/`targets` and
274
279
  * must respect the hand edits `meta gen` preserves, while the docs tree has no merge,
275
- * no manifest, and lives in a directory (`./docs` by default) full of hand-written
276
- * files it does not own. Folding them together would have to pick one of those rules
280
+ * no manifest, and lives in a directory the adopter chose (`./docs/generated` by
281
+ * default) that may hold hand-written files it does not own. Folding them together
282
+ * would have to pick one of those rules
277
283
  * for both.
278
284
  */
279
285
  docs: boolean;
@@ -309,11 +315,19 @@ export interface VerifyFlags {
309
315
  */
310
316
  lax: boolean;
311
317
  /**
312
- * How many advisory lines TEXT output prints per SECTION before truncating
313
- * (`DEFAULT_ADVISORY_LIMIT`, or Infinity for `--limit all`). Per-section, never
314
- * shared across them — the reason the two caps were separate in the first place
315
- * is that one shared budget lets the authoring lint push every gate warning off
316
- * the end. It never applies to a structured payload, which carries everything.
318
+ * How many advisory lines TEXT output prints per printed LIST before truncating
319
+ * (`DEFAULT_ADVISORY_LIMIT`, or Infinity for `--limit all`). Never shared across
320
+ * lists — one shared budget lets the biggest list push every other finding off the
321
+ * end. It never applies to a structured payload, which carries everything.
322
+ *
323
+ * Per LIST, not per section, and the distinction is real rather than pedantic: the
324
+ * anti-pattern tier prints TWO separately-headed lists (the hand-rolling findings and
325
+ * the F52 missing-`baseUrl` findings) while being ONE section everywhere else — one
326
+ * structured list keyed by `rule`, one `--no-antipatterns` muting both. So that
327
+ * section's text output can reach a MULTIPLE of the cap. Deliberate: a project with 200
328
+ * money-float sites would otherwise never see its `baseUrl` advisory, which is the one
329
+ * that silently drops the API prefix from every generated hook, and starving the small
330
+ * list is the worse failure. Pinned in advisory-structured-output.test.ts.
317
331
  */
318
332
  limit: number;
319
333
  }
@@ -323,7 +337,7 @@ export const VERIFY_OPTIONS = {
323
337
  prompts: { type: "string" },
324
338
  db: { type: "string" },
325
339
  dialect: { type: "string" },
326
- allow: { type: "string" },
340
+ allow: { type: "string", multiple: true },
327
341
  "skip-schema": { type: "boolean", default: false },
328
342
  templates: { type: "boolean", default: false },
329
343
  codegen: { type: "boolean", default: false },
@@ -338,6 +352,33 @@ export const VERIFY_OPTIONS = {
338
352
  "limit": { type: "string" },
339
353
  } as const;
340
354
 
355
+ /**
356
+ * The `--allow` tokens for one invocation, from EVERY occurrence of the flag.
357
+ *
358
+ * `--allow` is comma-separated, and repeating it is the natural reading (it is what
359
+ * `--server` does). It used to take the LAST occurrence only, so
360
+ * `--allow drop-fk --allow drop-view` silently discarded `drop-fk` and then printed
361
+ * "re-run with --allow drop-fk to apply" seven times — an instruction the user had just
362
+ * followed. Both spellings, and any mixture of them, now mean the union.
363
+ *
364
+ * Duplicates collapse: asking for the same permission twice is one permission.
365
+ */
366
+ function parseAllowTokens(raw: string | string[] | undefined): AllowToken[] {
367
+ const occurrences = raw === undefined ? [] : Array.isArray(raw) ? raw : [raw];
368
+ const seen = new Set<string>();
369
+ for (const occurrence of occurrences) {
370
+ for (const tok of occurrence.split(",").map((t) => t.trim()).filter((t) => t.length > 0)) {
371
+ if (!ALLOW_TOKENS.includes(tok as AllowToken)) {
372
+ throw new Error(
373
+ `invalid --allow token '${tok}'; expected one of: ${ALLOW_TOKENS.join(", ")}`,
374
+ );
375
+ }
376
+ seen.add(tok);
377
+ }
378
+ }
379
+ return [...seen] as AllowToken[];
380
+ }
381
+
341
382
  export function parseVerifyArgs(argv: string[]): VerifyFlags {
342
383
  const { values } = parseArgs({
343
384
  args: argv,
@@ -351,17 +392,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
351
392
  throw new Error(`invalid --dialect '${dialect}'; expected: ${DIALECTS.join(", ")}`);
352
393
  }
353
394
 
354
- const allowRaw = (values.allow as string | undefined) ?? "";
355
- const allowTokens = allowRaw.length === 0
356
- ? []
357
- : allowRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
358
- for (const tok of allowTokens) {
359
- if (!ALLOW_TOKENS.includes(tok as AllowToken)) {
360
- throw new Error(
361
- `invalid --allow token '${tok}'; expected one of: ${ALLOW_TOKENS.join(", ")}`,
362
- );
363
- }
364
- }
395
+ const allowTokens = parseAllowTokens(values.allow as string | string[] | undefined);
365
396
 
366
397
  const templates = !!values.templates;
367
398
  const codegen = !!values.codegen;
@@ -472,7 +503,7 @@ export const MIGRATE_OPTIONS = {
472
503
  "migration-format": { type: "string" },
473
504
  "out-dir": { type: "string" },
474
505
  "slug": { type: "string" },
475
- "allow": { type: "string" },
506
+ "allow": { type: "string", multiple: true },
476
507
  "on-ambiguous": { type: "string" },
477
508
  "dry-run": { type: "boolean", default: false },
478
509
  "from-db": { type: "boolean", default: false },
@@ -518,17 +549,7 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
518
549
  throw new Error(`invalid --on-ambiguous '${onAmb}'; expected: ${ON_AMBIGUOUS.join(", ")}`);
519
550
  }
520
551
 
521
- const allowRaw = (values.allow as string | undefined) ?? "";
522
- const allowTokens = allowRaw.length === 0
523
- ? []
524
- : allowRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
525
- for (const tok of allowTokens) {
526
- if (!ALLOW_TOKENS.includes(tok as AllowToken)) {
527
- throw new Error(
528
- `invalid --allow token '${tok}'; expected one of: ${ALLOW_TOKENS.join(", ")}`,
529
- );
530
- }
531
- }
552
+ const allowTokens = parseAllowTokens(values.allow as string | string[] | undefined);
532
553
 
533
554
  return {
534
555
  db: values.db as string | undefined,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Is this file text a HUMAN wrote, or a bundler's output?
3
+ *
4
+ * Both advisory scanners — the base-URL one and the anti-pattern one — exist to teach the
5
+ * author of a file something about the file. A bundle is not authored: nobody can act on a
6
+ * finding in it, the fix belongs in the source it was built from, and naming a generated
7
+ * artifact spends the scanners' whole false-positive budget in one line.
8
+ *
9
+ * The rule is a property of the ARTIFACT, not of a directory name. An estate proved why:
10
+ * its build output is `public/`, which no ignore list would have guessed (the lists cover
11
+ * `dist`, `build`, `.next`, `.output`, `out`), so `meta verify` reported
12
+ * `public/main.js:243 — <EntityFetcherProvider> is mounted with no baseUrl` while
13
+ * `client/src/main.tsx:26` — the source, the file a person edits — passes `baseUrl="/api"`
14
+ * correctly. Adding `public` to a list would have fixed that estate and nobody else.
15
+ *
16
+ * The two scanners keep their own walkers, deliberately (they diverge on eight axes and
17
+ * unifying them needs ~6 options for two callers). What they share is this judgment, so it
18
+ * lives here once rather than being decided twice.
19
+ */
20
+
21
+ /**
22
+ * Bundlers emit lines in the thousands-to-hundreds-of-thousands of characters; the estate
23
+ * bundle that produced the false positive has a longest line of 266,226. Authored source
24
+ * does not reach 5,000 — the threshold sits an order of magnitude above the longest line
25
+ * any human writes and an order of magnitude below what minification produces, so it is a
26
+ * wide gap rather than a tuned one.
27
+ *
28
+ * NOT `>= 512KB`, which `anti-patterns.ts` already applies for a different reason (cost):
29
+ * that gate is about how much work a scan does, and a 300KB bundle sails through it while
30
+ * being exactly as unauthored as a 700KB one.
31
+ */
32
+ const BUNDLED_LINE_LENGTH = 5000;
33
+
34
+ /** True when `text` looks like build output rather than source someone edits. */
35
+ export function looksBundled(text: string): boolean {
36
+ // Scan for a long line without splitting the whole file: a 700KB bundle would otherwise
37
+ // allocate an array of it, and this runs per file on every `meta verify` / `meta gen`.
38
+ let lineStart = 0;
39
+ for (let i = 0; i < text.length; i++) {
40
+ if (text.charCodeAt(i) !== 10 /* \n */) continue;
41
+ if (i - lineStart >= BUNDLED_LINE_LENGTH) return true;
42
+ lineStart = i + 1;
43
+ }
44
+ return text.length - lineStart >= BUNDLED_LINE_LENGTH;
45
+ }