From fd6bf299c400c570247f045a62ef4bc8ab530e4a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 09:30:33 +0000 Subject: [PATCH 1/2] test(cli): empty the scaffold ledger first, so each repair is measured red-first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- packages/cli/test/generate-scaffold-validates.test.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/cli/test/generate-scaffold-validates.test.ts b/packages/cli/test/generate-scaffold-validates.test.ts index f9ab230ecc..50350c431e 100644 --- a/packages/cli/test/generate-scaffold-validates.test.ts +++ b/packages/cli/test/generate-scaffold-validates.test.ts @@ -100,16 +100,7 @@ import { BUNDLE_REQUIRE_EXTERNALS } from '../src/utils/config.js'; * * All four are #14336. Repair the template, delete the line, same PR. */ -const KNOWN_UNVALIDATED_SCAFFOLDS: Record = { - object: - 'parses, then fails the author-time rules: `security-owd-unset` (no sharingModel authored).', - view: - 'unrecognized `pageSize` on the list view, and `type` / `objectName` on the view container.', - action: - "`type: 'custom'` is not an Action type, and `handler` is not an Action key.", - app: - '`navigation` takes an array of nav items; the scaffold writes a `{ type, items }` object.', -}; +const KNOWN_UNVALIDATED_SCAFFOLDS: Record = {}; /** The name `os g ` is invoked with throughout this file. */ const STEM = 'probe_thing'; From 5b3d7ee61aa2088e34eaf6d234d0b49d7aceb1f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 09:44:22 +0000 Subject: [PATCH 2/2] fix(cli): scaffold object, view, action and app in the shapes os validate accepts (#14336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- ...affolds-object-view-action-app-validate.md | 50 +++++++++ packages/cli/src/commands/generate.ts | 102 +++++++++++++++--- .../test/generate-scaffold-validates.test.ts | 39 ++++--- 3 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 .changeset/generate-scaffolds-object-view-action-app-validate.md diff --git a/.changeset/generate-scaffolds-object-view-action-app-validate.md b/.changeset/generate-scaffolds-object-view-action-app-validate.md new file mode 100644 index 0000000000..dec0cda365 --- /dev/null +++ b/.changeset/generate-scaffolds-object-view-action-app-validate.md @@ -0,0 +1,50 @@ +--- +"@objectstack/cli": patch +--- + +fix(cli): `os generate` scaffolds `object`, `view`, `action` and `app` that `os validate` accepts (#14336) + +Four of the seven `os generate` templates wrote artifacts the platform's own +validator refused, so an author following the documented path got a file their +own toolchain rejected. `#14087` fixed `flow` and recorded these four in a +shrink-only ledger; this empties it. Measured through the same harness, on the +same two steps `os validate` performs — schema parse, then the author-time rule +registry: + +``` +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`** now authors `sharingModel: 'private'`. This is not a new decision: +`security-owd-unset` is an error-severity rule asking for an authored org-wide +default, and `#9666` already took that decision for the `os init` templates — +this emits the same value with the same explanation, so both doors an author +can arrive through agree. + +**`view`** now emits a view CONTAINER instead of 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`, not `objectName`. +The flat shape mattered beyond the refusal — it parses to an *empty* container, +so zero views register and the Console renders nothing. `pageSize` moved to +`pagination`, which is the schema that declares it. + +**`action`** now 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 so no consumer has two places to disagree about. The target is the +name `os g flow NAME` writes, so the two scaffolds compose. + +**`app`** now emits `navigation` as the array of nav items it is declared as, +carrying one real `type: 'object'` entry rather than the `{ type: 'sidebar', +items: [] }` wrapper, which is not on the authoring surface at all. The entry +points at the object `os g object NAME` writes. + +`KNOWN_UNVALIDATED_SCAFFOLDS` is now empty, so every generator on the roster is +held to the clean pin: a template that stops validating is red on the day it +lands. The ledger stays shrink-only — a red there is a template to fix, never a +line to add. No other generator's output changed. diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index 3dae193374..7a93d0f641 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -33,6 +33,20 @@ const GENERATORS: Record `import * as Data from '@objectstack/spec/data'; /** @@ -54,6 +68,13 @@ const ${toCamelCase(name)}: Data.Object = { label: 'Description', }, }, + // Org-wide default (OWD): who can see records they don't own. 'private' is + // owner-only until access is widened by a permission grant or a sharing + // rule. Declaring it is required, deliberately: \`objectstack build\` + // refuses an object that declares no OWD, so the baseline is always an + // authored decision rather than an accident. The other values, and how to + // widen access safely: https://objectstack.ai/docs/permissions/sharing-rules + sharingModel: 'private', }; export default ${toCamelCase(name)}; @@ -63,33 +84,66 @@ export default ${toCamelCase(name)}; view: { description: 'List or form view', defaultDir: 'src/views', + /** + * A view CONTAINER — which is what a `view` artifact is (#14336). + * + * `ViewSchema` is `.strict()` and its view slots are `list` / `form` / + * `listViews` / `formViews`; `type` and `objectName` belong to a single + * VIEW, not to the container holding it. The template used to write both + * spellings at once: a flat list view's keys on the container AND a `list` + * block. `defineView` has guarded the flat shape since the container was + * introduced, and for a reason worth restating — a flat view parses to an + * EMPTY container, so zero views register and the Console renders nothing. + * + * `pageSize` moved too: it is `PaginationConfigSchema`'s key, reached + * through the list view's `pagination`, not a key on the list view itself. + * + * The object binding is `object` — the key `getViewsByObject()` reads and + * the one a stack-level `views: [...]` entry needs to say which object its + * views belong to. `objectName` is the spelling on the QUERY surface. + */ generate: (name: string) => `import * as UI from '@objectstack/spec/ui'; /** - * ${toTitleCase(name)} List View + * ${toTitleCase(name)} Views */ -const ${toCamelCase(name)}ListView: UI.View = { - name: '${toSnakeCase(name)}_list', - label: '${toTitleCase(name)} List', - type: 'list', - objectName: '${toSnakeCase(name)}', +const ${toCamelCase(name)}Views: UI.View = { + name: '${toSnakeCase(name)}', + label: '${toTitleCase(name)}', + object: '${toSnakeCase(name)}', list: { type: 'grid', columns: [ { field: 'name', width: 200 }, ], sort: [{ field: 'name', order: 'asc' }], - pageSize: 25, + pagination: { pageSize: 25 }, }, }; -export default ${toCamelCase(name)}ListView; +export default ${toCamelCase(name)}Views; `, }, action: { description: 'Button or batch action', defaultDir: 'src/actions', + /** + * `type` comes from `ActionType` — `script | url | modal | flow | api | + * form` — and the handler binding is the single `target` slot (#14336). + * + * The template used to write `type: 'custom'`, which is not a member, plus + * a `handler: { type, target }` block, which is not an Action key: the + * `execute`/`handler` second slot was removed in protocol 17 precisely so + * no consumer has two places to disagree about. What that block was trying + * to express is exactly `type: 'flow'` with `target` naming the flow, so + * that is what it now says — and it targets the name `os g flow NAME` + * writes, so the two scaffolds compose. + * + * `target` is REQUIRED for every type but `script`, enforced by + * `ActionSchema`'s own refinement, so this cannot drift back to an action + * bound to nothing. + */ generate: (name: string) => `import * as UI from '@objectstack/spec/ui'; /** @@ -98,12 +152,9 @@ export default ${toCamelCase(name)}ListView; const ${toCamelCase(name)}Action: UI.Action = { name: '${toSnakeCase(name)}', label: '${toTitleCase(name)}', - type: 'custom', + type: 'flow', objectName: '${toSnakeCase(name)}', - handler: { - type: 'flow', - target: '${toSnakeCase(name)}_flow', - }, + target: '${toSnakeCase(name)}_flow', }; export default ${toCamelCase(name)}Action; @@ -198,6 +249,19 @@ export default ${toCamelCase(name)}Dashboard; app: { description: 'Application navigation', defaultDir: 'src/apps', + /** + * `AppSchema.navigation` is an ARRAY of nav items (#14336). + * + * The template used to write `{ type: 'sidebar', items: [] }`. There is no + * `sidebar` wrapper on the authoring surface: the array IS the sidebar + * tree, and it nests through `type: 'group'` items carrying `children`. + * + * It scaffolds one real entry rather than an empty array, because the + * entry shape is the thing an author copies to add the second one — and + * because an app with no navigation renders a shell with nothing in it. + * The entry points at the object `os g object NAME` writes, so the two + * scaffolds compose. + */ generate: (name: string) => `import * as UI from '@objectstack/spec/ui'; /** @@ -206,10 +270,14 @@ export default ${toCamelCase(name)}Dashboard; const ${toCamelCase(name)}App: UI.App = { name: '${toSnakeCase(name)}_app', label: '${toTitleCase(name)}', - navigation: { - type: 'sidebar', - items: [], - }, + navigation: [ + { + id: '${toSnakeCase(name)}_nav', + type: 'object', + label: '${toTitleCase(name)}s', + objectName: '${toSnakeCase(name)}', + }, + ], }; export default ${toCamelCase(name)}App; diff --git a/packages/cli/test/generate-scaffold-validates.test.ts b/packages/cli/test/generate-scaffold-validates.test.ts index 50350c431e..2ba5543dfb 100644 --- a/packages/cli/test/generate-scaffold-validates.test.ts +++ b/packages/cli/test/generate-scaffold-validates.test.ts @@ -48,10 +48,10 @@ * either, so a generator added tomorrow is measured by this file on the day it * lands rather than the day someone remembers to extend a hand-kept list. * - * ## The ledger, and why this card did not empty it + * ## The ledger, and why it is now EMPTY * - * Running the roster is how it emerged that `flow` is not the only scaffold - * `os validate` refuses. Measured on the same commit, same harness: + * Running the roster is how it emerged that `flow` was not the only scaffold + * `os validate` refuses. Measured by this harness when #14087 landed: * * object parses, then FAILS the author-time rules — `security-owd-unset` * view `views[0].list.pageSize`, and `type` / `objectName` on the container @@ -60,20 +60,27 @@ * dashboard clean * skill clean * - * Those four are a separate card by triage's own fence — a census of the other - * artifacts is explicitly NOT folded into #14087 — and are filed as #14336, so - * this file RECORDS them instead of fixing them, in the shrink-only shape this - * repo uses elsewhere - * (`KNOWN_UNALIASED_TEST_IMPORTS`, the type-check debt ledger). Two properties - * follow, and both are asserted below: + * Those four were a separate card by triage's own fence — a census of the + * other artifacts was explicitly NOT folded into #14087 — so this file first + * RECORDED them, in the shrink-only shape this repo uses elsewhere + * (`KNOWN_UNALIASED_TEST_IMPORTS`, the type-check debt ledger). #14336 then + * repaired all four templates and deleted all four entries, which is the whole + * lifecycle the ledger was built to have. Two properties held throughout, and + * both are still asserted below: * * - a kind NOT in the ledger must validate clean. That is the pin. - * - a kind IN the ledger must still FAIL. So whoever repairs one of them - * turns this file red and deletes its entry in the same PR; the ledger - * cannot quietly outlive the defect it records, and it can never grow to - * cover a regression (a newly-broken kind is not in it, so it just fails). + * - a kind IN the ledger must still FAIL. So whoever repairs one turns this + * file red and deletes its entry in the same PR; the ledger cannot quietly + * outlive the defect it records, and it can never grow to cover a + * regression (a newly-broken kind is not in it, so it just fails). * - * `flow` is additionally asserted to be absent from the ledger, so this card's + * With the table empty, the second property has nothing to range over and the + * first covers the WHOLE roster: every generator this repo ships must write a + * stack `os validate` accepts, and a new one that does not is red on the day + * it lands. ⛔ The table is not the place to make that red go away — it is + * shrink-only, and it has already shrunk to nothing. + * + * `flow` is additionally asserted to be absent from the ledger, so #14087's * own defect cannot be re-admitted by adding a line to a table. */ @@ -98,7 +105,9 @@ import { BUNDLE_REQUIRE_EXTERNALS } from '../src/utils/config.js'; * — see the header. Adding an entry to silence a failure is the one edit this * table must never receive; the assertions below make a stale entry fail too. * - * All four are #14336. Repair the template, delete the line, same PR. + * EMPTY since #14336 repaired the last four (`object` / `view` / `action` / + * `app`), which means every generator on the roster is held to the clean pin. + * Keep it that way: a red here is a template to fix, never a line to add. */ const KNOWN_UNVALIDATED_SCAFFOLDS: Record = {};