Skip to content

fix(cli): os init writes a lint script into all three scaffold templates, and a pin holds the two scaffolders equal - #16888

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-16350-init-lint-script-parity
Sep 8, 2026
Merged

fix(cli): os init writes a lint script into all three scaffold templates, and a pin holds the two scaffolders equal#16888
os-project-manager merged 2 commits into
mainfrom
claude/issue-16350-init-lint-script-parity

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16350

packages/create-objectstack/src/templates/blank/package.json declares a lint script and the CI workflow that template ships runs pnpm lint (both added by #16330). The three script maps in packages/cli/src/commands/init.ts each declared validate and no lint, so npm create objectstack and os create / os init produced projects with different script sets, and only the first could run that workflow. This adds the missing line to all three maps and pins the two scaffolders together on the scripts that workflow runs.

Clause-②: no

1. All three maps, located by symbol

The maps were found by the string validate: 'objectstack validate', — not by the card's or triage's line numbers, which rot. On this branch that string occurs exactly three times in init.ts, once in each of the app, plugin and empty templates, and lint: 'objectstack lint', is inserted directly after each one, matching the template's order.

The zero this rests on was re-taken here with its control rather than inherited: before the change objectstack lint occurred zero times in init.ts while validate occurred 14 times in the same file, so the probe demonstrably reaches those maps. After the change, lint: 'objectstack lint', occurs three times and validate: 'objectstack validate', still occurs three times.

The card's lint != validate argument was also re-run here rather than relayed, with the criterion it came with. git grep hook-body-lowering -- packages returns exactly two hits — the one import in packages/cli/src/commands/lint.ts and the rule's own test at packages/cli/src/lint/hook-body-lowering.test.ts — which is the criterion the card stated. Firing control on the same channel: git grep -c runAuthoringRules -- packages/cli/src/commands returns three files (compile.ts, lint.ts, validate.ts), so the grep channel is live. checkHookBodyLowering has exactly one call site, src/commands/lint.ts:457, so hook-body/not-lowerable is unreachable from os validate.

2. Ratchet outcome: PIN LANDED

packages/cli/test/scaffold-ci-script-parity.test.ts (22 assertions, unit tier).

The pin does not compare the whole maps, because the two sides differ on purpose in places: init.ts's app map spells start as objectstack compile && objectstack serve (with the reasoning in a comment beside it) where the template says objectstack start; its build runs objectstack compile where the template names the objectstack build alias; and the plugin / empty templates scaffold a metadata package with no server to run at all. Whole-map equality would have needed an exemption ledger and an adjudication of which spelling wins — the design exercise the escape hatch exists to avoid.

Instead the required set is derived from the workflow the on-ramp template ships: the project scripts its pnpm steps run. That is the subset on which the two scaffolders make the same promise to the same user, it is exactly the harm the card names ("a project scaffolded through init.ts that later copies the template's workflow gets a red build on a step whose script it does not have"), and on it there is currently no accepted exception — the pin carries no ledger at all, on names or on values. Nothing is transcribed: the script names come from the workflow, the expected values come from the template's own package.json. Today the derivation resolves to validate, lint and typecheck.

Two guards against a vacuous green: the harvest is asserted non-empty before it is used (a parser or regex that stopped matching would otherwise read exactly like parity), and the template is asserted to declare each harvested script before its value is compared (undefined === undefined would otherwise pass).

The other half of the loop already exists in create-objectstack, holding that workflow against the template's own package.json; it is package-local by construction and cannot see init.ts. Its failure text says so in words. This file closes the sentence from the other side.

Because the pin reads a second package, packages/create-objectstack/src/templates/blank/.github/workflows/ci.yml is declared as a cross-package test input for @objectstack/cli and mirrored into turbo.json; pnpm check:cross-package-test-inputs is green (OK: 28 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob). The blank template's package.json was already declared.

3. Ablation — the pin fails when the two sides disagree

Driven from the committed state, one map mutated on disk, restored with git checkout HEAD --, with an absolute-path trap ... EXIT INT TERM on the restore.

HEAD_BLOB=504f2858e4f49f59e75f5ffbe5c22bff499885ec
BEFORE_BLOB=504f2858e4f49f59e75f5ffbe5c22bff499885ec     (tree at HEAD before mutating)
BEFORE_lint_lines=3  ->  AFTER_lint_lines=2              (mutation proved on disk)
MUTATED_BLOB=8726c76b99f1b2c8d6ac7687ea68646c3e21cc8b    (differs from HEAD_BLOB)
ABLATION_RED_EXIT=1
RESTORED_BLOB=504f2858e4f49f59e75f5ffbe5c22bff499885ec   (equals HEAD_BLOB)
git-diff-HEAD-nameonly=''   git-status-porcelain=''
RESTORED_lint_lines=3
ABLATION_GREEN_EXIT=0

Not read as a bare non-zero exit: the red leg ran 22 tests and failed 2, both naming the mutated map —

x os init -t app > declares `lint`
  -> the on-ramp's CI workflow runs `pnpm lint`, but `os init -t app` emits no such
     script. ... expected [ 'dev', 'start', 'build', ...(2) ] to include 'lint'
x os init -t app > runs the same command as the on-ramp for `lint`
  -> expected undefined to be 'objectstack lint'

and the green leg ran the same 22 tests, 22 passed. Equal counts on both legs, so neither leg was an empty collection. The pin is unit tier by the behaviour predicate in packages/cli/vitest-tiers.ts (it neither spawns nor boots a kernel), so it guards a PR and a merge-queue entry; it is not a nightly-tier file.

The dist preflight does not apply here: the pin imports ../src/commands/init.js relatively, so the subject resolves through source, never through a package exports entry into dist/.

4. Verification

Branch merged with origin/main at a5d4e286b6 before this run; all readings below are from 9bb46c764c.

  • Gate familiesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE reconciles: 77 derived famil(ies) accounted for - 77 run, 0 NOT-MEASURED. Derived and re-derived after the merge; the family list is byte-identical to the pre-merge derivation and the STALE TREE banner is gone.
  • 74 of the 77 measured green. Three exited 3 with PREREQUISITE NOT MET and are reported as NOT MEASURED, not as passes: check:dual-build-cjs-loads ("this gate reads built output, and some package has no dist/ ... nothing was measured"), check:i18n-coverage ("Nothing was compared: 12 config(s) did lint, but a partial round cannot judge"), check:type-check-debt ("PREREQUISITE NOT MET ... nothing was measured"). Each needs a full-workspace build; CI runs them over the built tree. check:dts-closure, check:sourcemap-no-sources-content, check:i18n and check:i18n-walk-parity were re-run after the build and are green.
  • Buildpnpm --filter "@objectstack/cli^..." build exit 0, then pnpm --filter @objectstack/cli build exit 0, then pnpm --filter @objectstack/spec check:generated exit 0 (spec moved on main's side of the merge).
  • Typespnpm --filter @objectstack/cli typecheck exit 0 (that script is tsc --noEmit && pnpm check:test-typecheck). The new test really is inside the checked program: tsc -p tsconfig.test.json --listFiles names it, 1 hit, with test/init.test.ts as a positive control at 1 hit. The package's plain tsconfig.json covers neither, which is what the test-layer project exists for.
  • Testspackages/cli unit tier, vitest run --project unit: 2524 passed | 6 expected fail | 11 skipped. Five suites failed on the shared box under concurrent load and all five pass when re-run: four doctor-* suites died on Cannot find module .../packages/spec/dist/security/index.mjs while a sibling build was rewriting that dist (the file is present, and the four are green on re-run), and src/commands/datasource/envelope-unwrap.test.ts hit Hook timed out in 10000ms inside oclif's Config.load and passes alone, 11/11. None of the five is reachable from this diff. The integration tier is declared to CI: this diff touches no integration-tier file, no spawn entry point and no driver or kernel boot path.
  • Lint — repo-wide pnpm lint (eslint . --no-inline-config) exit 0, at 9bb46c764c. Not narrowed, so no narrowing needed a proof.
  • Bytespnpm check:nul-bytes green, plus a direct control-character sweep over the five changed paths with no hit.

A patch changeset for @objectstack/cli is included: the maps are program output of a published package, so this publishes.

5. Out of scope, by the card's own words

The template half (#16330), any lint-rule change, and any change to os validate's rule set are all untouched. This diff adds one string property to three object literals and one test file, plus the two declaration rows that test needs. objectstack lint accepts and refuses exactly what it did before.

6. Docs drift advisory

Re-derived on a clean checkout, and the bot's list reproduces exactly. Worktree created detached at refs/pull/16888/merge, git status --porcelain empty before the run and still empty after. node scripts/docs-audit/affected-docs.mjs --json 613bfbd3db7eb9b311ed435eb33e30d81d96641c reports its own provenance as:

computedOn = {"head":"34c540c23c9dce49ea2477d6d266e3e20f817148",
              "headParents":["613bfbd3db7eb9b311ed435eb33e30d81d96641c",
                             "9bb46c764ca124ab9357a39d4ea45a136ec14bfd"],
              "diffBase":"613bfbd3db7eb9b311ed435eb33e30d81d96641c",
              "dirty":false}

dirty: false, same head and same diff base as the advisory. 6 docs, 5 hand-written + 1 release-owned, one anchor (os init, command, read off packages/cli/src/commands/init.ts), anchorlessChanges: [] — identical to the bot's rows, so on this PR its dirty-checkout provenance defect did not move the number in either direction.

What each hand-written page actually claims

  • content/docs/deployment/cli.mdx — two passages. The os init section (its table, its os init … examples and its options list) describes templates, placement and flags, and never enumerates the emitted scripts, so it is untouched. The passage that does enumerate is the Callout at lines 570-575: "os validate, os build and os lint share one rule registry … In a scaffolded project these are wired as npm run validate and npm run build". That names three commands and then maps them onto two scripts. After this PR every os init template also wires the third, so the mapping is under-inclusive. ⚠️ This is the one page the diff moves, and it is called out below.
  • content/docs/getting-started/your-first-project.mdx — walks npm create objectstack, not os init; its os init mention is a routing Callout. Its "What was generated" tree lists package.json as a filename and never opens it, and its steps invoke npm run dev / npm run validate / npm run build one at a time rather than as a closed set. Its dependency table describes @objectstack/cli as "The os / objectstack CLI: dev, validate, build, start" — a list of CLI commands, already silent on lint before this PR and equally silent after; nothing this diff does makes it false.
  • content/docs/getting-started/examples.mdx — its os init Callout is strictly about emitted FILES ("objectstack.config.ts plus a single object … with a matching barrel"). No script claim at all.
  • content/docs/plugins/index.mdx — names os init -t plugin only to route the reader away from it; the scaffold tree and pnpm build lines shown belong to os create plugin, a scaffolder this diff does not touch.
  • content/docs/protocol/kernel/index.mdx — same shape: one routing sentence about os init -t plugin, then os create plugin's generated tree. No script claim.

content/docs/releases/v17.mdx (release-owned, read only)

Read, not edited. It mentions os init four times and every one is a historical record of a past change: driver-memory dropped from scaffolded dependencies, engines: { protocol: '^17' } stamping, the sharingModel fix, and template descriptions no longer advertising kinds they never emit. None of them states anything about the emitted script set, so nothing on that page is falsified by this diff — there is no fact here to hand back, and no docs-only PR or issue is owed for it.

Answering the advisory's own blind spot by hand

The tool anchors on identifiers; a page that states a rule by its inputs shares none with the emitter. Swept all of content/ for the short tokens, every zero paired with a control from the same vocabulary:

token hits control (same vocabulary) hits
objectstack lint 2 — (probe fired)
"lint": 1 — (probe fired)
pnpm lint 0 pnpm build 10
npm run lint 1 — (probe fired)
objectstack validate 9 — (probe fired)
"validate": 0 "build": 1
"typecheck": 0 "build": 1

Every control fires, so the three zeros are readings. The non-zero hits are all unrelated: automation/hook-bodies.mdx discusses which command lowers a handler, releases/v15.mdx is historical, and the only "lint": / "build": / npm run lint hits sit in protocol/kernel/plugin-spec.mdx, in a kernel code plugin's package.json example whose lint script is eslint src/os create plugin's world, not os init's. No page anywhere under content/ prints an os init-emitted package.json scripts block, which is why nothing shows a scripts list this PR could contradict.

The os lint section of cli.mdx: incomplete before, incomplete after, not wrong

Re-measured rather than inherited: packages/cli/src/commands/lint.ts declares json, fix, strict, score, eval, generator, eval-min, skip-i18n, include-platform, i18n-strict and default-locale, while the page documents four (--score, --fix, --strict, --json). That under-documentation reproduces, is pre-existing, and is not touched by this diff. Separately, nothing in that section describes what scripts a scaffolded project declares, and this PR changes nothing os lint accepts or refuses — so this diff makes nothing on that page wrong. The two findings are different and only neither is mine to fix here.

Verdict

One page carries a sentence this PR moves — content/docs/deployment/cli.mdx:570-575. By the repo's own criterion it is incomplete, not wrong: a reader who copies npm run validate and npm run build succeeds, and the omission is a missing member of an enumeration rather than an instruction that now fails. It is therefore recorded below rather than filed, and ⛔ not fixed here — a docs edit is not this card's surface, and it pairs naturally with the already-known os lint flag gap on the same page in one docs-only follow-up.

验收备注

Observations from the neighbourhood, deliberately not filed and not fixed here:

  • noted, not filed: init.ts spells the build script objectstack compile where the blank template spells it objectstack build. packages/cli/src/commands/build.ts is class Build extends Compile with only a description override, so the two are the same command and the difference is a spelling, not a behaviour. Picking one spelling would be a decision about which name the docs teach; 承接者: whoever next revisits the scaffolders' emission policy.
  • noted, not filed: init.ts's app map spells start as objectstack compile && objectstack serve where the template says objectstack start; init.ts carries a comment explaining the choice, so this is a recorded difference rather than drift. 承接者: 无.
  • noted, not filed: content/docs/deployment/cli.mdx lines 570-575 say "os validate, os build and os lint share one rule registry ... In a scaffolded project these are wired as npm run validate and npm run build" — three commands mapped onto two scripts. Every os init template now wires the third as well, and the blank template has since scaffold: create-objectstack creates .github/ but ships no workflow, so every scaffolded app starts with zero CI — the gates exist as npm scripts nothing ever runs #16330, so the mapping is under-inclusive for both scaffolders. Incomplete, not wrong (copying either named script still works), so it is not filed; it belongs in a docs-only follow-up together with the pre-existing os lint flag gap on the same page. 承接者: whoever takes that docs-only PR.
  • noted, not filed: content/docs/deployment/cli.mdx documents os lint's flags as --score / --fix / --strict / --json, while packages/cli/src/commands/lint.ts declares eleven including --eval, --generator, --eval-min, --skip-i18n, --include-platform, --i18n-strict and --default-locale. Re-measured here rather than inherited; pre-existing and untouched by this diff. 承接者: the same docs-only PR.
  • noted, not filed: src/commands/datasource/envelope-unwrap.test.ts has a 10-second beforeAll budget around oclif's Config.load, which is enough alone and not enough on a loaded shared box. It is a timing budget, not a defect this branch can see reproduced deterministically. 承接者: 无.

Generated by Claude Code

`packages/create-objectstack`'s blank template declares `lint` and ships a CI
workflow that runs `pnpm lint`; the three script maps in `os init` declared
`validate` and no `lint`, so the two scaffolders emitted different projects and
only one of them could run that workflow.

Adds `lint: 'objectstack lint'` after `validate` in all three maps, and a pin
that derives the required script set from the workflow the on-ramp template
ships, so the next divergence reddens instead of being discovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
@github-actions github-actions Bot added the size/m label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/cli.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/plugins/index.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/protocol/kernel/index.mdx (via os init (command, read off packages/cli/src/commands/init.ts))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 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 613bfbd3db7eb9b311ed435eb33e30d81d96641cpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 34c540c23c9dce49ea2477d6d266e3e20f817148 — the merge of head 9bb46c764ca124ab9357a39d4ea45a136ec14bfd into base 613bfbd3db7eb9b311ed435eb33e30d81d96641c, 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 34c540c23c9dce49ea2477d6d266e3e20f817148 && git checkout 34c540c23c9dce49ea2477d6d266e3e20f817148
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 613bfbd3db7eb9b311ed435eb33e30d81d96641c 9bb46c764ca124ab9357a39d4ea45a136ec14bfd && git checkout -B drift-repro 613bfbd3db7eb9b311ed435eb33e30d81d96641c && git merge --no-ff 9bb46c764ca124ab9357a39d4ea45a136ec14bfd

node scripts/docs-audit/affected-docs.mjs --json 613bfbd3db7eb9b311ed435eb33e30d81d96641c

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

scaffold: os create writes no lint script, so the two scaffolders now disagree on the generated script set

2 participants