Skip to content

fix(cli): init scaffolds pass the author-time rules dev runs (OWD + scaffold rule set) - #9736

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-9666-init-template-owd-and-scaffold-rules
Aug 18, 2026
Merged

fix(cli): init scaffolds pass the author-time rules dev runs (OWD + scaffold rule set)#9736
os-zhuang merged 4 commits into
mainfrom
claude/issue-9666-init-template-owd-and-scaffold-rules

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9666

npx @objectstack/cli@17.0.0 init my-app -t app --install succeeded, printed
✓ Scaffold validated, and the very next command in the documented on-ramp —
npm run dev — failed to compile:

✗ Author-time rules failed (1 issue)
• object "my_app_item": custom object "my_app_item" declares no sharingModel (OWD)…
rule: security-owd-unset at objects[0].sharingModel
✗ Compile failed — fix errors above before starting dev server

The CLI's own shipped template was refused by the CLI's own shipped rule set, so
the dev server never started on a freshly generated project. The defect is live
on main, not only on the published 17.0.0.

Where it actually lives

The app template is not a file under packages/create-objectstack/src/templates/
— searching there returns nothing. All three built-in templates are emitted inline
from srcFiles maps inside packages/cli/src/commands/init.ts (app, plugin,
empty), and the init-time self-test sat just below them.

Two halves, both in this PR

1. The templates author an OWD.app and plugin now declare
sharingModel: 'private' on the object they emit — the rule's own recommended
default and the ADR-0090 D1 baseline (absence is not a decision). The rule is
untouched: no allowlist, no exemption, no threshold change. The template conforms
to the rule.

2. init's self-test runs the author-time rules. It previously checked only
that the rendered config loaded and carried a manifest.namespace, which is
exactly why a template that could not compile shipped. It now runs the author-time
rule registry over the generated project and refuses to report success when any
rule rejects it.

The rule set is the build one — the same set os dev reaches when it spawns
os compile. That is deliberate and is what keeps this a shift-left rather than a
new bar: nothing that compiles today stops compiling, and a template that cannot
compile now fails at generation time, in CI, instead of at a user's first dev.
The pipeline mirrors compile.ts step for step (normalize, lower callables, Zod
parse, registry) so a rule cannot see a differently-prepared stack here than there.

✓ Scaffold validated still prints, and now names how many author-time rules
passed. Rule failures render in the same shape os validate / os build use
(all failures at once, with rule id, path and hint) and keep the existing
"this is a CLI bug, please report it" remediation line, which is precisely right
when the CLI generated metadata its own rules refuse.

Per-template sweep — the assumption that was wrong

Once init runs the rules, every built-in template has to pass them. Measured
before any fix, generating each template and running the build rule set (41
rules) over the result:

templatepre-fixpost-fix
app1 error — security-owd-unset at objects[0].sharingModel0 errors
plugin1 error — security-owd-unset at objects[0].sharingModel0 errors
empty0 errors (emits no objects)0 errors

plugin was in the same state as the reported app. It is the same defect class
— same rule, same missing field, same mechanical fix pinned by the same authored
baseline — so it is fixed here rather than deferred, and named explicitly rather
than folded in silently. security-owd-unset keys on whether an object is a system
object, not on the stack's manifest type, so a plugin's object is judged exactly
like an app's.

Control checked rather than assumed: create-objectstack's blank template
already declares sharingModel: 'private' in note.object.ts. The CLI's own
inline templates were the ones that never got that repair.

The pin

packages/cli/test/init-scaffold-authoring-rules.test.ts generates each template
and runs the author-time rules over the result, so a future template regression
fails in CI rather than at a user's first dev — the defect class, not just this
instance. It iterates TEMPLATES, so a template added later is swept the day it
lands without anyone remembering to extend the file.

To keep the pin honest it drives the command's real code path on both ends: the
file emitter (writeTemplateSrcFiles) and the self-test itself
(validateScaffold) are now shared between init.ts and the test, instead of the
test re-implementing either. A re-implementation would be free to drift, and the
drift would land in exactly the class the pin exists to catch. The pin also asserts
ruleCount is above zero, so a rule set that ran nothing cannot read as green.

Reverse verification

Predicted in writing before running, then measured: delete sharingModel from the
app template only and the suite must go red at 2 of 35, both on app, naming
security-owd-unset; plugin and empty stay green; all 28 pre-existing
init.test.ts tests stay green because none of them reads sharingModel or runs
a rule.

Measured: 33 passed / 2 failed, exactly that split, with the failure text
reproducing the incident's own message:

AssertionError: template "app" generates a project its own author-time rules refuse.
[security-owd-unset] object "my_app_item" at objects[0].sharingModel: custom object
"my_app_item" declares no sharingModel (OWD)…

The fix was committed before the ablation, and restored from that commit
afterwards (git diff HEAD empty).

Verification

All at 1b03083e2 (final head, after merging main), heavy steps serialized on
the shared verify lock:

  • pnpm --filter @objectstack/cli test — 134 files, 1458 tests passed (baseline
    1452 + the 6 new ones)
  • pnpm --filter @objectstack/cli typecheck — exit 0
  • Gate union re-derived from the actual changed paths with
    node scripts/pm/dispatch-gates.mjs, all green: check:changeset-gate-self-tests,
    check:objectui-changeset, check:cross-package-test-inputs, check:nul-bytes,
    check:query-options-erasure, check:type-check-coverage,
    check:engine-double-contract, check:where-matcher,
    check-adr-0087-registration.mjs, check-changeset-no-major.mjs,
    check-empty-changeset.mjs, check-cross-package-test-inputs.mjs,
    check-affected-docs.mjs
  • check:type-check-debt --re-measure caught the new test file adding 4 raw tsc
    errors to @objectstack/cli's TEST_DEBT (146 to 150). Fixed rather than
    ledgered: two were TS2835 (relative imports need explicit .js extensions under
    moduleResolution: NodeNext) and the other two — TS7006 and TS18046 — were
    downstream of those imports resolving to any. Adding the extensions cleared all
    four and the entry sits back at its recorded 146.

Note on scope

init still validates only when dependencies were installed (--install, or an
install that succeeded), unchanged from before: the rendered config imports
@objectstack/spec, so with no node_modules there is nothing to load and no
rules to run. Widening that is a separate change and is not attempted here.

Generated by Claude Code


Generated by Claude Code

`objectstack init my-app -t app --install` printed `Scaffold validated` and the
next documented command, `npm run dev`, failed to compile: the shipped template
declared no `sharingModel`, which the shipped `security-owd-unset` rule refuses
(ADR-0090 D1 — absence is not a decision). The CLI's own template was rejected
by the CLI's own rule set, and the developer on-ramp was dead.
Two halves, both needed:
1. The `app` and `plugin` templates now author `sharingModel: 'private'` — the
rule's own recommended default. A per-template sweep found `plugin` in the
same state as the reported `app`; `empty` emits no objects and was clean.
2. `init`'s scaffold self-test now runs the author-time rule registry instead of
only checking that the rendered config loads. It runs the `build` command's
rule set — the same set `os dev` reaches by spawning `os compile` — so this
is a shift-left, not a new bar: nothing that compiles today stops compiling,
and a template that cannot compile fails at generation instead of at a user's
first `dev`.
The loader and the file emitter are now shared with the pin test
(`validateScaffold`, `writeTemplateSrcFiles`), so the test drives the real
command path rather than a copy that could drift from it. The pin sweeps
`TEMPLATES` rather than asserting on `app`, so a template added later is covered
the day it lands.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The TEST_DEBT ratchet measured +4 raw tsc errors from the new test file: two
TS2835 (relative imports need explicit extensions under moduleResolution
NodeNext) and two more — TS7006 and TS18046 — that were downstream of those
imports resolving to `any`. Fixing the two extensions cleared all four, so the
@objectstack/cli entry sits back at its recorded 146 rather than being raised.
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e196c6a9baa8684c14e5e72545c8009f98a9ed14.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectstack.config.ts (literal, 31 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 23 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 e196c6a9baa8684c14e5e72545c8009f98a9ed14packageMentionDocs.

Which tree this was computed on

This run read content/docs from 43526bd4b94f07ad5de8f31d80682710f860bdc8 — the merge of head 1b03083e21ea14c40795bfdd2b8aea958bd85328 into base e196c6a9baa8684c14e5e72545c8009f98a9ed14, 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 43526bd4b94f07ad5de8f31d80682710f860bdc8 && git checkout 43526bd4b94f07ad5de8f31d80682710f860bdc8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e196c6a9baa8684c14e5e72545c8009f98a9ed14 1b03083e21ea14c40795bfdd2b8aea958bd85328 && git checkout -B drift-repro e196c6a9baa8684c14e5e72545c8009f98a9ed14 && git merge --no-ff 1b03083e21ea14c40795bfdd2b8aea958bd85328
node scripts/docs-audit/affected-docs.mjs --json e196c6a9baa8684c14e5e72545c8009f98a9ed14

⚠️ 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 e196c6a9baa8684c14e5e72545c8009f98a9ed14 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 18, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 18, 2026 19:45
@os-zhuang
os-zhuang enabled auto-merge August 18, 2026 19:45
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — accepted, ready + auto-merge armed. ⛔ And it refuted a "finding" I sent it mid-run.

domain:cli seat, session session_012WKSnqAaoqtW3QX7SSf1Vk. Dev returned status: done, no open questions. target:v17.

⛔ My false positive, first — I sent it and it was wrong

Mid-run I messaged this dev a "measured finding": that content/docs/getting-started/examples.mdx reproduces my_app_item while mentioning sharingModel zero times, so the fix would leave a docs page showing a shape the CLI no longer emits. It checked instead of complying, and the premise does not hold. Verified myself:

examples.mdx:362-367 <Callout type="info">
`os init` scaffolds a minimal starter, not this full tree. It emits
`objectstack.config.ts` plus a single object under
`src/objects/{namespace}_item.ts` (e.g. `my_app_item.ts`) …

That is a prose Callout naming the scaffold's file path. It reproduces no object literal, and every claim in it stays true after the fix. And your-first-project.mdx:153 already declares sharingModel: 'private' with the comment "org-wide default — an explicit, authored decision".

What went wrong in my method, precisely. I ran two greps over one file — my_app_item present, sharingModel absent — and inferred a relationship between what they matched. My control (23 doc pages mention sharingModel) proved the search term was real; it proved nothing about whether the hit was an object literal.

⇒ ⭐ A control that proves your query works does not validate the inference you draw from the hit. I have spent this whole shift demanding controls, and here I had one and was still wrong, because it validated the wrong proposition. The dev's own enumeration went further than mine — it checked _item, the draft/active/archived triple, and all four pages documenting os init, and confirmed 'Scaffold validated' / 'Validating scaffold' appear 0 times in content/docs against a control of 5 pages for 'author-time rule'.

⭐ The Zone 2 item I flagged as most likely wrong was wrong — in the useful direction

I wrote that the assumption I most expected to fail was "the app template is the only one that trips the rule set", because a second template failing would start failing init once fix (2) landed.

Falsified: plugin was identical. Same rule, same path (objects[0].sharingModel), same mechanical fix — and it is fixed here. Measured through the real runner across all three templates: pre-fix app = 1 error, plugin = 1 error, empty = 0; post-fix all three 0 errors, 0 advisories. Had only app been fixed, fix (2) would have converted a latent template defect into a hard init failure for -t plugin.

Clause-② proven by construction rather than asserted

I ruled Clause-② no on the argument that fix (2)'s rules are the same set dev already runs. The dev did not take that on my word — it traced the call chain: dev.ts:192 spawns os compilecompile.ts:177 calls authoringRulesFor('build'), and init now calls the same authoringRulesFor('build') through a pipeline mirrored from compile.ts. ⇒ init cannot reject what dev accepts — structural, not a promise. That is the right way to discharge a ruling.

⭐ The ratchet failed and was FIXED, not ledgered

check:type-check-debt --re-measure initially went red: @objectstack/cli TEST_DEBT 146 → 150. The dev did not raise the ledger entry. It root-caused all four: 2× TS2835 (relative imports need .js under moduleResolution: NodeNext) plus TS7006 and TS18046 that were downstream of those imports resolving to any. Adding the two extensions cleared all four and the entry sits back at its recorded 146.

That is exactly the line: a ratchet going up is the gate reporting your defect, not an inconvenience to be re-baselined. ⛔ Raising it would have been gate-weakening.

The surface addition, declared as asked

packages/cli/src/utils/scaffold-validate.ts (new) plus an exported writeTemplateSrcFiles. My claim said "⛔ Nothing else in packages/cli", so this needed naming — and the reason given is a good one: it lets the pin drive the command's real loader, self-test and emitter instead of re-implementing them, "which is the drift the pin exists to catch". A pin that re-implements what it pins is a pin that passes while the command rots. Accepted.

Also correct

  • Deliberately did not add a fifth door to validating-metadata.mdx's "one gate, four doors" — reasoned that init self-tests the CLI's own output through the existing build door, rather than being a new gate over authored metadata. Right call; a docs edit there would have overstated the change.
  • Reverse verification: prediction written to file before running — 2 of 35 red, both on app, naming security-owd-unset at objects[0].sharingModel, with plugin/empty/registry-pin green and all 28 pre-existing init.test.ts green. Measured exactly that split, and the direction was RED as predicted — ⛔ not "more diagnostics", ⛔ not inverted.
  • Suite 1452 → 1458, +6 = exactly the new file's 6 tests. A delta that accounts for itself.
  • content/docs/releases/ untouched, despite the drift check naming four release-owned pages.
  • NOT MEASURED, declared: no real npx @objectstack/cli init against the npm registry. Honest — the card's own reproduction was against published 17.0.0, and this branch cannot be published to test it.

#9737 filed for the template descriptions advertising views/actions/extensions no template emits — needs a product call, correctly not fixed here. ⛔ Not graded by this seat.


Generated by Claude Code

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 init -t app scaffold does not compile on CLI 17.0.0 — the template's own object trips the security-owd-unset author-time rule

2 participants

@os-zhuang@claude