@metaobjectsdev/codegen-ts 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.
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/generator.d.ts +15 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/agent-docs-file.d.ts.map +1 -1
- package/dist/generators/agent-docs-file.js +12 -3
- package/dist/generators/agent-docs-file.js.map +1 -1
- package/dist/generators/agent-schema-input.d.ts +11 -0
- package/dist/generators/agent-schema-input.d.ts.map +1 -1
- package/dist/generators/agent-schema-page.d.ts.map +1 -1
- package/dist/generators/agent-schema-page.js +13 -1
- package/dist/generators/agent-schema-page.js.map +1 -1
- package/dist/generators/names-file.d.ts.map +1 -1
- package/dist/generators/names-file.js +2 -1
- package/dist/generators/names-file.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +10 -0
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +14 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/overwrite-policy.d.ts +15 -3
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +26 -0
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +47 -5
- package/dist/runner.js.map +1 -1
- package/dist/ui-tier-gate.d.ts +44 -0
- package/dist/ui-tier-gate.d.ts.map +1 -0
- package/dist/ui-tier-gate.js +68 -0
- package/dist/ui-tier-gate.js.map +1 -0
- package/package.json +6 -6
- package/src/constants.ts +11 -0
- package/src/generator.ts +15 -0
- package/src/generators/agent-docs-file.ts +11 -2
- package/src/generators/agent-schema-input.ts +11 -0
- package/src/generators/agent-schema-page.ts +14 -1
- package/src/generators/names-file.ts +2 -1
- package/src/index.ts +6 -2
- package/src/metaobjects-config.ts +20 -1
- package/src/overwrite-policy.ts +43 -2
- package/src/reference/names.ts +5 -1
- package/src/runner.ts +50 -5
- package/src/ui-tier-gate.ts +79 -0
package/src/index.ts
CHANGED
|
@@ -8,6 +8,10 @@ export type { RunGenOpts, RunGenResult } from "./runner.js";
|
|
|
8
8
|
|
|
9
9
|
export type { Generator, GenContext, EmittedFile, GeneratorFactory } from "./generator.js";
|
|
10
10
|
export { perEntity, perPackage, perModel, oncePerRun } from "./generator.js";
|
|
11
|
+
// The one aggregation of the UI-tier markers. Exported because `meta docs` is the
|
|
12
|
+
// SECOND door onto `agent/ui.md` and must answer "does this run emit a UI tier?"
|
|
13
|
+
// with the same function the gen runner uses, not a second derivation of it.
|
|
14
|
+
export { runEmitsUiTier, runEmitsHonoRoutes, UI_TIER_GENERATOR_NAMES } from "./ui-tier-gate.js";
|
|
11
15
|
|
|
12
16
|
// SP-1 declarative Mustache template-codegen — scope walks, neutral data dict,
|
|
13
17
|
// output-pattern, and the JSON template-spec the CLI ports reuse.
|
|
@@ -42,7 +46,7 @@ export { defineConfig, normalizeConfig, resolveGenerators, resolveDocsConfig } f
|
|
|
42
46
|
// The dialect every generator falls back to when a config declares none. Exported because
|
|
43
47
|
// `meta docs` must apply the SAME default `normalizeConfig` does — a command that resolved
|
|
44
48
|
// the dialect differently would document a schema the toolchain does not produce.
|
|
45
|
-
export { DEFAULT_DIALECT } from "./metaobjects-config.js";
|
|
49
|
+
export { DEFAULT_DIALECT, DEFAULT_DOCS_DIR } from "./metaobjects-config.js";
|
|
46
50
|
// The runner's own "does this model need a dialect?" guard. `meta docs` must ask it rather
|
|
47
51
|
// than defaulting, because DEFAULT_DIALECT is inert and a DB project with no dialect is one
|
|
48
52
|
// `meta gen` REFUSES — documenting it as sqlite states an answer the toolchain never gave.
|
|
@@ -78,7 +82,7 @@ export { decideAndWrite, GitMissingError, WRITE_STATUSES } from "./overwrite-pol
|
|
|
78
82
|
export { contentHash, readGeneratedHash, listGeneratedPaths } from "./overwrite-policy.js";
|
|
79
83
|
|
|
80
84
|
export { CodegenError } from "./errors.js";
|
|
81
|
-
export { GENERATED_HEADER, DEFAULT_OUT_DIR, RETIRED_CODEGEN_ATTRS, type RetiredCodegenAttr } from "./constants.js";
|
|
85
|
+
export { GENERATED_HEADER, NAMES_FILE_SUFFIX, DEFAULT_OUT_DIR, RETIRED_CODEGEN_ATTRS, type RetiredCodegenAttr } from "./constants.js";
|
|
82
86
|
export { warnRetiredCodegenAttrs } from "./retired-codegen-attrs.js";
|
|
83
87
|
|
|
84
88
|
export { formatTs } from "./format.js";
|
|
@@ -63,6 +63,11 @@ export interface ResolvedGenConfig {
|
|
|
63
63
|
* Undefined ⇒ false. Templates read the same fact off `RenderContext.includeNames`,
|
|
64
64
|
* which the runner sets from this same aggregation. */
|
|
65
65
|
includeNames?: boolean;
|
|
66
|
+
/** Whether any CLIENT UI generator (form, hook, grid, columns) is active in the
|
|
67
|
+
* run — aggregated by the runner from the suite's `emitsUiTier` markers.
|
|
68
|
+
* `agent/ui.md` reads this to decide whether a UI tier exists to describe;
|
|
69
|
+
* a metadata predicate can only say a UI *could* be generated. Undefined ⇒ false. */
|
|
70
|
+
includeUiTier?: boolean;
|
|
66
71
|
/**
|
|
67
72
|
* FR-019 / ADR-0026: the module specifier from which an externally-PROVIDED
|
|
68
73
|
* shared enum (`@provided: true` on an abstract package-level `field.enum`) is
|
|
@@ -289,13 +294,27 @@ export interface ResolvedDocsConfig {
|
|
|
289
294
|
/** Merge the config `docs:` block with CLI overrides over documented defaults.
|
|
290
295
|
* `fallbackLayout` is the project's `outputLayout` so docs default to the same
|
|
291
296
|
* page placement as codegen when `docs.layout` is unset. */
|
|
297
|
+
/**
|
|
298
|
+
* Where `meta docs` writes when nothing says otherwise. A SUB-directory on purpose: the
|
|
299
|
+
* generated pages are MetaObjects' to own and overwrite, and `docs/` as a whole is not.
|
|
300
|
+
*/
|
|
301
|
+
export const DEFAULT_DOCS_DIR = "./docs/generated";
|
|
302
|
+
|
|
292
303
|
export function resolveDocsConfig(
|
|
293
304
|
block: DocsConfig | undefined,
|
|
294
305
|
cli: Partial<ResolvedDocsConfig>,
|
|
295
306
|
fallbackLayout: OutputLayout,
|
|
296
307
|
): ResolvedDocsConfig {
|
|
297
308
|
return {
|
|
298
|
-
|
|
309
|
+
// F57 — NOT "./docs". `docs/` is the human documentation folder in most repos: one
|
|
310
|
+
// estate's held a business-strategy .docx, product screenshots, email drafts and an
|
|
311
|
+
// analytics roadmap, and a bare `meta docs` scattered 29 generated pages through it
|
|
312
|
+
// and wanted to own `docs/README.md`. The gate's own design language is right —
|
|
313
|
+
// "docs.outDir is a directory, not a namespace MetaObjects owns" — and that is
|
|
314
|
+
// precisely the argument for not defaulting into the most-owned directory name in
|
|
315
|
+
// the ecosystem. `meta init` scaffolds this key explicitly so a new project's choice
|
|
316
|
+
// is visible rather than implied.
|
|
317
|
+
outDir: cli.outDir ?? block?.outDir ?? DEFAULT_DOCS_DIR,
|
|
299
318
|
layout: cli.layout ?? block?.layout ?? fallbackLayout,
|
|
300
319
|
baseUrl: cli.baseUrl ?? block?.baseUrl ?? "",
|
|
301
320
|
// `requirements` defaults ON. Safe ONLY because requirementsFile() emits ZERO
|
package/src/overwrite-policy.ts
CHANGED
|
@@ -60,6 +60,12 @@ export const WRITE_STATUSES = [
|
|
|
60
60
|
"conflict",
|
|
61
61
|
"refused",
|
|
62
62
|
"skipped",
|
|
63
|
+
/** `--baseline=adopt` — the file WOULD have been refused, and instead its current
|
|
64
|
+
* content was recorded as the merge base. Nothing was written. Its own status
|
|
65
|
+
* rather than "unchanged" or "skipped" because the run's whole product is the
|
|
66
|
+
* manifest it just created: a summary that reported these as unchanged would
|
|
67
|
+
* hide the one thing the adopter has to commit. */
|
|
68
|
+
"adopted",
|
|
63
69
|
/** FR-038 §8 — deleted because it was generated by a previous run, is no longer
|
|
64
70
|
* generated, and was never edited by hand. Reported as a file outcome rather
|
|
65
71
|
* than a warning because a deletion is exactly as consequential as a write,
|
|
@@ -81,8 +87,20 @@ export type MergeStrategy = "overwrite" | "skip-existing";
|
|
|
81
87
|
/** "default" — the standard three-way merge flow described in the file header.
|
|
82
88
|
* "fresh" — opt-in via `meta gen --baseline=fresh`. When .gen-state is absent
|
|
83
89
|
* but the file exists, OVERWRITE with fresh content and seed .gen-state from
|
|
84
|
-
* the fresh content (caveat 3 escape hatch).
|
|
85
|
-
|
|
90
|
+
* the fresh content (caveat 3 escape hatch).
|
|
91
|
+
* "adopt" — opt-in via `meta gen --baseline=adopt`. Same population, opposite
|
|
92
|
+
* half: where the run would REFUSE, record the file's CURRENT content as the
|
|
93
|
+
* merge base and write nothing. It exists because the refusal's leading remedy
|
|
94
|
+
* ("commit .hashes.json") could not be performed by the population it named —
|
|
95
|
+
* nothing writes a manifest until a gen succeeds, and gen refuses until one
|
|
96
|
+
* exists. `adopt` is the run that produces the file to commit.
|
|
97
|
+
*
|
|
98
|
+
* What it is NOT: protection for an edit already inside one of those files. The
|
|
99
|
+
* base becomes the edited text, so base == ours and the next regen's three-way
|
|
100
|
+
* merge takes fresh output wholesale. It protects edits made FROM THEN ON, and
|
|
101
|
+
* it guarantees that establishing the baseline writes nothing — which is what
|
|
102
|
+
* lets the content change land as its own reviewable diff afterwards. */
|
|
103
|
+
export type BaselineMode = "default" | "fresh" | "adopt";
|
|
86
104
|
|
|
87
105
|
export interface DecideAndWriteOpts {
|
|
88
106
|
strategy?: MergeStrategy;
|
|
@@ -468,6 +486,7 @@ type WriteCase =
|
|
|
468
486
|
| { kind: "no-snapshot-unchanged" }
|
|
469
487
|
| { kind: "no-snapshot-pristine-overwrite" }
|
|
470
488
|
| { kind: "no-snapshot-refused"; hasRecord: boolean }
|
|
489
|
+
| { kind: "no-snapshot-adopt"; currentText: string }
|
|
471
490
|
| { kind: "snapshot-unchanged" }
|
|
472
491
|
| { kind: "snapshot-merge-required"; snapshotText: string };
|
|
473
492
|
|
|
@@ -521,6 +540,18 @@ function classifyWrite(
|
|
|
521
540
|
|
|
522
541
|
// Either somebody edited it (hash mismatch) or we have no record of writing
|
|
523
542
|
// it at all (no hash). Both are unprovable, so fail closed.
|
|
543
|
+
//
|
|
544
|
+
// …unless the caller has explicitly adopted what is on disk. `adopt` is placed
|
|
545
|
+
// HERE, at the refusal, rather than at the top of the no-snapshot branch, and
|
|
546
|
+
// that placement is the design: it means adopting can only ever convert a
|
|
547
|
+
// refusal into a recorded baseline. Everything else — unchanged, and the
|
|
548
|
+
// pristine-file overwrite a fresh clone depends on — keeps behaving exactly as
|
|
549
|
+
// it does without the flag, so a project cannot accidentally freeze its
|
|
550
|
+
// regeneration by passing it.
|
|
551
|
+
if ((opts.baseline ?? "default") === "adopt") {
|
|
552
|
+
return { kind: "no-snapshot-adopt", currentText: current };
|
|
553
|
+
}
|
|
554
|
+
|
|
524
555
|
return {
|
|
525
556
|
kind: "no-snapshot-refused",
|
|
526
557
|
hasRecord: readGeneratedHash(genStateDir, relPath) !== undefined,
|
|
@@ -570,6 +601,8 @@ export function previewWriteStatus(
|
|
|
570
601
|
return "overwrite";
|
|
571
602
|
case "no-snapshot-refused":
|
|
572
603
|
return "refused";
|
|
604
|
+
case "no-snapshot-adopt":
|
|
605
|
+
return "adopted";
|
|
573
606
|
case "snapshot-merge-required":
|
|
574
607
|
return "overwrite";
|
|
575
608
|
}
|
|
@@ -641,6 +674,14 @@ export function decideAndWrite(
|
|
|
641
674
|
"--baseline=fresh to overwrite it and adopt fresh output as the baseline.",
|
|
642
675
|
};
|
|
643
676
|
|
|
677
|
+
case "no-snapshot-adopt":
|
|
678
|
+
// Record what is ON DISK — deliberately `kase.currentText`, never `content`.
|
|
679
|
+
// Recording fresh output would claim we wrote a file we did not, and the next
|
|
680
|
+
// run would then read the adopter's file as edited-since-generated and refuse
|
|
681
|
+
// all over again, which is the loop this mode exists to end.
|
|
682
|
+
advanceSnapshot(genStateDir, relPath, kase.currentText);
|
|
683
|
+
return { path, status: "adopted" };
|
|
684
|
+
|
|
644
685
|
case "snapshot-unchanged":
|
|
645
686
|
return { path, status: "unchanged" };
|
|
646
687
|
|
package/src/reference/names.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
namesArtifactSuperOf,
|
|
37
37
|
renderNamesDecl,
|
|
38
38
|
resolveObjectNames,
|
|
39
|
+
NAMES_FILE_SUFFIX,
|
|
39
40
|
type EmittedFile,
|
|
40
41
|
type GenContext,
|
|
41
42
|
type Generator,
|
|
@@ -78,7 +79,10 @@ export function namesFile(opts?: NamesFileOpts): Generator {
|
|
|
78
79
|
// its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
|
|
79
80
|
// conflicting-duplicate-path failure as soon as two packages declare a
|
|
80
81
|
// same-bare-named entity.
|
|
81
|
-
|
|
82
|
+
// NAMES_FILE_SUFFIX, not a literal: `meta verify --codegen` treats this artifact as
|
|
83
|
+
// fail-closed (a hand edit in it IS drift, unlike every other generated file) and
|
|
84
|
+
// recognises it BY THIS SUFFIX. Rename it and you opt out of that protection.
|
|
85
|
+
entityOutputPath(layout, effectivePackage(obj), `${obj.name}${NAMES_FILE_SUFFIX}`);
|
|
82
86
|
|
|
83
87
|
const superSpecifierFor = (obj: MetaObject): string | undefined => {
|
|
84
88
|
const sup = namesArtifactSuperOf(obj);
|
package/src/runner.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, relative, resolve, isAbsolute, dirname } from "node:path";
|
|
2
2
|
import { warnMissingPromptGenerators } from "./prompt-generator-gate.js";
|
|
3
|
+
import { runEmitsHonoRoutes, runEmitsUiTier, warnUnmarkedUiGenerators } from "./ui-tier-gate.js";
|
|
3
4
|
import { warnRetiredCodegenAttrs } from "./retired-codegen-attrs.js";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -469,7 +470,20 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
469
470
|
// Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
|
|
470
471
|
// surface it on every generator's ctx.config so api-docs documents the Hono
|
|
471
472
|
// CRUD surface it actually emits (rather than silently omitting it).
|
|
472
|
-
|
|
473
|
+
// The explicit config key WINS over the marker aggregation, at both doors. `meta docs`
|
|
474
|
+
// already read `loadedConfig?.includeHonoRoutes ?? …`, and the runner did not — so an
|
|
475
|
+
// adopter who set the key got the surface documented and not emitted, or the reverse.
|
|
476
|
+
// The key is settable because `MetaobjectsGenConfig extends Omit<ResolvedGenConfig,…>`,
|
|
477
|
+
// and it is the escape hatch for an owned generator whose name was changed; an escape
|
|
478
|
+
// hatch honoured by one of two doors is the defect these flags exist to close.
|
|
479
|
+
const includeHonoRoutes = config.includeHonoRoutes ?? runEmitsHonoRoutes(config.generators);
|
|
480
|
+
|
|
481
|
+
// Same auto-detection for the CLIENT UI tier. `agent/ui.md` describes forms, grids
|
|
482
|
+
// and the endpoints their hooks call; whether any of that is emitted is a generator
|
|
483
|
+
// fact, and the page's own gate could only see metadata. Run-scoped like
|
|
484
|
+
// includeHonoRoutes — the page asks "is this surface in the run?", never "does it
|
|
485
|
+
// land in my target?".
|
|
486
|
+
const includeUiTier = config.includeUiTier ?? runEmitsUiTier(config.generators);
|
|
473
487
|
|
|
474
488
|
// §A6 — same auto-detection for the OPT-IN names generator. The entity tier may only
|
|
475
489
|
// REFERENCE `<Entity>Names` when something in this run actually emits it; the names
|
|
@@ -493,6 +507,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
493
507
|
// this, said nothing — while `meta verify` reported the template "clean". See
|
|
494
508
|
// prompt-generator-gate.ts. Self-extinguishing; warning only.
|
|
495
509
|
warnMissingPromptGenerators(root, config.generators, (m) => warnings.push(m));
|
|
510
|
+
warnUnmarkedUiGenerators(config.generators, (m) => warnings.push(m));
|
|
496
511
|
|
|
497
512
|
// <Entity>Names is opt-in on TypeScript and an existing project gets no signal that
|
|
498
513
|
// it exists. Fires ONCE, on the first gen after crossing the release that made it the
|
|
@@ -598,6 +613,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
598
613
|
outputLayout: selfTarget.outputLayout,
|
|
599
614
|
includeHonoRoutes,
|
|
600
615
|
includeNames: namesTargets.has(selfTarget.name),
|
|
616
|
+
includeUiTier,
|
|
601
617
|
},
|
|
602
618
|
renderContext,
|
|
603
619
|
...(projectRoot !== undefined && { projectRoot }),
|
|
@@ -719,10 +735,15 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
719
735
|
`no codegen hash manifest — so 'meta gen' cannot tell your edits from its own ` +
|
|
720
736
|
`stale output, and it will not guess. This is the expected first run for a ` +
|
|
721
737
|
`project created before the manifest was committed. ` +
|
|
722
|
-
`ONE-TIME FIX:
|
|
723
|
-
|
|
724
|
-
`
|
|
725
|
-
`
|
|
738
|
+
`ONE-TIME FIX: re-run with --baseline=adopt — it records the files you have as ` +
|
|
739
|
+
`the merge base and writes NOTHING — then commit ` +
|
|
740
|
+
`'.metaobjects/.gen-state/.hashes.json' (un-ignore it in .metaobjects/.gitignore ` +
|
|
741
|
+
`with '.gen-state/*' + '!.gen-state/.hashes.json') and run 'meta gen' again, where ` +
|
|
742
|
+
`the regeneration arrives as its own reviewable diff. Adopting DECLARES these files ` +
|
|
743
|
+
`to be generated output, so an edit already inside one of them is part of the ` +
|
|
744
|
+
`baseline and that regeneration will replace it — commit before you run it. ` +
|
|
745
|
+
`To write fresh output NOW and DISCARD any hand edits in these files, ` +
|
|
746
|
+
`--baseline=fresh does both in one step. Files: ${names.join(", ")}${more}.`,
|
|
726
747
|
);
|
|
727
748
|
return;
|
|
728
749
|
}
|
|
@@ -762,6 +783,28 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
762
783
|
);
|
|
763
784
|
};
|
|
764
785
|
|
|
786
|
+
// `--baseline=adopt` — the counterpart of the aggregate above, and aggregated for the
|
|
787
|
+
// same reason: every adopted file has one cause and one next step. Reported even though
|
|
788
|
+
// nothing failed, because a run that writes nothing and exits 0 is otherwise
|
|
789
|
+
// indistinguishable from a no-op, and the file it DID produce (`.hashes.json`) is the
|
|
790
|
+
// one the adopter now has to commit.
|
|
791
|
+
const reportAdoptions = (): void => {
|
|
792
|
+
const adopted = writes.filter((w) => w.status === "adopted");
|
|
793
|
+
if (adopted.length === 0) return;
|
|
794
|
+
const names = adopted.slice(0, MAX_NAMED).map((w) => relativeForDisplay(w.path));
|
|
795
|
+
const more = adopted.length > MAX_NAMED ? `, and ${adopted.length - MAX_NAMED} more` : "";
|
|
796
|
+
warnings.push(
|
|
797
|
+
`Recorded ${adopted.length} existing file(s) as the codegen baseline and wrote ` +
|
|
798
|
+
`nothing. NEXT: commit '.metaobjects/.gen-state/.hashes.json' (un-ignore it in ` +
|
|
799
|
+
`.metaobjects/.gitignore with '.gen-state/*' + '!.gen-state/.hashes.json'), then run ` +
|
|
800
|
+
`'meta gen' — these files regenerate normally from here, as their own reviewable ` +
|
|
801
|
+
`diff. Adopting DECLARED them to be generated output: an edit already inside one is ` +
|
|
802
|
+
`part of the baseline and that regeneration will replace it, so check the diff ` +
|
|
803
|
+
`against git. Edits you make FROM NOW ON merge, because the base now exists. ` +
|
|
804
|
+
`Files: ${names.join(", ")}${more}.`,
|
|
805
|
+
);
|
|
806
|
+
};
|
|
807
|
+
|
|
765
808
|
const sweep = (dryRun: boolean): void => {
|
|
766
809
|
if (projectRoot === undefined || orphanJobs.length === 0) return;
|
|
767
810
|
|
|
@@ -863,6 +906,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
863
906
|
});
|
|
864
907
|
}
|
|
865
908
|
reportRefusals();
|
|
909
|
+
reportAdoptions();
|
|
866
910
|
// A preview that hides a pending deletion is worse than no preview at all, so
|
|
867
911
|
// the sweep still runs — in decide-and-report mode, touching nothing.
|
|
868
912
|
sweep(true);
|
|
@@ -896,6 +940,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
896
940
|
}
|
|
897
941
|
|
|
898
942
|
reportRefusals();
|
|
943
|
+
reportAdoptions();
|
|
899
944
|
|
|
900
945
|
// Sweep AFTER the writes: writing is the primary job, and a deletion that runs
|
|
901
946
|
// first would be unrecoverable if a later write threw. Ordering cannot change
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Generator } from "./generator.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generators that emit a CLIENT UI artifact — a form, a hook, a grid or its columns.
|
|
5
|
+
*
|
|
6
|
+
* Names, not identities, for the same reason `PROMPT_GENERATOR_NAMES` uses names: under
|
|
7
|
+
* ADR-0034 scaffold-and-own an adopter runs their OWN copy, and `meta eject` copies the
|
|
8
|
+
* template verbatim, so the name survives the copy while the object identity does not.
|
|
9
|
+
*
|
|
10
|
+
* This set exists ONLY as a compatibility path for a copy ejected before `emitsUiTier`
|
|
11
|
+
* existed. The marker is the mechanism; a name match makes the run behave as it did
|
|
12
|
+
* before AND says the marker is missing (see {@link warnUnmarkedUiGenerators}), because
|
|
13
|
+
* a page silently disappearing is exactly the kind of degradation that must announce
|
|
14
|
+
* itself.
|
|
15
|
+
*/
|
|
16
|
+
export const UI_TIER_GENERATOR_NAMES: ReadonlySet<string> = new Set([
|
|
17
|
+
"form-file",
|
|
18
|
+
"tanstack-query",
|
|
19
|
+
"tanstack-grid",
|
|
20
|
+
"tanstack-grid-hook",
|
|
21
|
+
"angular-form",
|
|
22
|
+
"angular-grid",
|
|
23
|
+
"angular-service",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Generators in the suite that emit a UI artifact but carry no `emitsUiTier` marker. */
|
|
27
|
+
function unmarked(generators: readonly Generator[]): Generator[] {
|
|
28
|
+
return generators.filter(
|
|
29
|
+
(g) => g.emitsUiTier !== true && UI_TIER_GENERATOR_NAMES.has(g.name),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Does this run emit a client UI tier?
|
|
35
|
+
*
|
|
36
|
+
* The declared marker OR the legacy name. `agent/ui.md` gates on the answer: its own
|
|
37
|
+
* predicate was metadata-only (`servesReadApi`), which says a UI *could* be generated
|
|
38
|
+
* for an object and never that this run generates one — so a project with
|
|
39
|
+
* `generators: []` was handed a confident page naming endpoints nothing serves.
|
|
40
|
+
*/
|
|
41
|
+
export function runEmitsUiTier(generators: readonly Generator[]): boolean {
|
|
42
|
+
return generators.some((g) => g.emitsUiTier === true) || unmarked(generators).length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A generator ejected before `emitsUiTier` existed still emits its tier, so the run is
|
|
47
|
+
* NOT degraded — the name match above keeps `agent/ui.md` emitting. It is warned about
|
|
48
|
+
* anyway: an adopter who renames their copy loses the page with no signal at all, and
|
|
49
|
+
* the one-line fix belongs where the fact is known rather than in a migration note
|
|
50
|
+
* nobody re-reads. Self-extinguishing — adding the marker silences it forever.
|
|
51
|
+
*/
|
|
52
|
+
export function warnUnmarkedUiGenerators(
|
|
53
|
+
generators: readonly Generator[],
|
|
54
|
+
warn: (msg: string) => void,
|
|
55
|
+
): void {
|
|
56
|
+
const stale = unmarked(generators);
|
|
57
|
+
if (stale.length === 0) return;
|
|
58
|
+
const names = stale.map((g) => `'${g.name}'`).join(", ");
|
|
59
|
+
warn(
|
|
60
|
+
`${names} emit${stale.length === 1 ? "s" : ""} a UI artifact but carr${stale.length === 1 ? "ies" : "y"} no ` +
|
|
61
|
+
`'emitsUiTier: true' marker — an owned copy ejected before the marker existed. ` +
|
|
62
|
+
`agent/ui.md still emits (matched by name), but a renamed copy would not: add ` +
|
|
63
|
+
`'emitsUiTier: true' beside 'name:' in your copy, or re-eject it.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Does this run emit the OPT-IN Hono routes surface?
|
|
69
|
+
*
|
|
70
|
+
* Lives beside `runEmitsUiTier` for one reason: both are "aggregate a marker across the
|
|
71
|
+
* suite", and both were being re-derived at the `meta docs` door — which read the RAW
|
|
72
|
+
* config, where a generator wired by stable name (`generators: ["routes-hono"]`, legal
|
|
73
|
+
* under ADR-0021 #1) is a string carrying no marker at all. So `api-docs` omitted the
|
|
74
|
+
* Hono surface for a project that had wired it. One helper, called after the strings are
|
|
75
|
+
* resolved, is what stops the two doors disagreeing again.
|
|
76
|
+
*/
|
|
77
|
+
export function runEmitsHonoRoutes(generators: readonly Generator[]): boolean {
|
|
78
|
+
return generators.some((g) => g.emitsHonoRoutes === true);
|
|
79
|
+
}
|