Skip to content

docs(cli): os create plugin standalone output is unscoped and private, not a publishable @objectstack package - #17547

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-17103-create-plugin-docs-unscoped
Sep 10, 2026
Merged

os-justin merged 2 commits into
mainfrom
claude/issue-17103-create-plugin-docs-unscoped

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

os create plugin's standalone output is an unscoped, private package named plugin-NAME. Three hand-written statements in content/docs/ still promised the pre-ruling behaviour — a publishable @objectstack/plugin-NAME — so a reader who followed them is refused by npm publish at the last step of a documented workflow, with the doc as the only thing that told them to try.

Closes #17103

The emitted name, re-derived from the generator

Read out of packages/cli/src/commands/create.ts on origin/main, never copied from the ruling text or from the card:

// :153  — standalone is the default placement
export const DEFAULT_PLACEMENT: ScaffoldPlacement = 'standalone';

// :313-315  — the emitted directory name
function pluginDirName(name: string): string {
  return `plugin-${name}`;
}

// :357-360  — the emitted PACKAGE name, COMPOSED from the directory name
function pluginPackageName(placement: ScaffoldPlacement, name: string): string {
  return placement === 'in-repo'
    ? `@objectstack/${pluginDirName(name)}`
    : pluginDirName(name);
}

// :381  — the structural half: npm publish refuses a private manifest
...(standalone ? { private: true } : {}),

// :668  — how placement is chosen
const placement: ScaffoldPlacement = flags['in-repo'] ? 'in-repo' : DEFAULT_PLACEMENT;

⇒ standalone, i.e. the default: plugin-NAME plus "private": true. --in-repo: @objectstack/plugin-NAME, publishable, landing under packages/plugins/.

Cross-checked against this package's own literal pins in packages/cli/test/create.test.ts:260-281. packages/cli is read-only for this change — the generator is the evidence, never the target.

What changed — three statements, not two

The card names two. A third turned up at source and is the same claim in the same section, so it is corrected here rather than left to falsify the fix:

Site Was Now
content/docs/deployment/cli.mdx scaffolder-routing table, Publishable? cell "Yes — a publishable @objectstack/plugin-NAME package" "Only with --in-repo — the default standalone emission is an unscoped plugin-NAME marked private: true"
content/docs/deployment/cli.mdx, #### os create intro prose "the Plugin contract, built by tsc, publishable" the word dropped; the section now states the emitted name, the private flag and why
content/docs/plugins/index.mdx "Which artifact this page means" callout "built by tsc, published as @objectstack/plugin-NAME" the default unscoped/private name, with the scoped one attached to --in-repo

The Publishable? column — what it now says, and why it still discriminates

The card's third item is a consequence, not a false claim: once os create plugin is also private: true, a column whose two cells both read "No" stops doing the one job that table exists for. The decision taken here is to keep the column and make each cell say what is actually true of that scaffolder, which restores the discrimination on a real axis rather than a stale one:

  • os init NAME -t plugin → "No — the emitted package.json is private: true, and no flag lifts it". Measured, not assumed: renderScaffoldPackageJson in packages/cli/src/commands/init.ts:409-425 writes a bare, unscoped name and an unconditional private: true, with no placement flag anywhere in that command.
  • os create plugin NAME → "Only with --in-repo", with both halves spelled out.

So the two cells give different answers, and the difference is the one a reader needs: one scaffolder can never be published, the other can be, under a flag that is for platform work inside this monorepo.

The card's executable criterion, run as a two-legged proof

The criterion: after the fix, no hand-written page states that os create plugin's standalone output is publishable or @objectstack-scoped, while the --in-repo behaviour is still described accurately. The same sweep was run over both trees, scoped to content/docs/ and excluding the release-owned content/docs/releases/.

Leg A — origin/main (must find them). Placeholder-scoped spelling: 2 lines, content/docs/deployment/cli.mdx:106 and content/docs/plugins/index.mdx:81. Publishability claims on pages naming os create plugin: 4 lines, adding cli.mdx:1337 (the third statement) and the column header.

Leg B — this branch (must not). Placeholder-scoped spelling: 3 lines, and reading each one, every occurrence is attached to --in-repo: cli.mdx:106 ("--in-repo emits a publishable …"), cli.mdx:1371-1372 ("Only --in-repo below emits a scoped, publishable …"), plugins/index.mdx:82 ("… under --in-repo"). No line states the standalone output is publishable or scoped.

The --in-repo half survives, and is described accurately. --in-repo is named on 8 lines across 4 pages after the change (5 lines / 4 pages before — the fix adds descriptions of it, it removes none), and both files' --in-repo prose is unchanged except where it now carries the scoped name the standalone emission gave up.

Controls. Positive: @objectstack/plugin-auth is on 30 lines in both legs, unchanged. Fabricated: @objectstack/plugin-zzznotarealplugin and os create widget each return 0 in both legs, so the instrument can answer zero.

What was deliberately NOT touched

@objectstack/plugin- occurs 89 times in content/docs/ outside releases/ on this branch; 86 of those are genuinely first-party, genuinely published plugins and are untouched. That is a reading, not an assumption: the 11 distinct names behind those 86 occurrences — plugin-auth, plugin-security, plugin-audit, plugin-hono-server, plugin-email, plugin-sharing, plugin-approvals, plugin-reports, plugin-pinyin-search, plugin-dev, plugin-webhooks — were each resolved to a real packages/plugins/plugin-*/package.json whose name matches and which carries no private flag. A blanket find-and-replace on that prefix would have been a much larger defect than the one being fixed.

Read and left alone, as the card asked: content/docs/protocol/kernel/index.mdx:387-408 describes the scaffold and already prints ./plugin-NAME/ unscoped, and its --in-repo sentence is about workspace:* dependencies, not publishability — accurate. content/docs/getting-started/your-first-project.mdx:75 only routes between the two scaffolders by artifact and makes no scope or publish claim — accurate. content/docs/deployment/cli.mdx:1341 already read os create plugin analytics # Create ./plugin-analytics and served as a positive control that the correct spelling was already reachable in this page.

Verification

  • 41 of 41 derived gate families run, every one exit 0, reconciled with node scripts/pm/dispatch-gates.mjs --ran at 36d9b307: "41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED". Exit codes were captured by redirect-then-$?, never through a pipe.
  • Five of those first answered PREREQUISITE NOT MET (three at exit 3, two at exit 1 with the same substance) because the tree was unbuilt. Nothing was read from those runs; @objectstack/spec, @objectstack/lint, @objectstack/formula, @objectstack/client and @objectstack/client-react were built and all five re-run to a real exit 0.
  • pnpm lint — the whole repository, eslint . --no-inline-config, exit 0. Not narrowed.
  • Tree freshness: derived once before merging origin/main and once after; both lists are byte-identical at 41 commands. The post-merge derivation is the one reconciled.

Changeset

skip-changeset, and it is measured rather than assumed. No package's files[] ships content/docs/ — checked across all 70 workspace packages that declare a files[], with dist as the positive control (70/70 match it) and a fabricated term as the negative (0). The docs site itself, apps/docs, is private: true. So this diff publishes nothing from any released package, which is exactly what that label is for.

Clause-②: no

Contract-text: packages/cli/src/commands/create.ts:313-315, :357-360 and :381, quoted above. Correcting prose to match code that already shipped is a pull-back onto already-declared behaviour: it widens no accept set, adds no key to any published payload and exports no symbol.

Scope

Two files, both hand-written docs. content/docs/releases/ is release-owned and untouched. packages/cli is untouched. Numbers cited for context only, with no verb beside them: the ruling is on issue #15530, its code half landed as PR #17096, and issue #16140 was considered and rejected as a neighbour by the card.

Generated by Claude Code


Generated by Claude Code

The #15530 ruling landed in PR #17096: the standalone default emission of
`os create plugin` names the package `plugin-<name>` — unscoped — and writes
`"private": true`. `--in-repo` is unchanged and still emits a publishable
`@objectstack/plugin-<name>` under `packages/plugins/`.

Three hand-written statements still promised the pre-ruling behaviour and sent
a reader to a `npm publish` that npm refuses:

- `content/docs/deployment/cli.mdx` scaffolder-routing table, "Publishable?"
- `content/docs/deployment/cli.mdx` `#### os create` intro prose ("publishable")
- `content/docs/plugins/index.mdx` "Which artifact this page means" callout

All three are re-derived from the generator (`packages/cli/src/commands/create.ts`
`pluginDirName` / `pluginPackageName` / the standalone `private: true`), not from
the ruling text.

The "Publishable?" column keeps discriminating between the two scaffolders: the
`os init -t plugin` cell is "No, and no flag lifts it"; the `os create plugin`
cell is "Only with --in-repo".

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 10, 2026 — with Claude
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review — ACCEPT (domain:cli dispatch seat, default judgment tier)

Head reviewed: 36d9b30772, compared once against the PR object's head.sha — identical, and all 31 check runs carry it. Every reading below is taken against GitHub and origin/main, ⛔ none from the report.

① Derived judgments

  • Scope, from the changed-file list. 2 files: content/docs/deployment/cli.mdx, content/docs/plugins/index.mdx, +16/−6. ⛔ packages/cli untouched; ⛔ content/docs/releases/ untouched — the two bounds the card set.
  • Governed surface: does not apply. 0 hits across docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md. ⚠️ content/docs/** is deliberately not governed.
  • Clause-② gate, both legs unhit. Path leg: 0 files under packages/spec/src/**. Declaration leg: no, carrying its Contract-text:. Machine predicate ⇒ exit 0, 「both carriers agree, and its diff carries no widening tell」 — with the gate's own caveat kept: 「A tell is not a proof and its absence is not one either」.
  • The emitted name was re-derived from the generator, which is the instruction triage put above all others. DEFAULT_PLACEMENT is standalone; pluginDirName returns the plugin- template literal; pluginPackageName composes @objectstack/ onto it only for in-repo; private: true is added for standalone only. ⇒ the correction is anchored in code, ⛔ not transcribed from the ruling.
  • CI, both reads. 31 check runs collapsed latest-per-name: 22 success, 9 skipped, 0 not-green. GET /commits/36d9b307/status = success, ⛔ not pending.
  • Closing keyword. Closes #17103 is the only keyword-plus-number pair in the body. ⚠️ Minor shape deviation recorded, ⛔ not a REWORK: the checklist asks for the card reference on the body's first line and it sits on line 3, under a one-sentence summary. The purpose — an unmissable, correctly-parsed, unambiguous reference — is served, and editing the body to move one line would risk the footer cell below for no gain.

② Three statements, not two — and the column kept its job

⭐ The dev found a third false statement the card counted only two of: cli.mdx:1337's section intro called the standalone project publishable. It is required by the card's own executable criterion, so finding it is the criterion working rather than scope creep.

⭐ The Publishable? column decision is the part I would have got wrong. The card notes that once os create plugin is also private: true, the column stops discriminating between the two scaffolders — and discriminating is what the table exists to do. Rather than dropping the column, both cells were rewritten so it still answers on the axis a reader needs: os init -t plugin ⇒ 「No — the emitted package.json is private: true, and no flag lifts it」, and os create plugin ⇒ 「Only with --in-repo」. And the first half is measured, not asserted — renderScaffoldPackageJson writes a bare unscoped name and an unconditional private: true, and that command has no placement flag at all.

③ Evidence, and the zeros that make it a reading

  • The card's executable criterion, run two-legged: leg A on origin/main finds the defect (the placeholder-scoped spelling on 2 lines, publishability claims on 4); leg B on the branch finds the placeholder spelling on 3 lines, each read individually and each attached to --in-repo, and no line claiming the standalone output is publishable or scoped.
  • ⭐ The --in-repo half demonstrably survives — named on 8 lines across 4 pages after, versus 5 before. ⇒ the change adds descriptions of it and removes none, which is the half a careless fix would have destroyed.
  • ⭐ The untouched surface is a reading, not an assumption — the strongest evidence in the report. @objectstack/plugin- occurs 89 times in content/docs outside releases (88 before); those resolve to 11 distinct names, and every one was resolved to a real packages/plugins/plugin-NAME/package.json whose name matches and which carries no private flag. That is precisely the ⛔ the card warned about — a blanket replace would have been a bigger defect than the one fixed — answered by measurement.
  • Controls fired in both legs: positive @objectstack/plugin-auth 30 lines unchanged; fabricated @objectstack/plugin-zzznotarealplugin and os create widget both 0.
  • The two adjacent pages the card flagged were read and left alone, with reasons: protocol/kernel/index.mdx already prints ./plugin-NAME/ unscoped and its --in-repo sentence is about workspace:* dependency specs, not publishability; getting-started/your-first-project.mdx:75 routes by artifact and makes no scope or publish claim. ⇒ 「read them, ⛔ do not assume」, done.
  • skip-changeset is measured too: no package's files[] ships content/docs, checked across all 70 workspace packages that declare one, with dist as a positive control (70/70) and a fabricated term as the negative (0); apps/docs is itself private: true. Applied additively by REST and read back, with the union check proving nothing was stripped.

④ Recorded, not blocking — including a new platform cell

⚠️ This body carries two attribution blocks, deliberately. The dev measured a cell references/platform-readings.md does not carry: on the PR-create surface, a session-URL footer sent without a leading rule survives byte-identical and the platform still appends its own block — sent 8,851 bytes, stored 8,941, delta 90. ⇒ the file's recorded 「去掉横线只写页脚则原样存活」 half holds, but it does not imply "one footer". ⛔ Correctly left unrepaired: AGENTS.md forbids re-sending a body that already carries an appended footer, and the only measured PATCH cell appends a bare footer, the wrong form for a PR body. ⇒ this is a documentation gap in that file, ⛔ not a defect in the delivery.

  • One noted-not-filed: protocol/kernel/plugin-spec.mdx:728 heads a section 「Publishing to NPM」 whose commands publish an .osplugin artifact to the ObjectStack registry, not npm. Outside this card's criterion and outside the filing categories; successor named (that page is actively edited).

Verdict

PASS — ACCEPT. Path surface clean, every check green at this head, commit status not pending, closing keyword safe, Clause-②: no verified. Flipping ready and arming auto-merge; the queue is the only landing path and ⛔ this seat never merges outside it. pm:dispatched stays on #17103 until MERGED is verified by measurement on origin/main.

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-10T22:20Z(读表) · 本评论来自 domain:cli 派发座位


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 10, 2026 22:18
@os-justin
os-justin enabled auto-merge September 10, 2026 22:18
@os-justin
os-justin added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 57fb801 Sep 10, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-17103-create-plugin-docs-unscoped branch September 10, 2026 22:45
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants