Skip to content

Commit 79c71d2

Browse files
Trumpclaude
andauthored
fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts (#14548)
* test(cli): empty the scaffold ledger first, so each repair is measured red-first `KNOWN_UNVALIDATED_SCAFFOLDS` is a shrink-only ledger carrying an anti-staleness assertion: a kind listed in it must still FAIL. Deleting the four entries BEFORE touching a template is therefore the instrument, not bookkeeping — it turns the suite red for `object` / `view` / `action` / `app` and prints each kind's refusal verbatim, which is the baseline the repairs in the next commit are measured against. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza * fix(cli): scaffold object, view, action and app in the shapes os validate accepts (#14336) Four of the seven `os generate` templates wrote artifacts the platform's own validator refuses, so an author following the documented path got a file their own toolchain rejected. Re-measured on this base through the #14087 harness: objects[0].sharingModel security-owd-unset — declares no sharingModel (OWD) views[0].list unrecognized key(s) on this list view: `pageSize` views[0] unrecognized key(s) on this view container: `type`, `objectName` actions[0].type invalid option: expected "script"|"url"|"modal"|"flow"|"api"|"form" actions[0] unrecognized key(s) on this action: `handler` apps[0].navigation expected array, received object `object` authors `sharingModel: 'private'` — not a new decision, but the one #9666 already took for the `os init` templates, emitted with the same explanation so both authoring doors agree. `view` emits a view CONTAINER rather than a flat list view: the container's slots are `list` / `form` / `listViews` / `formViews`, `type` belongs to a single view, and the object binding is `object`. The flat shape mattered past the refusal — it parses to an EMPTY container, so zero views register and the Console renders nothing. `pageSize` moves to `pagination`, the schema that declares it. `action` emits `type: 'flow'` with `target` naming the flow, which is what its `handler: { type: 'flow', target }` block was trying to express; `custom` is not an ActionType and the second handler slot was removed in protocol 17. `app` emits `navigation` as the array it is declared as, carrying one real `type: 'object'` entry instead of a `{ type: 'sidebar', items: [] }` wrapper that is not on the authoring surface. The `action` and `app` entries target the names `os g flow NAME` and `os g object NAME` write, so the scaffolds compose instead of each standing alone. `KNOWN_UNVALIDATED_SCAFFOLDS` is now empty, so the clean pin covers the whole roster. The ledger stays shrink-only: a red there is a template to fix, never a line to add. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 01e3866 commit 79c71d2

3 files changed

Lines changed: 160 additions & 42 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os generate` scaffolds `object`, `view`, `action` and `app` that `os validate` accepts (#14336)
6+
7+
Four of the seven `os generate` templates wrote artifacts the platform's own
8+
validator refused, so an author following the documented path got a file their
9+
own toolchain rejected. `#14087` fixed `flow` and recorded these four in a
10+
shrink-only ledger; this empties it. Measured through the same harness, on the
11+
same two steps `os validate` performs — schema parse, then the author-time rule
12+
registry:
13+
14+
```
15+
objects[0].sharingModel security-owd-unset — declares no sharingModel (OWD)
16+
views[0].list unrecognized key(s) on this list view: `pageSize`
17+
views[0] unrecognized key(s) on this view container: `type`, `objectName`
18+
actions[0].type invalid option: expected "script"|"url"|"modal"|"flow"|"api"|"form"
19+
actions[0] unrecognized key(s) on this action: `handler`
20+
apps[0].navigation expected array, received object
21+
```
22+
23+
**`object`** now authors `sharingModel: 'private'`. This is not a new decision:
24+
`security-owd-unset` is an error-severity rule asking for an authored org-wide
25+
default, and `#9666` already took that decision for the `os init` templates —
26+
this emits the same value with the same explanation, so both doors an author
27+
can arrive through agree.
28+
29+
**`view`** now emits a view CONTAINER instead of a flat list view. The
30+
container's slots are `list` / `form` / `listViews` / `formViews`; `type`
31+
belongs to a single view and the object binding is `object`, not `objectName`.
32+
The flat shape mattered beyond the refusal — it parses to an *empty* container,
33+
so zero views register and the Console renders nothing. `pageSize` moved to
34+
`pagination`, which is the schema that declares it.
35+
36+
**`action`** now emits `type: 'flow'` with `target` naming the flow, which is
37+
what its `handler: { type: 'flow', target }` block was trying to express.
38+
`custom` is not an `ActionType`, and the second handler slot was removed in
39+
protocol 17 so no consumer has two places to disagree about. The target is the
40+
name `os g flow NAME` writes, so the two scaffolds compose.
41+
42+
**`app`** now emits `navigation` as the array of nav items it is declared as,
43+
carrying one real `type: 'object'` entry rather than the `{ type: 'sidebar',
44+
items: [] }` wrapper, which is not on the authoring surface at all. The entry
45+
points at the object `os g object NAME` writes.
46+
47+
`KNOWN_UNVALIDATED_SCAFFOLDS` is now empty, so every generator on the roster is
48+
held to the clean pin: a template that stops validating is red on the day it
49+
lands. The ledger stays shrink-only — a red there is a template to fix, never a
50+
line to add. No other generator's output changed.

packages/cli/src/commands/generate.ts

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ const GENERATORS: Record<string, {
3333
object: {
3434
description: 'Business data object',
3535
defaultDir: 'src/objects',
36+
/**
37+
* Carries an AUTHORED `sharingModel` (#14336).
38+
*
39+
* Unlike the other three repairs on that card this one is not shape drift:
40+
* the object parsed fine and was refused one layer later, by
41+
* `security-owd-unset` — an author-time ERROR rule saying the org-wide
42+
* default must be a decision rather than an accident. So the scaffold
43+
* handed the author a file their own `os validate` rejected.
44+
*
45+
* The value is NOT a fresh decision taken here. #9666 took it once for the
46+
* `os init` templates, and this emits the SAME value with the same
47+
* explanation, so the two doors an author can arrive through agree. If
48+
* that template's value ever moves, this one moves with it.
49+
*/
3650
generate: (name: string) => `import * as Data from '@objectstack/spec/data';
3751
3852
/**
@@ -54,6 +68,13 @@ const ${toCamelCase(name)}: Data.Object = {
5468
label: 'Description',
5569
},
5670
},
71+
// Org-wide default (OWD): who can see records they don't own. 'private' is
72+
// owner-only until access is widened by a permission grant or a sharing
73+
// rule. Declaring it is required, deliberately: \`objectstack build\`
74+
// refuses an object that declares no OWD, so the baseline is always an
75+
// authored decision rather than an accident. The other values, and how to
76+
// widen access safely: https://objectstack.ai/docs/permissions/sharing-rules
77+
sharingModel: 'private',
5778
};
5879
5980
export default ${toCamelCase(name)};
@@ -63,33 +84,66 @@ export default ${toCamelCase(name)};
6384
view: {
6485
description: 'List or form view',
6586
defaultDir: 'src/views',
87+
/**
88+
* A view CONTAINER — which is what a `view` artifact is (#14336).
89+
*
90+
* `ViewSchema` is `.strict()` and its view slots are `list` / `form` /
91+
* `listViews` / `formViews`; `type` and `objectName` belong to a single
92+
* VIEW, not to the container holding it. The template used to write both
93+
* spellings at once: a flat list view's keys on the container AND a `list`
94+
* block. `defineView` has guarded the flat shape since the container was
95+
* introduced, and for a reason worth restating — a flat view parses to an
96+
* EMPTY container, so zero views register and the Console renders nothing.
97+
*
98+
* `pageSize` moved too: it is `PaginationConfigSchema`'s key, reached
99+
* through the list view's `pagination`, not a key on the list view itself.
100+
*
101+
* The object binding is `object` — the key `getViewsByObject()` reads and
102+
* the one a stack-level `views: [...]` entry needs to say which object its
103+
* views belong to. `objectName` is the spelling on the QUERY surface.
104+
*/
66105
generate: (name: string) => `import * as UI from '@objectstack/spec/ui';
67106
68107
/**
69-
* ${toTitleCase(name)} List View
108+
* ${toTitleCase(name)} Views
70109
*/
71-
const ${toCamelCase(name)}ListView: UI.View = {
72-
name: '${toSnakeCase(name)}_list',
73-
label: '${toTitleCase(name)} List',
74-
type: 'list',
75-
objectName: '${toSnakeCase(name)}',
110+
const ${toCamelCase(name)}Views: UI.View = {
111+
name: '${toSnakeCase(name)}',
112+
label: '${toTitleCase(name)}',
113+
object: '${toSnakeCase(name)}',
76114
list: {
77115
type: 'grid',
78116
columns: [
79117
{ field: 'name', width: 200 },
80118
],
81119
sort: [{ field: 'name', order: 'asc' }],
82-
pageSize: 25,
120+
pagination: { pageSize: 25 },
83121
},
84122
};
85123
86-
export default ${toCamelCase(name)}ListView;
124+
export default ${toCamelCase(name)}Views;
87125
`,
88126
},
89127

90128
action: {
91129
description: 'Button or batch action',
92130
defaultDir: 'src/actions',
131+
/**
132+
* `type` comes from `ActionType` — `script | url | modal | flow | api |
133+
* form` — and the handler binding is the single `target` slot (#14336).
134+
*
135+
* The template used to write `type: 'custom'`, which is not a member, plus
136+
* a `handler: { type, target }` block, which is not an Action key: the
137+
* `execute`/`handler` second slot was removed in protocol 17 precisely so
138+
* no consumer has two places to disagree about. What that block was trying
139+
* to express is exactly `type: 'flow'` with `target` naming the flow, so
140+
* that is what it now says — and it targets the name `os g flow NAME`
141+
* writes, so the two scaffolds compose.
142+
*
143+
* `target` is REQUIRED for every type but `script`, enforced by
144+
* `ActionSchema`'s own refinement, so this cannot drift back to an action
145+
* bound to nothing.
146+
*/
93147
generate: (name: string) => `import * as UI from '@objectstack/spec/ui';
94148
95149
/**
@@ -98,12 +152,9 @@ export default ${toCamelCase(name)}ListView;
98152
const ${toCamelCase(name)}Action: UI.Action = {
99153
name: '${toSnakeCase(name)}',
100154
label: '${toTitleCase(name)}',
101-
type: 'custom',
155+
type: 'flow',
102156
objectName: '${toSnakeCase(name)}',
103-
handler: {
104-
type: 'flow',
105-
target: '${toSnakeCase(name)}_flow',
106-
},
157+
target: '${toSnakeCase(name)}_flow',
107158
};
108159
109160
export default ${toCamelCase(name)}Action;
@@ -198,6 +249,19 @@ export default ${toCamelCase(name)}Dashboard;
198249
app: {
199250
description: 'Application navigation',
200251
defaultDir: 'src/apps',
252+
/**
253+
* `AppSchema.navigation` is an ARRAY of nav items (#14336).
254+
*
255+
* The template used to write `{ type: 'sidebar', items: [] }`. There is no
256+
* `sidebar` wrapper on the authoring surface: the array IS the sidebar
257+
* tree, and it nests through `type: 'group'` items carrying `children`.
258+
*
259+
* It scaffolds one real entry rather than an empty array, because the
260+
* entry shape is the thing an author copies to add the second one — and
261+
* because an app with no navigation renders a shell with nothing in it.
262+
* The entry points at the object `os g object NAME` writes, so the two
263+
* scaffolds compose.
264+
*/
201265
generate: (name: string) => `import * as UI from '@objectstack/spec/ui';
202266
203267
/**
@@ -206,10 +270,14 @@ export default ${toCamelCase(name)}Dashboard;
206270
const ${toCamelCase(name)}App: UI.App = {
207271
name: '${toSnakeCase(name)}_app',
208272
label: '${toTitleCase(name)}',
209-
navigation: {
210-
type: 'sidebar',
211-
items: [],
212-
},
273+
navigation: [
274+
{
275+
id: '${toSnakeCase(name)}_nav',
276+
type: 'object',
277+
label: '${toTitleCase(name)}s',
278+
objectName: '${toSnakeCase(name)}',
279+
},
280+
],
213281
};
214282
215283
export default ${toCamelCase(name)}App;

packages/cli/test/generate-scaffold-validates.test.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
* either, so a generator added tomorrow is measured by this file on the day it
4949
* lands rather than the day someone remembers to extend a hand-kept list.
5050
*
51-
* ## The ledger, and why this card did not empty it
51+
* ## The ledger, and why it is now EMPTY
5252
*
53-
* Running the roster is how it emerged that `flow` is not the only scaffold
54-
* `os validate` refuses. Measured on the same commit, same harness:
53+
* Running the roster is how it emerged that `flow` was not the only scaffold
54+
* `os validate` refuses. Measured by this harness when #14087 landed:
5555
*
5656
* object parses, then FAILS the author-time rules — `security-owd-unset`
5757
* view `views[0].list.pageSize`, and `type` / `objectName` on the container
@@ -60,20 +60,27 @@
6060
* dashboard clean
6161
* skill clean
6262
*
63-
* Those four are a separate card by triage's own fence — a census of the other
64-
* artifacts is explicitly NOT folded into #14087 — and are filed as #14336, so
65-
* this file RECORDS them instead of fixing them, in the shrink-only shape this
66-
* repo uses elsewhere
67-
* (`KNOWN_UNALIASED_TEST_IMPORTS`, the type-check debt ledger). Two properties
68-
* follow, and both are asserted below:
63+
* Those four were a separate card by triage's own fence — a census of the
64+
* other artifacts was explicitly NOT folded into #14087 — so this file first
65+
* RECORDED them, in the shrink-only shape this repo uses elsewhere
66+
* (`KNOWN_UNALIASED_TEST_IMPORTS`, the type-check debt ledger). #14336 then
67+
* repaired all four templates and deleted all four entries, which is the whole
68+
* lifecycle the ledger was built to have. Two properties held throughout, and
69+
* both are still asserted below:
6970
*
7071
* - a kind NOT in the ledger must validate clean. That is the pin.
71-
* - a kind IN the ledger must still FAIL. So whoever repairs one of them
72-
* turns this file red and deletes its entry in the same PR; the ledger
73-
* cannot quietly outlive the defect it records, and it can never grow to
74-
* cover a regression (a newly-broken kind is not in it, so it just fails).
72+
* - a kind IN the ledger must still FAIL. So whoever repairs one turns this
73+
* file red and deletes its entry in the same PR; the ledger cannot quietly
74+
* outlive the defect it records, and it can never grow to cover a
75+
* regression (a newly-broken kind is not in it, so it just fails).
7576
*
76-
* `flow` is additionally asserted to be absent from the ledger, so this card's
77+
* With the table empty, the second property has nothing to range over and the
78+
* first covers the WHOLE roster: every generator this repo ships must write a
79+
* stack `os validate` accepts, and a new one that does not is red on the day
80+
* it lands. ⛔ The table is not the place to make that red go away — it is
81+
* shrink-only, and it has already shrunk to nothing.
82+
*
83+
* `flow` is additionally asserted to be absent from the ledger, so #14087's
7784
* own defect cannot be re-admitted by adding a line to a table.
7885
*/
7986

@@ -98,18 +105,11 @@ import { BUNDLE_REQUIRE_EXTERNALS } from '../src/utils/config.js';
98105
* — see the header. Adding an entry to silence a failure is the one edit this
99106
* table must never receive; the assertions below make a stale entry fail too.
100107
*
101-
* All four are #14336. Repair the template, delete the line, same PR.
108+
* EMPTY since #14336 repaired the last four (`object` / `view` / `action` /
109+
* `app`), which means every generator on the roster is held to the clean pin.
110+
* Keep it that way: a red here is a template to fix, never a line to add.
102111
*/
103-
const KNOWN_UNVALIDATED_SCAFFOLDS: Record<string, string> = {
104-
object:
105-
'parses, then fails the author-time rules: `security-owd-unset` (no sharingModel authored).',
106-
view:
107-
'unrecognized `pageSize` on the list view, and `type` / `objectName` on the view container.',
108-
action:
109-
"`type: 'custom'` is not an Action type, and `handler` is not an Action key.",
110-
app:
111-
'`navigation` takes an array of nav items; the scaffold writes a `{ type, items }` object.',
112-
};
112+
const KNOWN_UNVALIDATED_SCAFFOLDS: Record<string, string> = {};
113113

114114
/** The name `os g <type> <name>` is invoked with throughout this file. */
115115
const STEM = 'probe_thing';

0 commit comments

Comments
 (0)