@metaobjectsdev/codegen-ts 0.24.4 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -12
- package/dist/api-surface.d.ts +29 -0
- package/dist/api-surface.d.ts.map +1 -1
- package/dist/api-surface.js +47 -0
- package/dist/api-surface.js.map +1 -1
- package/dist/client-directive.d.ts +18 -0
- package/dist/client-directive.d.ts.map +1 -0
- package/dist/client-directive.js +37 -0
- package/dist/client-directive.js.map +1 -0
- package/dist/column-mapper.d.ts +12 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +1 -0
- package/dist/column-mapper.js.map +1 -1
- package/dist/constants.d.ts +24 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +54 -19
- package/dist/constants.js.map +1 -1
- package/dist/db-emitting.d.ts +6 -0
- package/dist/db-emitting.d.ts.map +1 -0
- package/dist/db-emitting.js +26 -0
- package/dist/db-emitting.js.map +1 -0
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +7 -1
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +5 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/agent-docs-file.d.ts +19 -0
- package/dist/generators/agent-docs-file.d.ts.map +1 -0
- package/dist/generators/agent-docs-file.js +257 -0
- package/dist/generators/agent-docs-file.js.map +1 -0
- package/dist/generators/agent-requirements-page.d.ts +22 -0
- package/dist/generators/agent-requirements-page.d.ts.map +1 -0
- package/dist/generators/agent-requirements-page.js +175 -0
- package/dist/generators/agent-requirements-page.js.map +1 -0
- package/dist/generators/agent-schema-input.d.ts +69 -0
- package/dist/generators/agent-schema-input.d.ts.map +1 -0
- package/dist/generators/agent-schema-input.js +23 -0
- package/dist/generators/agent-schema-input.js.map +1 -0
- package/dist/generators/agent-schema-page.d.ts +21 -0
- package/dist/generators/agent-schema-page.d.ts.map +1 -0
- package/dist/generators/agent-schema-page.js +241 -0
- package/dist/generators/agent-schema-page.js.map +1 -0
- package/dist/generators/agent-ui-page.d.ts +22 -0
- package/dist/generators/agent-ui-page.d.ts.map +1 -0
- package/dist/generators/agent-ui-page.js +253 -0
- package/dist/generators/agent-ui-page.js.map +1 -0
- package/dist/generators/api-docs-file.d.ts.map +1 -1
- package/dist/generators/api-docs-file.js +13 -6
- package/dist/generators/api-docs-file.js.map +1 -1
- package/dist/generators/api-model.d.ts +6 -0
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +54 -35
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/callable-file.js +1 -1
- package/dist/generators/callable-file.js.map +1 -1
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +47 -2
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +6 -2
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/index.d.ts +3 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +4 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/names-file.d.ts +17 -0
- package/dist/generators/names-file.d.ts.map +1 -0
- package/dist/generators/names-file.js +95 -0
- package/dist/generators/names-file.js.map +1 -0
- package/dist/generators/requirements-markdown.d.ts +18 -1
- package/dist/generators/requirements-markdown.d.ts.map +1 -1
- package/dist/generators/requirements-markdown.js +11 -9
- package/dist/generators/requirements-markdown.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +17 -2
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +15 -12
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +17 -2
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +10 -8
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-generator.d.ts.map +1 -1
- package/dist/generators/template-generator.js +20 -6
- package/dist/generators/template-generator.js.map +1 -1
- package/dist/index.d.ts +21 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -4
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +67 -1
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +10 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/names.d.ts +275 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +434 -0
- package/dist/names.js.map +1 -0
- package/dist/naming.d.ts +17 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +21 -0
- package/dist/naming.js.map +1 -1
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +15 -3
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +4 -3
- package/dist/payload-codegen.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts +25 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +79 -6
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/prompt-generator-gate.d.ts +27 -0
- package/dist/prompt-generator-gate.d.ts.map +1 -0
- package/dist/prompt-generator-gate.js +62 -0
- package/dist/prompt-generator-gate.js.map +1 -0
- package/dist/reference-templates.d.ts +22 -5
- package/dist/reference-templates.d.ts.map +1 -1
- package/dist/reference-templates.js +55 -21
- package/dist/reference-templates.js.map +1 -1
- package/dist/render-context.d.ts +20 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +2 -0
- package/dist/render-context.js.map +1 -1
- package/dist/retired-codegen-attrs.d.ts +32 -0
- package/dist/retired-codegen-attrs.d.ts.map +1 -0
- package/dist/retired-codegen-attrs.js +47 -0
- package/dist/retired-codegen-attrs.js.map +1 -0
- package/dist/routes-expose.d.ts +63 -0
- package/dist/routes-expose.d.ts.map +1 -0
- package/dist/routes-expose.js +57 -0
- package/dist/routes-expose.js.map +1 -0
- package/dist/runner.d.ts +18 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +115 -7
- package/dist/runner.js.map +1 -1
- package/dist/templates/callable-file.d.ts +7 -2
- package/dist/templates/callable-file.d.ts.map +1 -1
- package/dist/templates/callable-file.js +54 -9
- package/dist/templates/callable-file.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +236 -65
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.d.ts +15 -6
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +85 -161
- package/dist/templates/entity-constants.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +38 -4
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/entity-meta-file.d.ts +15 -1
- package/dist/templates/entity-meta-file.d.ts.map +1 -1
- package/dist/templates/entity-meta-file.js +12 -2
- package/dist/templates/entity-meta-file.js.map +1 -1
- package/dist/templates/entity-ui-descriptor.d.ts +112 -0
- package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
- package/dist/templates/entity-ui-descriptor.js +238 -0
- package/dist/templates/entity-ui-descriptor.js.map +1 -0
- package/dist/templates/field-meta.d.ts +46 -9
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +95 -21
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/names-decl.d.ts +38 -0
- package/dist/templates/names-decl.d.ts.map +1 -0
- package/dist/templates/names-decl.js +104 -0
- package/dist/templates/names-decl.js.map +1 -0
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +54 -8
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/projection-decl.d.ts +21 -0
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +53 -29
- package/dist/templates/projection-decl.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +17 -2
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +3 -1
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/requirement-test.d.ts.map +1 -1
- package/dist/templates/requirement-test.js +48 -21
- package/dist/templates/requirement-test.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts +2 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +11 -2
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts +2 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +54 -21
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +15 -1
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/view-decl.d.ts +37 -1
- package/dist/templates/view-decl.d.ts.map +1 -1
- package/dist/templates/view-decl.js +27 -3
- package/dist/templates/view-decl.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +21 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +40 -5
- package/dist/templates/zod-validators.js.map +1 -1
- package/dist/view-context.d.ts +24 -0
- package/dist/view-context.d.ts.map +1 -0
- package/dist/view-context.js +81 -0
- package/dist/view-context.js.map +1 -0
- package/package.json +6 -6
- package/src/api-surface.ts +53 -0
- package/src/client-directive.ts +36 -0
- package/src/column-mapper.ts +13 -0
- package/src/constants.ts +91 -16
- package/src/db-emitting.ts +31 -0
- package/src/generator-registry.ts +7 -0
- package/src/generator.ts +5 -0
- package/src/generators/agent-docs-file.ts +297 -0
- package/src/generators/agent-requirements-page.ts +199 -0
- package/src/generators/agent-schema-input.ts +88 -0
- package/src/generators/agent-schema-page.ts +297 -0
- package/src/generators/agent-ui-page.ts +290 -0
- package/src/generators/api-docs-file.ts +13 -6
- package/src/generators/api-model.ts +63 -34
- package/src/generators/callable-file.ts +1 -1
- package/src/generators/docs-data-builder.ts +46 -2
- package/src/generators/docs-data.ts +6 -2
- package/src/generators/index.ts +10 -0
- package/src/generators/names-file.ts +98 -0
- package/src/generators/requirements-markdown.ts +36 -2
- package/src/generators/routes-file-hono.ts +26 -15
- package/src/generators/routes-file.ts +20 -7
- package/src/generators/template-generator.ts +20 -6
- package/src/index.ts +63 -5
- package/src/metaobjects-config.ts +78 -2
- package/src/names.ts +630 -0
- package/src/naming.ts +23 -0
- package/src/overwrite-policy.ts +15 -3
- package/src/payload-codegen.ts +4 -3
- package/src/projection/extract-view-spec.ts +85 -6
- package/src/prompt-generator-gate.ts +73 -0
- package/src/reference/barrel.ts +3 -0
- package/src/reference/entity.ts +28 -2
- package/src/reference/names.ts +125 -0
- package/src/reference/queries.ts +14 -2
- package/src/reference/routes-hono.ts +113 -0
- package/src/reference/routes.ts +30 -9
- package/src/reference-templates.ts +61 -21
- package/src/render-context.ts +22 -1
- package/src/retired-codegen-attrs.ts +52 -0
- package/src/routes-expose.ts +86 -0
- package/src/runner.ts +129 -11
- package/src/templates/callable-file.ts +60 -8
- package/src/templates/drizzle-schema.ts +248 -60
- package/src/templates/entity-constants.ts +95 -184
- package/src/templates/entity-file.ts +46 -4
- package/src/templates/entity-meta-file.ts +16 -3
- package/src/templates/entity-ui-descriptor.ts +305 -0
- package/src/templates/field-meta.ts +100 -19
- package/src/templates/names-decl.ts +165 -0
- package/src/templates/output-parser.ts +54 -7
- package/src/templates/projection-decl.ts +72 -31
- package/src/templates/queries-file.ts +17 -2
- package/src/templates/render-helper.ts +3 -1
- package/src/templates/requirement-test.ts +49 -18
- package/src/templates/routes-file-hono.ts +14 -2
- package/src/templates/routes-file.ts +69 -24
- package/src/templates/value-object-file.ts +15 -1
- package/src/templates/view-decl.ts +53 -3
- package/src/templates/zod-validators.ts +41 -5
- package/src/view-context.ts +88 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// `agent/ui.md` — the generated UI surface, for an agent about to touch a form or a grid.
|
|
2
|
+
//
|
|
3
|
+
// The gap this closes is narrow and real: the neutral model entity page renders NO
|
|
4
|
+
// `view.*` or `layout.*` metadata at all, so an agent asked to change a form had nowhere
|
|
5
|
+
// to learn what the form already is and would read the generated TSX — the disposable
|
|
6
|
+
// artifact — as if it were the source.
|
|
7
|
+
//
|
|
8
|
+
// EVERY FIELD ROW COMES FROM `buildEntityUiDescriptor`, the same derivation
|
|
9
|
+
// `renderEntityConstants` emits as the `<Entity>` const in `<Entity>.meta.ts`, which is
|
|
10
|
+
// what `useEntityForm` reads at runtime. One derivation, two renderings: the page cannot
|
|
11
|
+
// describe a control the form does not render, because both answers come from the same
|
|
12
|
+
// call. That is the point of the descriptor extraction, not a nicety.
|
|
13
|
+
//
|
|
14
|
+
// The three columns the descriptor does NOT carry — `@formExclude`, `@filterable` and
|
|
15
|
+
// `@sortable` — are read here from the field, and `@sortable`'s inherit-from-`@filterable`
|
|
16
|
+
// default resolves through `isSortableField`, the SAME predicate the generated
|
|
17
|
+
// `<Entity>SortAllowlist` is built from, so the page cannot say a field is sortable that
|
|
18
|
+
// the endpoint will reject. They are deliberately absent from the descriptor: the
|
|
19
|
+
// descriptor describes how a field is PRESENTED, while those three say whether it appears
|
|
20
|
+
// at all and what the LIST endpoint will accept, which is a different question the form
|
|
21
|
+
// control has no opinion on.
|
|
22
|
+
//
|
|
23
|
+
// THREE THINGS THE DESCRIPTOR ALONE CANNOT ANSWER, each read from the predicate the
|
|
24
|
+
// generator that decides it uses, never re-derived here:
|
|
25
|
+
//
|
|
26
|
+
// • WHERE the object is served — `restPath`, because a TPH subtype is mounted under its
|
|
27
|
+
// discriminator base and its own `$path` names nothing;
|
|
28
|
+
// • WHETHER a form exists at all — `hasGeneratedForm`, the form generator's own filter,
|
|
29
|
+
// because a discriminator base has a write endpoint and still gets no form;
|
|
30
|
+
// • WHAT the control is for a `field.object` — `valueObjectFor`, shared with the form
|
|
31
|
+
// generator, because a resolvable `@objectRef` is rendered as a nested sub-form and
|
|
32
|
+
// the field's view kind is not consulted.
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
FIELD_ATTR_FILTERABLE,
|
|
36
|
+
FIELD_ATTR_FORM_EXCLUDE,
|
|
37
|
+
LAYOUT_DATA_GRID_ATTR_COLUMNS,
|
|
38
|
+
LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD,
|
|
39
|
+
LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER,
|
|
40
|
+
LAYOUT_DATA_GRID_ATTR_PAGE_SIZE,
|
|
41
|
+
LAYOUT_SUBTYPE_DATA_GRID,
|
|
42
|
+
TYPE_LAYOUT,
|
|
43
|
+
} from "@metaobjectsdev/metadata";
|
|
44
|
+
import type { MetaData, MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
45
|
+
import { GENERATED_HEADER } from "../constants.js";
|
|
46
|
+
import { hasGeneratedForm, restPath, servedPath, servesReadApi } from "../api-surface.js";
|
|
47
|
+
import {
|
|
48
|
+
buildEntityUiDescriptor,
|
|
49
|
+
type UiFieldDescriptor,
|
|
50
|
+
type UiRule,
|
|
51
|
+
} from "../templates/entity-ui-descriptor.js";
|
|
52
|
+
import { isSortableField } from "../templates/filter-shared.js";
|
|
53
|
+
import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
|
|
54
|
+
import { declaresTphDiscriminator } from "../templates/zod-validators.js";
|
|
55
|
+
|
|
56
|
+
const GENERATED_MARKER = `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`;
|
|
57
|
+
|
|
58
|
+
function mdCell(text: string): string {
|
|
59
|
+
return text.replace(/\|/g, "\\|");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One rule, as a reader wants to see it — the value, not the message. */
|
|
63
|
+
function ruleText(r: UiRule): string {
|
|
64
|
+
switch (r.kind) {
|
|
65
|
+
case "required":
|
|
66
|
+
return "required";
|
|
67
|
+
case "minLength":
|
|
68
|
+
return `minLength ${r.value}`;
|
|
69
|
+
case "maxLength":
|
|
70
|
+
return `maxLength ${r.value}`;
|
|
71
|
+
case "pattern":
|
|
72
|
+
return `pattern \`${mdCell(r.pattern)}\``;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The CONTROL cell — what the generated form renders, which is not always a `view.*`
|
|
78
|
+
* subtype.
|
|
79
|
+
*
|
|
80
|
+
* A `field.object` whose `@objectRef` resolves is emitted as a nested `<fieldset>`
|
|
81
|
+
* sub-form (a `useFieldArray` repeatable group when the field is an array), and the form
|
|
82
|
+
* generator never consults the field's view kind for it. This column used to print that
|
|
83
|
+
* view kind — `text`, the `defaultViewForSubType` fallback — which said the control was a
|
|
84
|
+
* free-text input for a field that has no input at all. Same shape as the `field.enum`
|
|
85
|
+
* descriptor bug, one subtype family over; the predicate is shared with the form
|
|
86
|
+
* generator (`valueObjectFor`) so the two cannot answer differently again.
|
|
87
|
+
*/
|
|
88
|
+
function controlCell(f: UiFieldDescriptor): string {
|
|
89
|
+
if (f.nested === undefined) return `\`${f.view}\``;
|
|
90
|
+
return f.nested.isArray ? "nested sub-form (repeatable)" : "nested sub-form";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The HTML type cell — empty for a nested sub-form, which is not an input at all. */
|
|
94
|
+
function htmlTypeCell(f: UiFieldDescriptor): string {
|
|
95
|
+
if (f.nested !== undefined || f.htmlType === undefined) return "";
|
|
96
|
+
return `\`${f.htmlType}\``;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `yes` / `` — an empty cell reads better than a column of "no". */
|
|
100
|
+
function flag(value: unknown): string {
|
|
101
|
+
return value === true ? "yes" : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The endpoint line under an object's heading.
|
|
106
|
+
*
|
|
107
|
+
* THE `apiPrefix` IS PART OF THE ADDRESS. `routes-file.ts` registers every mount inside
|
|
108
|
+
* `fastify.register(…, { prefix: apiPrefix })`, so a project configuring `/api` serves
|
|
109
|
+
* `/api/owners` and nothing at `/owners`. This page states that its heading is the address
|
|
110
|
+
* the routes mount at, so omitting the prefix made that promise false for every project
|
|
111
|
+
* that sets one — and a reader following it gets a 404.
|
|
112
|
+
*
|
|
113
|
+
* `restPath`, NOT `descriptor.path`: a TPH subtype's own `$path` names an address
|
|
114
|
+
* nothing mounts — the hierarchy is mounted from the discriminator base, each subtype
|
|
115
|
+
* at `<base>/<segment>` — and this page printed that non-existent address as fact.
|
|
116
|
+
* Whether a form exists is `hasGeneratedForm` — the form generator's OWN filter —
|
|
117
|
+
* never `servesWriteApi`. A read-only object (a projection, a view-backed entity) has
|
|
118
|
+
* no Insert/Update schema; a TPH discriminator BASE has both and still gets no form,
|
|
119
|
+
* because you cannot create a base and its polymorphic mount is read-only by
|
|
120
|
+
* construction. Saying so is the difference between "the form is missing" and "there
|
|
121
|
+
* is deliberately no form"; asking the endpoint question announced one for every base.
|
|
122
|
+
*/
|
|
123
|
+
function endpointLine(obj: MetaObject, root: MetaRoot, apiPrefix: string): string {
|
|
124
|
+
const endpoint = servedPath(obj, apiPrefix);
|
|
125
|
+
if (hasGeneratedForm(obj)) return `Endpoint \`${endpoint}\`.`;
|
|
126
|
+
// `isTphDiscriminatorBase`, which requires at least one CONCRETE subtype — the same
|
|
127
|
+
// predicate `routes-file.ts` switches on. `@discriminator` with no subtype yet is a
|
|
128
|
+
// refactor-in-progress shape: the routes generator emits the VANILLA full-CRUD file for
|
|
129
|
+
// it, so "each concrete subtype below has its own" would name subtypes that do not
|
|
130
|
+
// exist, on an object that does have a write endpoint.
|
|
131
|
+
// THREE reasons, not two. `isTphDiscriminatorBase` requires at least one CONCRETE
|
|
132
|
+
// subtype — the same predicate `routes-file.ts` switches on — and the case it excludes
|
|
133
|
+
// is real: an object declaring `@discriminator` with no subtype yet gets the VANILLA
|
|
134
|
+
// full-CRUD routes file, so it is NOT read-only, while the form generator declines it
|
|
135
|
+
// for declaring `@discriminator` at all. Calling that "read-only" is false in the one
|
|
136
|
+
// direction that matters, since a reader would conclude it cannot be written to.
|
|
137
|
+
const reason = isTphDiscriminatorBase(obj, root)
|
|
138
|
+
? "for a discriminator base — its own mount is list/get only, and each concrete " +
|
|
139
|
+
"subtype below has its own form"
|
|
140
|
+
: declaresTphDiscriminator(obj)
|
|
141
|
+
? "— the form generator declines any object declaring `@discriminator`, and this " +
|
|
142
|
+
"one has no concrete subtype yet. Its routes are the ordinary full CRUD set"
|
|
143
|
+
: "(read-only)";
|
|
144
|
+
return (
|
|
145
|
+
`Endpoint \`${endpoint}\` — **no form is generated** ${reason}. ` +
|
|
146
|
+
"The fields below describe the grid and the filters."
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The `layout.dataGrid` children an object declares. ADR-0039: resolving. */
|
|
151
|
+
function dataGrids(obj: MetaObject): MetaData[] {
|
|
152
|
+
return obj
|
|
153
|
+
.children()
|
|
154
|
+
.filter((c) => c.type === TYPE_LAYOUT && c.subType === LAYOUT_SUBTYPE_DATA_GRID);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* True when a UI generator would emit for this object.
|
|
159
|
+
*
|
|
160
|
+
* `servesReadApi` — the api-surface predicate the hook, grid and form generators
|
|
161
|
+
* themselves gate on — NOT "has fields" and never an object-subtype test. A form, a grid
|
|
162
|
+
* and a hook are all clients of a generated endpoint, so an object with no endpoint has no
|
|
163
|
+
* UI to document.
|
|
164
|
+
*
|
|
165
|
+
* Getting this wrong is not cosmetic. Gating on "has fields" put a prompt payload
|
|
166
|
+
* (`object.value`, no source, no routes) on the page under a heading that announced an
|
|
167
|
+
* endpoint derived from its name — an address that does not exist, stated as fact, on the
|
|
168
|
+
* page an agent is told to trust. Instance artifacts derive from a declared SOURCE; the
|
|
169
|
+
* UI tier asks the endpoint question and never a storage or subtype one.
|
|
170
|
+
*/
|
|
171
|
+
export function hasUiSurface(obj: MetaObject): boolean {
|
|
172
|
+
return servesReadApi(obj);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function gridSection(grid: MetaData): string[] {
|
|
176
|
+
const out: string[] = [];
|
|
177
|
+
out.push(`**Grid \`${grid.name}\`**`);
|
|
178
|
+
out.push("");
|
|
179
|
+
const columns = grid.attr(LAYOUT_DATA_GRID_ATTR_COLUMNS);
|
|
180
|
+
if (Array.isArray(columns) && columns.length > 0) {
|
|
181
|
+
out.push(`- columns: ${columns.map((c) => `\`${String(c)}\``).join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
const sortField = grid.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD);
|
|
184
|
+
if (typeof sortField === "string" && sortField !== "") {
|
|
185
|
+
const order = grid.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER);
|
|
186
|
+
const suffix = typeof order === "string" && order !== "" ? `:${order}` : "";
|
|
187
|
+
out.push(`- default sort: \`${sortField}${suffix}\``);
|
|
188
|
+
}
|
|
189
|
+
const pageSize = grid.attr(LAYOUT_DATA_GRID_ATTR_PAGE_SIZE);
|
|
190
|
+
if (typeof pageSize === "number") out.push(`- page size: ${pageSize}`);
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Render the page. Returns "" when no object in the model has a UI surface — the surface
|
|
196
|
+
* then emits no FILE, so a headless project sees nothing rather than an empty page.
|
|
197
|
+
*/
|
|
198
|
+
export function renderAgentUiPage(root: MetaRoot, apiPrefix = ""): string {
|
|
199
|
+
const objects = root.objects();
|
|
200
|
+
const withUi = objects.filter(hasUiSurface);
|
|
201
|
+
if (withUi.length === 0) return "";
|
|
202
|
+
|
|
203
|
+
const out: string[] = [];
|
|
204
|
+
out.push(GENERATED_MARKER);
|
|
205
|
+
out.push("");
|
|
206
|
+
out.push("# UI");
|
|
207
|
+
out.push("");
|
|
208
|
+
out.push(
|
|
209
|
+
"What the generated forms and grids already are. Read it before changing a form, a " +
|
|
210
|
+
"grid or a filter — the generated `.tsx` is the disposable artifact, this is what " +
|
|
211
|
+
"produced it.",
|
|
212
|
+
);
|
|
213
|
+
out.push("");
|
|
214
|
+
out.push(
|
|
215
|
+
"- Every row here is the SAME derivation the runtime reads — `buildEntityUiDescriptor`, " +
|
|
216
|
+
"which is emitted as the `<Entity>` const in `<Entity>.meta.ts` and consumed by " +
|
|
217
|
+
"`useEntityForm`. Changing the metadata changes both. A READ-ONLY projection's const " +
|
|
218
|
+
"carries the subset that applies to it (no form, so no rules and no HTML type) plus " +
|
|
219
|
+
"the `dbCol` only a view-backed const has.",
|
|
220
|
+
);
|
|
221
|
+
out.push(
|
|
222
|
+
"- `Control` is what the FORM renders. A field declaring several views is described by " +
|
|
223
|
+
"the one named `form`; a `field.object` whose `@objectRef` resolves is not an input " +
|
|
224
|
+
"at all but a nested sub-form over that value object, and says so.",
|
|
225
|
+
);
|
|
226
|
+
out.push(
|
|
227
|
+
"- `Filter` / `Sort` are what the generated LIST endpoint accepts. `@sortable` " +
|
|
228
|
+
"defaults to `@filterable`, and the resolved answer is what is shown.",
|
|
229
|
+
);
|
|
230
|
+
out.push("");
|
|
231
|
+
|
|
232
|
+
for (const obj of withUi) {
|
|
233
|
+
out.push(`## \`${obj.resolutionKey()}\``);
|
|
234
|
+
out.push("");
|
|
235
|
+
const descriptor = buildEntityUiDescriptor(obj, root);
|
|
236
|
+
out.push(endpointLine(obj, root, apiPrefix));
|
|
237
|
+
out.push("");
|
|
238
|
+
if (descriptor.fields.length > 0) {
|
|
239
|
+
// The field nodes, keyed by name, so the three non-descriptor columns can be read
|
|
240
|
+
// off the field the descriptor row came from.
|
|
241
|
+
const byName = new Map(obj.fields().map((f) => [f.name, f]));
|
|
242
|
+
out.push("| Field | Label | Control | HTML type | Rules | Excluded | Filter | Sort |");
|
|
243
|
+
out.push("|---|---|---|---|---|---|---|---|");
|
|
244
|
+
for (const f of descriptor.fields) {
|
|
245
|
+
const node = byName.get(f.name);
|
|
246
|
+
const cells = [
|
|
247
|
+
`\`${f.name}\``,
|
|
248
|
+
mdCell(f.label),
|
|
249
|
+
controlCell(f),
|
|
250
|
+
htmlTypeCell(f),
|
|
251
|
+
f.rules.map(ruleText).join(" · "),
|
|
252
|
+
node === undefined ? "" : flag(node.attr(FIELD_ATTR_FORM_EXCLUDE)),
|
|
253
|
+
node === undefined ? "" : flag(node.attr(FIELD_ATTR_FILTERABLE)),
|
|
254
|
+
node === undefined ? "" : flag(isSortableField(node)),
|
|
255
|
+
];
|
|
256
|
+
out.push(`| ${cells.join(" | ")} |`);
|
|
257
|
+
}
|
|
258
|
+
// Which value object a nested field expands into rides below the table: it is the
|
|
259
|
+
// one thing a reader needs to go and edit, and it does not fit a cell.
|
|
260
|
+
const nested = descriptor.fields.filter((f) => f.nested !== undefined);
|
|
261
|
+
if (nested.length > 0) {
|
|
262
|
+
out.push("");
|
|
263
|
+
for (const f of nested) {
|
|
264
|
+
out.push(
|
|
265
|
+
`- \`${f.name}\` — expands \`${f.nested?.objectRef}\`` +
|
|
266
|
+
`${f.nested?.isArray === true ? ", one group per element" : ""}. Change the ` +
|
|
267
|
+
"fields inside it on that value object, not here.",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const money = descriptor.fields.filter((f) => f.currency !== undefined);
|
|
272
|
+
if (money.length > 0) {
|
|
273
|
+
out.push("");
|
|
274
|
+
for (const f of money) {
|
|
275
|
+
out.push(
|
|
276
|
+
`- \`${f.name}\` — money: \`${f.currency?.currency}\` formatted for ` +
|
|
277
|
+
`\`${f.currency?.locale}\`. Stored and sent as INTEGER MINOR UNITS; never format it server-side.`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const grid of dataGrids(obj)) {
|
|
283
|
+
out.push("");
|
|
284
|
+
out.push(...gridSection(grid));
|
|
285
|
+
}
|
|
286
|
+
out.push("");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return `${out.join("\n").replace(/\n+$/, "")}\n`;
|
|
290
|
+
}
|
|
@@ -18,12 +18,16 @@
|
|
|
18
18
|
// Part 3) — so it appears in `gen --list` and is selectable by its stable name
|
|
19
19
|
// `api-docs`. It is NOT a `meta docs` mode.
|
|
20
20
|
//
|
|
21
|
-
// It is NOT
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
21
|
+
// It is NOT part of the default `meta gen` scaffold suite: it is registry-listed but not
|
|
22
|
+
// auto-run. `meta docs` is the door that emits it.
|
|
23
|
+
//
|
|
24
|
+
// THE POINTER IS NO LONGER DEFERRED. This header used to record "surfacing the agent form
|
|
25
|
+
// (AGENT-API.md) to a coding agent via a pointer from the installed `.metaobjects/`
|
|
26
|
+
// context" as an agent-context-coordination follow-up. It was deferred, no roadmap item
|
|
27
|
+
// tracked it, and for as long as that held, the page existed and nothing routed an agent
|
|
28
|
+
// to it. `agent-context/templates/always-on.md.mustache` now names this file and the three
|
|
29
|
+
// `agent/` pages, and says when to read each. If you rename or move this artifact, that
|
|
30
|
+
// template is the other half.
|
|
27
31
|
|
|
28
32
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
29
33
|
import type { Generator, GeneratorFactory, EmittedFile } from "../generator.js";
|
|
@@ -95,6 +99,9 @@ export const apiDocsFile = function apiDocsFile(opts?: ApiDocsFileOpts): Generat
|
|
|
95
99
|
loadedRoot: ctx.loadedRoot,
|
|
96
100
|
outputLayout: layout,
|
|
97
101
|
includeHonoRoutes: ctx.config.includeHonoRoutes ?? false,
|
|
102
|
+
// The project's prefix, because every route is mounted inside it. Without
|
|
103
|
+
// this the page documents `/authors` for routes served at `/api/authors`.
|
|
104
|
+
apiPrefix: ctx.renderContext?.apiPrefix ?? "",
|
|
98
105
|
...(ctx.renderContext?.pkMap !== undefined && { pkMap: ctx.renderContext.pkMap }),
|
|
99
106
|
});
|
|
100
107
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// (templates/routes-file-hono.ts) — the OPT-IN Hono variant
|
|
36
36
|
// of the Fastify REST surface. Documented ONLY when the
|
|
37
37
|
// adopter wires routesFileHono() (ctx.includeHonoRoutes), and
|
|
38
|
-
// gated by the SAME
|
|
38
|
+
// gated by the SAME source + TPH rules as the Fastify surface.
|
|
39
39
|
// • template.prompt (T5):
|
|
40
40
|
// - prompt : render<Name> (payload, provider): string — the prompt
|
|
41
41
|
// render handle promptRender() emits into a single
|
|
@@ -75,10 +75,17 @@
|
|
|
75
75
|
// / update<Sub>ById / delete<Sub>ById scoped to the shared table) and the
|
|
76
76
|
// subtype REST subpaths are NOT YET documented by this builder — that fuller
|
|
77
77
|
// TPH modeling is a tracked follow-up (under-documentation, allowed).
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
78
|
+
//
|
|
79
|
+
// NOT modelled here (stated so the gap is known + intentional): a generator's own
|
|
80
|
+
// `filter` option. `routesFile({ filter })` narrows what the routes generator emits,
|
|
81
|
+
// and this builder reads the MODEL, not the wired generator set, so it cannot see
|
|
82
|
+
// that narrowing and will over-document REST for a filtered-out entity. That is
|
|
83
|
+
// under-documentation's opposite and it is a real limitation — but it is inherent to
|
|
84
|
+
// a docs pass that runs off metadata, and it applies equally to every generator's
|
|
85
|
+
// filter. (It used to be dodged for routes ALONE via an @emitRoutes:false metadata
|
|
86
|
+
// read; that attribute was never registered vocabulary, so `meta verify` rejected the
|
|
87
|
+
// very models this builder was reading it from. The read is gone — see
|
|
88
|
+
// constants.ts.)
|
|
82
89
|
|
|
83
90
|
import {
|
|
84
91
|
type MetaRoot,
|
|
@@ -112,8 +119,7 @@ import { isTphSubtype } from "../templates/zod-validators.js";
|
|
|
112
119
|
import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
|
|
113
120
|
import { isCallableEntity } from "../templates/callable-file.js";
|
|
114
121
|
import { hasAnyRdbSource } from "../source-detect.js";
|
|
115
|
-
import {
|
|
116
|
-
import { resourcePath } from "../templates/entity-constants.js";
|
|
122
|
+
import { servedPath } from "../api-surface.js";
|
|
117
123
|
import { isProjection } from "../projection/projection-detector.js";
|
|
118
124
|
import { buildPkMap } from "../pk-resolver.js";
|
|
119
125
|
import { buildRelationMap, type RelationEntry, type RelationMap } from "../relation-resolver.js";
|
|
@@ -263,6 +269,12 @@ export interface ApiModelContext {
|
|
|
263
269
|
* run didn't configure). The Fastify REST surface is always documented (it is
|
|
264
270
|
* the default-suite routes generator). Defaults to false. */
|
|
265
271
|
includeHonoRoutes?: boolean;
|
|
272
|
+
/** The project's `apiPrefix`. Every generated route is mounted inside
|
|
273
|
+
* `fastify.register(…, { prefix: apiPrefix })`, so it is part of the address a
|
|
274
|
+
* documented endpoint is served at — a page that omits it is one path segment
|
|
275
|
+
* short. Threaded explicitly rather than read off the RenderContext below,
|
|
276
|
+
* which is a `{ pkMap }` shim and carries nothing else. Defaults to "". */
|
|
277
|
+
apiPrefix?: string;
|
|
266
278
|
}
|
|
267
279
|
|
|
268
280
|
// ---------------------------------------------------------------------------
|
|
@@ -275,13 +287,14 @@ export function buildApiModel(root: MetaRoot, ctx: ApiModelContext): ApiModel {
|
|
|
275
287
|
// shim is sufficient (and avoids forcing callers to build a full context).
|
|
276
288
|
const pkCtx = { pkMap } as RenderContext;
|
|
277
289
|
const layout = ctx.outputLayout ?? "flat";
|
|
290
|
+
const apiPrefix = ctx.apiPrefix ?? "";
|
|
278
291
|
const relationMap = ctx.relationMap ?? buildRelationMap(root);
|
|
279
292
|
const includeHono = ctx.includeHonoRoutes ?? false;
|
|
280
293
|
|
|
281
294
|
const units: ApiUnitDoc[] = [];
|
|
282
295
|
|
|
283
296
|
for (const obj of root.objects()) {
|
|
284
|
-
units.push(buildEntityUnit(obj, pkCtx, root, layout, relationMap, includeHono));
|
|
297
|
+
units.push(buildEntityUnit(obj, pkCtx, root, layout, relationMap, includeHono, apiPrefix));
|
|
285
298
|
}
|
|
286
299
|
|
|
287
300
|
for (const tmpl of templateOutputs(root)) {
|
|
@@ -353,15 +366,6 @@ function isQueryable(obj: MetaObject): boolean {
|
|
|
353
366
|
return hasAnyRdbSource(obj) && !isTphSubtype(obj);
|
|
354
367
|
}
|
|
355
368
|
|
|
356
|
-
/** Whether the routes generator emits REST routes for this entity. It filters
|
|
357
|
-
* out @emitRoutes:false (routes-file.ts:27), unlike the queries + validator
|
|
358
|
-
* generators which always emit. So REST symbols are gated separately from the
|
|
359
|
-
* other queryable kinds. */
|
|
360
|
-
function emitsRoutes(obj: MetaObject): boolean {
|
|
361
|
-
// ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
|
|
362
|
-
return obj.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
369
|
function buildEntityUnit(
|
|
366
370
|
obj: MetaObject,
|
|
367
371
|
ctx: RenderContext,
|
|
@@ -369,6 +373,7 @@ function buildEntityUnit(
|
|
|
369
373
|
layout: OutputLayout,
|
|
370
374
|
relationMap: RelationMap,
|
|
371
375
|
includeHono: boolean,
|
|
376
|
+
apiPrefix: string,
|
|
372
377
|
): ApiUnitDoc {
|
|
373
378
|
const name = obj.name;
|
|
374
379
|
const symbols: ApiSymbol[] = [];
|
|
@@ -392,13 +397,15 @@ function buildEntityUnit(
|
|
|
392
397
|
if (isQueryable(obj)) {
|
|
393
398
|
symbols.push(...dataAccessSymbols(obj, ctx, root, layout));
|
|
394
399
|
symbols.push(...validationSymbols(obj, entityMod));
|
|
395
|
-
// REST
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
// REST needs no gate of its own: the routes generator's built-in filter is
|
|
401
|
+
// `hasAnyRdbSource && !isTphSubtype` — exactly isQueryable — so every queryable
|
|
402
|
+
// object gets routes. (Its `filter` option can narrow that further; this builder
|
|
403
|
+
// reads the model, not the wired generator set, and cannot see it. See the module
|
|
404
|
+
// header.)
|
|
405
|
+
symbols.push(...restSymbols(obj, layout, root, apiPrefix));
|
|
406
|
+
// The OPT-IN Hono variant mounts the SAME CRUD verbs under the same rules —
|
|
407
|
+
// documented only when the adopter wired it.
|
|
408
|
+
if (includeHono) symbols.push(...restHonoSymbols(obj, layout, apiPrefix));
|
|
402
409
|
}
|
|
403
410
|
|
|
404
411
|
// --- relation: the drizzle relations() export, when the resolver derives a
|
|
@@ -565,15 +572,35 @@ function validationSymbols(obj: MetaObject, entityMod: string): ApiSymbol[] {
|
|
|
565
572
|
* generator does NOT emit one function per verb — it emits a single
|
|
566
573
|
* `<name>Routes(fastify)` handler that mounts the standard CRUD verb set at the
|
|
567
574
|
* entity's $path via mountCrudRoutes (or the read-only subset via
|
|
568
|
-
* mountReadOnlyCrudRoutes for a projection).
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
575
|
+
* mountReadOnlyCrudRoutes for a projection). The verb→path mapping mirrors the
|
|
576
|
+
* runtime mountCrudRoutes contract referenced in routes-file.ts's comments.
|
|
577
|
+
*
|
|
578
|
+
* THE DOCUMENTED PATH IS THE ADDRESS THE ROUTES ACTUALLY SERVE, which takes two
|
|
579
|
+
* things this comment used to claim it got from one. It said it reused
|
|
580
|
+
* `resourcePath()` — "the same function entity-constants.ts uses to compute
|
|
581
|
+
* $path" — and both halves of that had stopped being true:
|
|
582
|
+
*
|
|
583
|
+
* • `$path` is `restPath()` now, not `resourcePath()`. A TPH subtype is mounted
|
|
584
|
+
* at `<base>/<segment>`, so `resourcePath` named an address nothing serves —
|
|
585
|
+
* the defect fixed for `$path` itself, still live here.
|
|
586
|
+
* • Every mount sits inside `fastify.register(…, { prefix: apiPrefix })`, so a
|
|
587
|
+
* project configuring "/api" was documented one path segment short.
|
|
588
|
+
*
|
|
589
|
+
* A TPH DISCRIMINATOR BASE serves only `TPH_POLYMORPHIC_VERBS` at its own path, and that
|
|
590
|
+
* set is imported rather than restated. `isProjection` alone was the read-only test, so a
|
|
591
|
+
* base — which is not a projection — was documented with POST/PATCH/DELETE against a mount
|
|
592
|
+
* `routes-file.ts` builds as `intersectExpose(TPH_POLYMORPHIC_VERBS, expose)`: three verbs
|
|
593
|
+
* that 404, published in the file an adopter is told to call generated code from, under the
|
|
594
|
+
* comment three lines above claiming these paths match the routes exactly. The per-subtype
|
|
595
|
+
* mounts at `<base path>/<segment>`, which is where writes actually live, remain a
|
|
596
|
+
* documented deferral (see the module header).
|
|
572
597
|
*/
|
|
573
|
-
function restSymbols(
|
|
598
|
+
function restSymbols(
|
|
599
|
+
obj: MetaObject, layout: OutputLayout, root: MetaRoot, apiPrefix: string,
|
|
600
|
+
): ApiSymbol[] {
|
|
574
601
|
const name = obj.name;
|
|
575
|
-
const path =
|
|
576
|
-
const readOnly = isProjection(obj);
|
|
602
|
+
const path = servedPath(obj, apiPrefix);
|
|
603
|
+
const readOnly = isProjection(obj) || isTphDiscriminatorBase(obj, root);
|
|
577
604
|
|
|
578
605
|
// REST endpoints are not importable functions — to WIRE them an adopter
|
|
579
606
|
// imports the entity's route registrar (`<entity>Routes`) from the routes
|
|
@@ -776,13 +803,15 @@ function callableArgsRef(obj: MetaObject, root: MetaRoot): string | undefined {
|
|
|
776
803
|
/**
|
|
777
804
|
* The OPT-IN Hono CRUD registrar `register<Entity>Routes(app, deps)` the
|
|
778
805
|
* routesFileHono generator emits into `<Entity>.routes.hono.ts`. Parallels the
|
|
779
|
-
* Fastify restSymbols (same verb set, same
|
|
806
|
+
* Fastify restSymbols (same verb set, same served address, read-only for
|
|
780
807
|
* projections) but carries the Hono registrar name + import module. Documented
|
|
781
808
|
* only when the adopter opts into the Hono variant (includeHonoRoutes).
|
|
782
809
|
*/
|
|
783
|
-
function restHonoSymbols(
|
|
810
|
+
function restHonoSymbols(
|
|
811
|
+
obj: MetaObject, layout: OutputLayout, apiPrefix: string,
|
|
812
|
+
): ApiSymbol[] {
|
|
784
813
|
const name = obj.name;
|
|
785
|
-
const path =
|
|
814
|
+
const path = servedPath(obj, apiPrefix);
|
|
786
815
|
const readOnly = isProjection(obj);
|
|
787
816
|
|
|
788
817
|
const honoMod = entityModulePath(layout, obj, `${name}.routes.hono`);
|
|
@@ -33,7 +33,7 @@ export const callableFile = function callableFile(opts?: CallableFileOpts): Gene
|
|
|
33
33
|
entity.package,
|
|
34
34
|
`${entity.name}.callable.ts`,
|
|
35
35
|
),
|
|
36
|
-
content: await formatTs(renderCallableFile(entity)),
|
|
36
|
+
content: await formatTs(renderCallableFile(entity, ctx.renderContext)),
|
|
37
37
|
};
|
|
38
38
|
}),
|
|
39
39
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// markdown structure now lives in templates/docs/entity-page.md.mustache.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
type MetaData,
|
|
7
8
|
type MetaObject,
|
|
8
9
|
type MetaField,
|
|
9
10
|
type MetaIdentity,
|
|
@@ -699,17 +700,60 @@ export function buildEntityDocData(
|
|
|
699
700
|
//
|
|
700
701
|
// ENTITY-GRAIN. Object coverage is entity-grain, so a claimed value/projection gets
|
|
701
702
|
// nothing — surfacing one would imply a coverage rule the ledger does not have.
|
|
703
|
+
//
|
|
704
|
+
// "Entity-grain" is about which PAGE the row lands on, NOT about how deep the claim may
|
|
705
|
+
// point. A claim is authored at whatever grain the requirement is really about, and an L5
|
|
706
|
+
// one names a MEMBER — `acme::Subscriber.status` in the shipped showcase. `walkRequirements`
|
|
707
|
+
// resolves that to the FIELD node, so an identity test against `entity` missed it and the
|
|
708
|
+
// showcase's only claim produced no "Required by" at all. The claim belongs on the owning
|
|
709
|
+
// entity's page either way: that is where a reader looks, and the member is named in the row
|
|
710
|
+
// rather than being the reason the row disappears.
|
|
711
|
+
//
|
|
712
|
+
// Hence: match the claimed node OR any ANCESTOR of it up to and including this entity. The
|
|
713
|
+
// walk stops AT the entity, so a claim on some other object's member cannot reach this page
|
|
714
|
+
// by climbing past its own root.
|
|
715
|
+
const memberPathTo = (target: MetaData): string | undefined => {
|
|
716
|
+
const segments: string[] = [];
|
|
717
|
+
for (let n: MetaData | undefined = target; n !== undefined; n = n.parent) {
|
|
718
|
+
if (n === entity) return segments.length > 0 ? segments.reverse().join(".") : "";
|
|
719
|
+
segments.push(n.name);
|
|
720
|
+
}
|
|
721
|
+
return undefined; // not under this entity at all
|
|
722
|
+
};
|
|
723
|
+
/** Dotted-path depth. "" is the entity itself — depth 0, not the 1 a naive split gives. */
|
|
724
|
+
const memberDepth = (path: string): number => (path === "" ? 0 : path.split(".").length);
|
|
702
725
|
const claimedByMatches: UsedByDoc[] = [];
|
|
703
726
|
if (entity.subType === OBJECT_SUBTYPE_ENTITY) {
|
|
704
727
|
for (const walked of walkRequirements(root)) {
|
|
705
|
-
|
|
728
|
+
// The SHALLOWEST member path among this requirement's targets that lands on this
|
|
729
|
+
// entity: "" for an entity-grain claim, "status" for an L5 member claim. undefined
|
|
730
|
+
// when no target is under this entity.
|
|
731
|
+
let member: string | undefined;
|
|
732
|
+
let memberD = Number.POSITIVE_INFINITY;
|
|
733
|
+
for (const t of walked.targets) {
|
|
734
|
+
const path = memberPathTo(t.node);
|
|
735
|
+
if (path === undefined) continue;
|
|
736
|
+
const d = memberDepth(path);
|
|
737
|
+
// Shallowest by SEGMENT COUNT, not string length. `path.length` made "a.b" — two
|
|
738
|
+
// segments, three characters — beat "status", one segment and six, so the row
|
|
739
|
+
// named the deeper member. Ties break lexicographically so the page does not
|
|
740
|
+
// depend on the order the requirement walk happens to return targets in.
|
|
741
|
+
if (member === undefined || d < memberD || (d === memberD && path < member)) {
|
|
742
|
+
member = path;
|
|
743
|
+
memberD = d;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (member === undefined) continue;
|
|
706
747
|
const v = walked.view;
|
|
707
748
|
const level = v.level === undefined ? "" : ` · **L${v.level}**`;
|
|
708
749
|
const status = v.status === undefined ? "" : ` · status: \`${v.status}\``;
|
|
709
750
|
const statement = walked.node.attr(REQUIREMENT_ATTR_STATEMENT);
|
|
710
751
|
const said = typeof statement === "string" && statement.length > 0 ? ` — ${statement}` : "";
|
|
752
|
+
// A member-grain claim says WHICH member, so the row is actionable rather than
|
|
753
|
+
// merely present. An entity-grain claim renders exactly as before (byte-identical).
|
|
754
|
+
const on = member === "" ? "" : ` · on \`${member}\``;
|
|
711
755
|
claimedByMatches.push({
|
|
712
|
-
bullet: `\`requirement.${v.subType} ${v.path}\`${level}${status}${said}`,
|
|
756
|
+
bullet: `\`requirement.${v.subType} ${v.path}\`${level}${status}${on}${said}`,
|
|
713
757
|
});
|
|
714
758
|
}
|
|
715
759
|
}
|
|
@@ -264,9 +264,13 @@ export interface EntityDocData {
|
|
|
264
264
|
* BYTE-IDENTICAL to its pre-feature output. That is the no-churn contract:
|
|
265
265
|
* a project with no ledger, or an entity nothing claims, must see no diff.
|
|
266
266
|
*
|
|
267
|
-
* ENTITY
|
|
267
|
+
* ENTITY PAGES ONLY. Object coverage is entity-grain (`spec/capability-ledger.md`),
|
|
268
268
|
* so a claimed `object.value` / `object.projection` gets nothing here — surfacing
|
|
269
|
-
* one would imply a coverage rule the ledger does not actually have.
|
|
269
|
+
* one would imply a coverage rule the ledger does not actually have.
|
|
270
|
+
*
|
|
271
|
+
* That governs which PAGE a row lands on, not how deep the CLAIM may point: an L5
|
|
272
|
+
* claim names a member, resolves to the FIELD, and still renders on the owning
|
|
273
|
+
* entity's page with the member named in the row (`· on \`status\``). */
|
|
270
274
|
claimedBy?: UsedByDoc[];
|
|
271
275
|
/** Present-and-non-empty flag for the claimedBy section. */
|
|
272
276
|
hasClaimedBy?: boolean;
|
package/src/generators/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { callableFile, type CallableFileOpts } from "./callable-file.js";
|
|
|
13
13
|
/** @deprecated ADR-0034 — own a copy instead: `import { routesFile } from "./codegen/generators/routes"` (scaffolded by `meta init`). */
|
|
14
14
|
export { routesFile, type RoutesFileOpts } from "./routes-file.js";
|
|
15
15
|
export { routesFileHono, type RoutesFileHonoOpts } from "./routes-file-hono.js";
|
|
16
|
+
export { namesFile } from "./names-file.js";
|
|
16
17
|
/** @deprecated ADR-0034 — own a copy instead: `import { barrel } from "./codegen/generators/barrel"` (scaffolded by `meta init`). */
|
|
17
18
|
export { barrel, type BarrelOpts } from "./barrel.js";
|
|
18
19
|
/** @deprecated ADR-0021 D1 — neutral artifact owned by `meta docs` (ADR-0020); not part of the recommended `meta gen` suite. */
|
|
@@ -50,6 +51,15 @@ export { buildTemplateDocData } from "./template-doc-builder.js";
|
|
|
50
51
|
// behaviour composes its own generator instead of filing an issue here.
|
|
51
52
|
export { requirementTests } from "./requirement-tests.js";
|
|
52
53
|
export { requirementsFile } from "./requirements-file.js";
|
|
54
|
+
// The `agent` docs surface — internal engine of `meta docs --agent`, like docsFile()
|
|
55
|
+
// and apiDocsFile(). Not a `meta gen` config generator.
|
|
56
|
+
export { agentDocsFile, type AgentDocsFileOpts } from "./agent-docs-file.js";
|
|
57
|
+
export type {
|
|
58
|
+
AgentSchemaInput,
|
|
59
|
+
SchemaColumnLike,
|
|
60
|
+
SchemaTableLike,
|
|
61
|
+
SchemaViewLike,
|
|
62
|
+
} from "./agent-schema-input.js";
|
|
53
63
|
export type { RequirementRow } from "./requirements-view.js";
|
|
54
64
|
export type {
|
|
55
65
|
RequirementTestsOpts,
|