fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts - #14548

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate
Sep 2, 2026
Merged

fix(cli): os generate scaffolds object, view, action and app in the shapes os validate accepts#14548
os-trump merged 3 commits into
mainfrom
claude/issue-14336-generate-scaffolds-validate

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#14336

Four of the seven os generate templates wrote artifacts the platform's own validator refuses. The generator is the path the docs point at, and a .strict() refusal enumerates what is allowed rather than saying where a key moved to — so an author following the documented path got a file their own os validate rejected, with an error that did not say where to put the key instead.

#14087 fixed flow and recorded these four in KNOWN_UNVALIDATED_SCAFFOLDS, a shrink-only ledger with an anti-staleness assertion: a ledgered kind must still FAIL. This PR repairs all four templates and deletes all four entries, which empties the ledger.

The four ledger deletions

All four lines are removed from KNOWN_UNVALIDATED_SCAFFOLDS in packages/cli/test/generate-scaffold-validates.test.ts, leaving the table empty. ⛔ No kind was added to it.

deleted entryrepaired by
object — "parses, then fails the author-time rules: security-owd-unset (no sharingModel authored)."an authored sharingModel: 'private'
view — "unrecognized pageSize on the list view, and type / objectName on the view container."a view CONTAINER, pageSize under pagination
action — "type: 'custom' is not an Action type, and handler is not an Action key."type: 'flow' + target
app — "navigation takes an array of nav items; the scaffold writes a { type, items } object."navigation as an array of nav items

Because the table is empty, the harness's clean pin now ranges over the whole roster: every generator must write a stack os validate accepts, and a template that stops validating is red on the day it lands.

Per kind — the refusal before, the shape after

Refusals re-measured on this branch's base through the #14087 harness (schema parse after normalizeStackInput and the unknown-key lints, then runAuthoringRules('validate')), not copied from the card.

object — the one that is not shape drift

Before:

security-owd-unset at objects[0].sharingModel: custom object "probe_thing" declares no
sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline
must be an authored decision, not an accident — this is the exact shape of the
leave_request incident (objectui#2348).

The object parses fine and is refused one layer later, by an error-severity author-time rule asking for a security decision. Per triage, this is not a new authoring decision: #9666 already took it for the os init templates. Measured on this base, both os init sites emit sharingModel: 'private' — so this emits the same value, with the same comment block naming ADR-0090 and the sharing-rules doc, and the two doors an author can arrive through agree. (Had that template emitted none, or a different value, the disagreement would have been reported instead of a third answer invented.)

After:

// 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. ...sharingModel: 'private',

view — a container, not a flat list view

Before:

views.0.list: Unrecognized key(s) on this list view: `pageSize`.
views.0: Unrecognized key(s) on this view container: `type`, `objectName`.
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })` ... The container's own keys are
`list`, `form`, `listViews`, `formViews`.

The template wrote both spellings at once — a flat list view's keys on the container and a list block. That mattered past the refusal: a flat view parses to an empty container, so zero views register and the Console renders nothing (which is why defineView has carried a bespoke guard for it). pageSize is PaginationConfigSchema's key, reached through the list view's pagination. The object binding is object — the key getViewsByObject() reads; objectName is the spelling on the query surface.

After: name / label / object on the container, one list holding type / columns / sort / pagination: { pageSize: 25 }. The const is renamed to NAMEViews and its JSDoc follows, because the artifact is a container now, not "a list view".

action — a declared type and the single target slot

Before:

actions.0.type: Invalid option: expected one of "script"|"url"|"modal"|"flow"|"api"|"form"
actions.0: Unrecognized key(s) on this action: `handler`.

custom is not an ActionType, and handler is not an Action key — the second handler slot was removed in protocol 17 precisely so no consumer has two places to disagree about. What the old handler: { type: 'flow', target: 'NAME_flow' } block was trying to express is exactly type: 'flow' with target naming the flow, so that is what it now says. ActionSchema's own refinement makes target required for every type but script, so this cannot drift back to an action bound to nothing.

After: type: 'flow', target: 'NAME_flow' — the name os g flow NAME writes.

app — navigation is an array

Before:

apps.0.navigation: Invalid input: expected array, received object

AppSchema.navigation is z.array(NavigationItemSchema).optional(). There is no sidebar wrapper on the authoring surface: the array is the sidebar tree, and it nests through type: 'group' items carrying children.

After: one real type: 'object' entry (id / type / label / objectName) rather than an empty array — the entry shape is the thing an author copies to add the second one, and an app with no navigation renders a shell with nothing in it. It points at the object os g object NAME writes.

The action and app repairs deliberately target the names the sibling generators write, so the scaffolds compose instead of each standing alone.

Scope

packages/spec was read but not changed — none of the four needed a spec change; each repair moves the scaffold onto a shape the schema already documents. dashboard and skill were clean and stay clean; the flow template (#14087) is untouched, and the harness still asserts flow is absent from the ledger. The migration codegen region of generate.ts is untouched.

Verification

All runs serialized through this container's shared verify lock. Every exit code captured with the redirect-then-capture form, and each gate's verdict read from the line the gate itself prints — never from a bare $? behind a pipe.

Red-first, then green (the ledger is the instrument, so it was emptied before any template was touched — first commit on the branch):

red-first (ledger emptied, templates untouched) Tests 4 failed | 14 passed (18)
x os g 'object' / 'view' / 'action' / 'app' writes a stack os validate accepts
after repairs Tests 18 passed (18)

The four reds printed exactly the refusals quoted per kind above — re-measured here, not copied from the card.

Ablation — one refused shape re-introduced on disk (type: 'custom' back into the action template), predicted direction stated before running: red for action only.

HEAD_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e
MUTATION ON DISK: injected(type:'custom')=1 remaining(type:'flow')=0
MUT_BLOB=4cd7d698d7af8efe1831d9959c11aadc72dc44b4 (differs from HEAD blob)
ABLATION RESULT: Tests 1 failed | 17 passed (18) -- the failure is `os g 'action'`
RES_BLOB=7a93d0f64124640be59b2d95c2718504c73f309e (byte-identical to HEAD)
RESTORE PROVEN: hash==HEAD_BLOB, git diff HEAD empty, type:'flow' back=1, type:'custom' gone=0

The mutation was proven on disk by grep counts of both the injected and the removed text plus a blob-hash comparison — not by the editor's exit code — and the restore leg got the same treatment. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild leg was needed and none was skipped: the harness reaches the template through a relative ../src/commands/generate.js import that vitest transpiles from source, so no dist/ sits between the edit and the measurement.

Gate union — re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (the first derivation printed STALE TREE, so origin/main was merged in and it was derived again; the second run printed no staleness warning and the union was unchanged). It reports the change set as exactly the three files this PR touches. All 37 were run:

32 passed
5 NOT MEASURED - each printing its own prerequisite verdict, none a finding:
exit 3 node scripts/check-test-completeness.mjs
exit 3 node scripts/pm/check-half-states.mjs
exit 3 pnpm check:dual-build-cjs-loads
exit 1 pnpm check:i18n
exit 1 pnpm check:i18n-coverage

The five are all "nothing was measured" by the gate's own words, and all name the same missing prerequisite — a whole-repo build, or a saved turbo run test log, i.e. inputs CI has and a single dev container does not:

  • check:dual-build-cjs-loads — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured."
  • check:test-completeness — "Nothing was measured: this gate exited before parsing a single summary line ... ⛔ It is NOT a finding."
  • pm/check-half-states — "Treat this exit as an unread instrument, never as a quiet board."
  • check:i18n / check:i18n-coverage — "Nothing was checked / Nothing was measured", both naming the same ten-package build closure. Neither can be moved by this diff: it adds no label to any authored artifact and touches no translation bundle — the scaffold labels live inside a template string, not in metadata any extract config loads.

⛔ Reported as NOT MEASURED, never folded into the green count. Notably green and directly relevant to the test-file edit: check:type-check-debt, check:type-check-coverage, check:type-source-resolution and check:test-source-alias all pass, so emptying the ledger moved no type-check debt.

Package-level, on the final tree 467e26e9cd:

pnpm --filter @objectstack/cli exec vitest run test/generate-scaffold-validates.test.ts
-> Test Files 1 passed, Tests 18 passed (18)
pnpm --filter @objectstack/cli typecheck -> exit 0
pnpm check:nul-bytes -> OK (7922 text files scanned, no raw control bytes)
pnpm --filter @objectstack/cli test -> exit 0; Test Files 228 passed, Tests 2621 passed
pnpm lint (whole repo, eslint . --no-inline-config)
-> exit 0; 5709 files linted, 0 errors, 0 warnings

⚠️ One honest caveat on the typecheck, so it is not read as covering more than it does: packages/cli/tsconfig.json has include: ["src"], so pnpm --filter @objectstack/cli typecheck type-checks the repaired generator but not the edited test file. What covers the test tree here is the type-check debt/coverage gate family, and those pass unchanged (above). Recorded as a boundary, not as coverage.

Notes for review

  • The object repair was the one place a wrong answer would have been invented rather than measured. Triage's stop-and-report condition — os init emitting no sharingModel, or a different one — did not fire: both sites emit 'private', so this PR emits 'private' and copies their comment block verbatim rather than writing a new rationale.
  • ⛔ Nothing was added to KNOWN_UNVALIDATED_SCAFFOLDS; it is now empty. The anti-staleness assertion and the flow-absent assertion are both retained.
  • packages/spec/** is unchanged. No scaffold needed a spec change; if one had, this would have stopped and reported instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…d 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
…date 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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command, read off packages/cli/src/commands/generate.ts))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages), sharingModel (symbol, 39 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

Which tree this was computed on

This run read content/docs from 100d0cf12fb102526c31f8e8db5e67461485a754 — the merge of head 467e26e9cdb5735f15101bdace858dd5b860fcc5 into base 4a378701156050de0608f728d479c0be038ea836, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 100d0cf12fb102526c31f8e8db5e67461485a754 && git checkout 100d0cf12fb102526c31f8e8db5e67461485a754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a378701156050de0608f728d479c0be038ea836 467e26e9cdb5735f15101bdace858dd5b860fcc5 && git checkout -B drift-repro 4a378701156050de0608f728d479c0be038ea836 && git merge --no-ff 467e26e9cdb5735f15101bdace858dd5b860fcc5
node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4a378701156050de0608f728d479c0be038ea836 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trump
os-trump marked this pull request as ready for review September 2, 2026 14:18
@os-trump
os-trump added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 79c71d2Sep 2, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-14336-generate-scaffolds-validate branch September 2, 2026 14:57
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os generate scaffolds four more artifact kinds os validate refuses — object, view, action and app

2 participants

@os-trump@claude