docs(cli): give the two plugin artifacts distinct nouns and rewrite "Which scaffolder?" as a two-question decision - #16689
Conversation
… "Which scaffolder?" as a two-question decision (#16484) `plugin` names two different artifacts in this CLI and nothing said which one a reader was about to get. `os init <name> -t plugin` writes a metadata package — declarative objects another stack loads, compiled, `private: true`. `os create plugin <name>` writes a kernel code plugin — TypeScript implementing the kernel `Plugin` contract, built by `tsc`, publishable. Someone who wanted a "plugin skeleton" and reached for the nearer of the two got the wrong artifact silently. No flag and no subcommand is renamed: `-t plugin` and `os create plugin` are published surface and are spelled exactly as before. What moved is the NOUN each user-facing string uses, so the two shapes stop sharing one word. The "Which scaffolder?" guidance is now a two-question decision — metadata or kernel code, then a new project or an addition to a directory you already have — landing on exactly one of the four entry points with the reason to pick it: `npm create objectstack@latest` (equivalently `npx create-objectstack`), `os init`, `os init <name> -t plugin`, `os create plugin <name>`. `os create example` is deliberately absent; it was retired in #16483. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eb1f6969a1ac399d0aa4ade324f26ef71acd3878 && git checkout eb1f6969a1ac399d0aa4ade324f26ef71acd3878
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 001a83b0486391847d45f6866896c53ad8714569 947426a7b8a6868fcbe46e2234377bd340d047d7 && git checkout -B drift-repro 001a83b0486391847d45f6866896c53ad8714569 && git merge --no-ff 947426a7b8a6868fcbe46e2234377bd340d047d7
node scripts/docs-audit/affected-docs.mjs --json 001a83b0486391847d45f6866896c53ad8714569
|
PM 受理 — 两问已裁,Clause-② 已核;⛔ 但漂移机器人点名的三个页面未被交代,落地前必须先答
✅ Clause-②
|
…older page means (#16484) The docs-drift bot named three hand-written pages this change touches. Two of them still carried the collision this card exists to remove. `content/docs/getting-started/your-first-project.mdx` listed `os init`'s templates as "`app` / `plugin` / `empty`" with nothing saying which of the two artifacts the middle one makes — on the page a reader who does not yet know there are two arrives at first. It now names the metadata package, says it is not the kernel code plugin `os create plugin` writes, and links the chooser. `content/docs/protocol/kernel/index.mdx` showed `os create plugin` under a bare "Plugin Development" heading. The command it shows was already the right one for what the section describes, so nothing there routed a reader wrong, but the word was still doing double duty on a page that scaffolds. One sentence now names the artifact and points at the chooser. `content/docs/getting-started/examples.mdx` is not falsified and is untouched: its only `os init` sentence is about the starter tree that command emits, names no template and routes no one to a scaffolder choice, and its three other "plugin" uses are the runtime auto-detection sense and the manifest `type` literal — neither is the two-scaffolder collision. No flag and no subcommand is renamed here either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
Docs-drift rows answered — 2 falsified and fixed, 1 not falsifiedRead on the tree the bot named.
1.
|
三页已答,阻塞解除 —— 而其中一页确实被证伪了
⭐ 拦这一轮是对的,并且它抓到的正是最贵的那一页
裸
另两页的处理也对:
机器人的盲区被手工确认为空,而不是被假定为空
载体与档位,在新头上复核
⭐ 一处诚实记账,值得单独点出承接者报告:本轮锁包装器的各段用
落地序列(未执行)CI 于 Generated by Claude Code |
已撤 draft、已武装 auto-merge
落地读数35 项 check-run,latest-per-name 收敛后: 序列按规矩走:先撤 draft(21:41:4xZ),后武装(21:41:52Z, 回声是有内容的(
|
…nd a row says whether it came from prose (objectstack-ai#18515) Part of objectstack-ai#16696. The docs-drift bot's contract is that a row is *reportable*: "Each row says which anchor put it there, **so a wrong row is reportable rather than merely annoying**." objectstack-ai#16696 read all nine hand-written rows on PR objectstack-ai#16694 by hand and reported three shapes. Triage (comment `5578416342`) ruled a **different disposition for each**, and this PR keeps them apart. --- ## Finding 1 — the diagnosis first: this is MATCH WIDENING, not mis-attribution Triage's hard precondition: 「⛔ 修之前先定位是"匹配放宽"还是"归因记错"——这两者的修法相反」. **It is the match widening.** The row named the anchor that genuinely did select the page; the anchor's own doc-side matcher is what accepted a path family the anchor never described. The evidence that decides it, run through the tool's own `routePatternFor` against the real page: ``` ANCHOR TAIL : /environments/:environmentId PATTERN : \/environments\/(?::[A-Za-z_$][\w$]*|\{[A-Za-z_$][\w$]*\}|[A-Za-z0-9_%-]+) …then a NEGATIVE LOOKAHEAD on the class [\w-]. Spelled out, not written: this body's sanitizer eats the bang out of a lookahead opener — measured on the first write of this very PR, where the line came back one byte short. PAGE : content/docs/protocol/kernel/metadata-service.mdx regex .test : true MATCH :204 matched="/environments/:id" line="artifact route (`/pub/v1/environments/:id/artifact[?commit=ID]`) serves" MATCH :213 matched="/environments/env_42" line="path: 'https://cloud.example.com/pub/v1/environments/env_42/artifact?commit=cmt_1a2b'," ``` Reading taken 2026-09-16T17:13Z, tree `8cf527f8e`. Mis-attribution would mean some *other* anchor selected the page and the row named the wrong one. The stated anchor's own pattern matches, twice, so the attribution is correct and the matcher is the defect. The card's grep legs reproduce exactly, with the control the card itself required (2026-09-16T17:13Z, tree `8cf527f8e`, file `content/docs/protocol/kernel/metadata-service.mdx`): | leg | value | |:--|:--| | POSITIVE CONTROL `metadata` | **19** — the instrument fires | | SUBJECT `environments/:environmentId` | **0** — a real zero | | NEAR MISS `environments/:id` | **1**, at `:204` | | DARK CONTROL | **0** | ### The fix, and why it is two arms The two hits fail for two different reasons, and the card names both — "a different path family **and** a different parameter name". 1. **A parameter written as a parameter must name the same parameter** (either spelling: `:environmentId` and `{environmentId}` are one parameter, `:id` is another). Kills `:204`. 2. **A concrete example value filling the tail's LAST segment must end the documented path.** Every earlier parameter is still bounded by the segments to its right; the last one has nothing behind it, so a value there degrades the pattern to a prefix test on `/environments/`. Kills `:213`. ### ⛔ What was measured and rejected Requiring **every** match to sit at the end of the documented path — the way the same tail is matched against a ledger row, `route.endsWith(tail)` — kills both bad hits and reads **−36.3%**. It also deletes `api/environment-routing.mdx`, the most on-target page of that run, whose every occurrence is `/api/v1/environments/:environmentId/...` with a segment after it, plus `publish-and-preview.mdx`, `single-project-mode.mdx` and `http-protocol.mdx`. A route *prefix* written with the route's own parameter named IS a page documenting that route. That is why arm 2 is scoped to a concrete value in the last position and nothing else. ### Corpus measurement behind the narrowing Denominator stated: the **228** distinct route tails declared in this repo's **28** route-ledger files, against all **195** hand-written docs (`scripts/docs-audit/handwritten-docs.json` — the same denominator the tool's own recall figures use). Taken on tree `fef76a4aa`, 2026-09-16T17:45Z. The A/B harness was cross-checked against the shipped `routePatternFor` tail by tail: **228 compared, 0 regex sources differing**. | variant | tail×page rows | delta | tails matching NO page | |:--|--:|--:|--:| | before | 571 | — | 119 | | arm 1 only | 566 | −0.9% | 119 | | **shipped (arm 1 + arm 2)** | **524** | **−8.2%** | **119** | | rejected end-of-path variant | 364 | −36.3% | 120 | **Zero tails go from matching some page to matching none.** Arm 1 accounts for 5 of the 47 dropped rows; the other 42 are one shape — a page documenting a *longer* route, or not a route at all. `/packages/:id` matched eleven pages on the monorepo source paths `packages/core`, `packages/spec`, `packages/plugins` …; `/meta/:type` matched `api/environment-routing.mdx` on the prose *"data/meta/AI/automation"*. --- ## Finding 2 — the reading first, then the argument⚠️ Neither triage nor the PM had taken this reading. It is the card's own command, run before anything was built on it. Reading taken 2026-09-16T17:14Z against PR objectstack-ai#16694's diff `001a83b048...cd9f934`: ``` $ git diff 001a83b...cd9f934 -- packages/client/src/index.ts | grep -n 'environments/:environmentId' 73:+ * `registerForBase` replay against `/environments/:environmentId` — so ``` **Exactly one line, added, and it is English prose inside a JSDoc block:** ``` + /** + * The durable change-log for a metadata item, scoped to this + * environment. Reaches the SAME handler as the unscoped twin — one + * `registerForBase` replay against `/environments/:environmentId` — so + * the body is byte-identical and the declaration must be too. ``` Controls for that reading: the same diff for that file is **102 lines** with **41** `+`-prefixed lines, so the instrument reads a non-empty diff; a dark control (`zzz_no_such_token_zzz`) over the same diff returns **0**. Across the whole diff the anchor occurs **3** times — once in `.changeset/history-door-schema-rebind.md`, once in `packages/client/src/index.ts`, once in `packages/client/src/return-type-precision.test.ts`; only the middle one is an anchor source (the changeset is not `packages/**`, the test file is skipped as a test file). ⇒ the card's claim holds. **Count correction, stated rather than repeated:** the card and PR objectstack-ai#16694's own audit comment both say *six* rows carried this anchor. Reproducing the run gives **seven** hand-written rows plus the one release-owned row — **eight**. The shape of the finding is unaffected; the number is corrected here because this PR re-derives it. ### The option taken: MARK the row, ⛔ do not exclude comments Triage: 「要么把注释排除出锚源,要么在行里标注锚来自注释……⛔ 不要两个都做成硬排除:JSDoc 里新增一条真实路由的文档,有时**正是**该页需要更新的信号」. This PR takes the **second** option and **confirms it did not do both as hard exclusions**: nothing is excluded from anchor sources. A comment line remains a changed line, a path in a JSDoc still mints a `route` anchor, and no page that was listed for a comment-sourced anchor stops being listed. The comment mask is read **only to word the provenance clause** — `anchors[].from` is publication, never discrimination, exactly as the objectstack-ai#12824 ruling set it. Why this option and not exclusion: exclusion is irreversible at the reader's end. A JSDoc that newly documents a real route is the signal in exactly the case the tool exists for, and a reader who never sees the row cannot recover it. A marked row costs a glance and keeps the recall. Rows on PR objectstack-ai#16694 before → after: ``` before: /environments/:environmentId (route, a path literal in meta) after : /environments/:environmentId (route, a path literal in a comment in meta) ``` That distinction also turned out to be load-bearing for the *second* route anchor on that PR: `/meta/:type/:name/history` likewise enters only through a `//` comment — the line is `+ // [objectstack-ai#13523] The change-log body of GET /meta/:type/:name/history, the one`. Both route anchors on that run are comment-sourced, and now both say so. --- ## Finding 3 — left alone `IMetadataService.getHistory` → `MetadataHistoryQueryResult` versus the SDK's `getHistory` → `HistoryMetaItemResponse`. The card recorded it as **working-as-designed**, a trap for the next reader and not a defect; triage agreed and ruled ⛔ do not change. **Nothing in this PR touches it.** Its row is byte-identical before and after: ``` content/docs/kernel/contracts/metadata-service.mdx via getHistory (sdk, the bare tail of client method meta.getHistory, bound to GET /api/v1/meta/:type/:name/history) ``` Triage noted the row should say "锚为裸方法名" — it already does (`the bare tail of client method …`, objectstack-ai#12824), so there was nothing to add. --- ## Two-direction acceptance ⛔ Triage's fence: 「一个"把弱命中一律丢掉"的实现会让本卡三条全绿,同时把工具的价值删掉」, and the card's own first ⛔: *"Not that the bot should be quieter."* Both directions are exercised. ### Direction 1 — the wrong row is gone `node scripts/docs-audit/affected-docs.mjs 001a83b --json`, run in a detached worktree at PR objectstack-ai#16694's head `cd9f93413e` (whose `content/docs` tree is the object the bot computed on). **10 rows → 9.** The single drop: ``` DROPPED content/docs/protocol/kernel/metadata-service.mdx ['/environments/:environmentId (route, a path literal in meta)'] ``` Every other row is kept; seven are re-claused with the comment marker; the `getHistory` row is untouched. The run before the change reproduces the bot's published comment on PR objectstack-ai#16694 exactly — the same 9 hand-written rows and the same 1 release-owned row, with identical `via` clauses. ### Direction 2 — a genuinely falsified row is still listed, with a strong-hit anchor PR objectstack-ai#16689 is the sibling sweep the card cites, where the hand read found rows that **were** genuinely falsified and fixed (its own answer comment: "2 falsified and fixed, 1 not falsified"). Same command, base `c8e5ac645f`, head `947426a7b8`: **7 rows → 7 rows, every `via` clause byte-identical.** The page that PR objectstack-ai#16689 recorded as FALSIFIED and then fixed, `content/docs/getting-started/your-first-project.mdx`, is still listed, and its anchors carry no comment marker — a strong hit: ``` KEPT getting-started/your-first-project.mdx ['os create (command, read off packages/cli/src/commands/create.ts)', 'os init (command, read off packages/cli/src/commands/init.ts)'] ``` ### And in the self-test A new battery, `ROUTE-ANCHOR PRECISION, IN BOTH DIRECTIONS (objectstack-ai#16696)`, 20 cases, every narrowing case paired with a KEEP case from the same run — including the positive control that `/data/:object` still matches a page writing `POST /api/v1/data/accounts`, and that a comment-only anchor is **still an anchor** and merely gains a clause. **Reverse verification**, from the committed state, each leg proved on disk by blob hash and restored with `git checkout HEAD -- PATH`: | ablation | self-test exit | what failed | |:--|--:|:--| | revert the whole narrowing (restore the old widened parameter arm) | **1** | 6 assertions, all of them the narrowing cases; ⛔ every KEEP case stayed green | | blind the comment mask (`masked = lines`) | **1** | 4 assertions, all of them the provenance clauses | | restored | **0** | 605 cases pass; `git diff HEAD` empty, blob `6010e3c0` = HEAD blob | --- ## ⛔ No occurrence rate is asserted The card: 「One PR is not a population」. Triage: 「⛔ 修卡的人也不例外」. This PR measures **one** PR's rows on objectstack-ai#16694 and **one** on objectstack-ai#16689, and makes no claim about how often either shape occurs. The corpus table above is a statement about the **matcher over the declared route surface** — 228 tails × 195 docs — which is a different question and the only one measured here. ## Not touched `content/docs/releases/` — untouched. The `releases/implementation-status.mdx` row was audited read-only by the card and judged correctly listed; it is still listed after this change, now via the `{environmentId}` spelling at `:190`. ## Changeset — `skip-changeset`, measured The sole criterion is whether anything **published** moves, read off each package's actual `files[]`, ⛔ not off the path name: - **70** non-private `package.json` files scanned; **0** have a `files[]` entry that reaches `scripts/docs-audit/affected-docs.mjs` or `scripts/docs-audit/README.md`. - **Positive control** that the scan reads real entries, and the exact shape that made this reflex wrong on card objectstack-ai#18169: **147** non-`dist` `files[]` entries exist across those packages, and they are dominated by `README.md` / `CHANGELOG.md` — so a non-`dist` entry is visible to this scan and none of them reaches these paths. - **0** published packages name a `scripts` entry in `files[]` at all. - The root `package.json` is `private: true`. - Symbol leg: `routePatternFor` occurs in exactly one file in the tree — the changed one. The three `packages/**` hits for the string `docs-audit/affected-docs` are prose inside comments (`packages/objectql/src/declared-fields.ts:183`, `packages/spec/scripts/build-schemas.ts:1259`) and a JSON `description` field. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from this worktree — **33** commands. Every one recorded in the report. All green, including `check:docs-audit-scope` (which runs this file's `--self-test`), `check:comment-mask-corpus`, `scripts/docs-audit/check-affected-docs.mjs` and `scripts/docs-audit/check-drift-comment.mjs`. ## Acceptance notes Noted here rather than filed — none is a reproducible defect, a declared-contract violation, or a metadata-authoring trap: - The card and PR objectstack-ai#16694's audit comment both say *six* rows carried the `/environments/:environmentId` anchor; reproducing the run gives seven hand-written plus one release-owned. An arithmetic slip in a narrative, corrected above, not a defect in code. Carrier: this PR body. - `/packages/:id` matching monorepo source paths such as `packages/core` on eleven pages is the same widening shape as finding 1, and this change removes it as a side effect. It was never filed separately and is not filed now — it is inside this card's fix, not beside it. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16484
Clause-②: no
pluginnamed two different artifacts in this CLI and nothing told a reader which one they were about to get:os init NAME -t pluginscaffolds a metadata package — declarative objects another stack loads, built byobjectstack compile, emittedprivate: true.os create plugin NAMEscaffolds a kernel code plugin — TypeScript implementing the kernelPlugincontract, built bytsc, publishable as@objectstack/plugin-NAME.Someone who wanted a "plugin skeleton" and reached for the nearer of the two got the wrong artifact, and nothing anywhere failed to say so: the metadata package has no
Pluginto implement, and the kernel code plugin has no declarative objects to compile.The fence is respected: no flag and no subcommand is renamed
-t pluginandos create pluginare published surface and are spelled exactly as before. Renaming them is a separate decision and is not attempted here. What moved is the noun each user-facing string uses for the artifact, so the two shapes stop sharing one word. Nothing in this diff touchespackages/cli/src/commands/create.ts's command/args/flags spellings orinit.ts'sTEMPLATESkeys.The chooser is now a two-question decision naming all four entry points
content/docs/deployment/cli.mdx, underos init. Question 1 is metadata, or kernel code?; question 2 is a whole new project, or an addition to a directory you already have? The table that answers them names four entry points, each with the reason to pick it:npm create objectstack@latest NAME— equivalentlynpx create-objectstack NAMEos init— oros init -t emptyfor a bare configos init NAME -t plugin--in-repoos create plugin NAMEos create exampleis deliberately absent: it was retired by #16483 / PR #16665, which is what made this card writable. The two places that PR moved — the Scaffolding roster row and the wholeos createsection carrying the retirement notice — are left as it left them; everything here was located by text, never by the line numbers that merge voided.Every user-facing string, measured against the built CLI
Rendered from
packages/cli/bin/run.jsafterpnpm build, not read off the source:Docs pages:
content/docs/deployment/cli.mdx(chooser, the word-collision table, theos initexamples, the options list, the Templates table, theos createintro, the "Why the two scaffolders are deliberately separate" callout) andcontent/docs/plugins/index.mdx(a callout saying which of the two artifacts that page teaches, pointing back at the chooser — that page is the destinationcli.mdxsends kernel-code readers to).Changeset reading — you decide, here is mine
The card says
skip-changeset(docs only) unless help strings change, then a patch changeset. Help strings did change (os init --helpand theTemplate:line above are both shipped CLI output), so this PR carries.changeset/cli-plugin-word-disambiguation.mdatpatchand does not applyskip-changeset. Not breaking, so no ADR-0087 marker is owed andcheck-adr-0087-registrationagrees (green, run below).One bounded in-place fix, declared
packages/cli/README.md'sos createroster row readCreate a new package/plugin/example from template— barepluginfor the shape (this card's defect class) and aexamplethat PR #16665 retired. The retired-docs-parity pin covers the fourcontent/docspages and not this file, so it was still shipping on npm. It is the same table cell this card had to rewrite anyway; leaving a false half in a line I was already editing was not an option. Same class, mechanical, same gate family, no new verification surface, no other claim on the file.Pins held, not relaxed
packages/cli/test/create-example-retired-docs-parity.test.ts— the queue-tier pin feat(cli)!: retireos create example; the refusal namesos init(#16483) #16665 added. Its control (each of the four pages still shows a runnableos create pluginfence) and both assertions pass; no page acquired a copyable retired command.packages/cli/test/create-plugin-docs-parity.test.ts— the three file-tree pages still promise exactly what the template emits. Theplugins/index.mdxaddition is prose OUTSIDE the fence, so it adds no file token to the harvested set.packages/cli/test/init.test.ts's template-description pin (os inittemplate descriptions advertise views/actions/extensions that no template emits #9737) — the newplugindescription claimsobjectsand the template emitssrc/objects/; it claims noviews/actions/extensions.Verification
Head every result below was measured on:
48bf085e34, this branch's final commit, working tree clean.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 81 families from the changeset itself (not from a hand-written path list); each ran with its exit code captured before any pipe;--ranreconciles81 derived, 81 run, 0 NOT-MEASURED, 0 UNRUN.check:i18n,check:i18n-coverage,check:i18n-walk-parity,check:dual-build-cjs-loadsand@objectstack/spec check:skill-examples, each naming a missingdist/. A fullpnpm build(73/73 tasks, exit 0) was run and all five were re-run to a real green. None of them is reported on the strength of the refusal.pnpm exec eslint . --no-inline-config --format jsonre-run AT48bf085e34— exit 0, 6318 files linted, 0 errors, 0 warnings. No narrowing argument is needed because the full population was measured;pnpm check:nul-bytesat the same head scanned 8225 text files clean.pnpm --filter @objectstack/cli typecheck— exit 0 (tsc --noEmitpluscheck:test-typecheck, whose shrink-only debt ledger is unchanged).packages/cliunit tier —pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 184 files passed (184), 2509 tests passed, 6 expected-fail, lock verdictcommand-exit 0. The integration tier is declared to CI: this diff touches neitherbin/, nortest/helpers/serve-process.ts, nor any driver/kernel boot path, so no integration-layer file is reached.scripts/pm/os-verify-lock.sh; verdicts are read from its ownVERDICTlines.验收备注
content/docs/deployment/cli.mdx's Templates table saysappcreates "Full application with objects, barrel imports" while the CLI's ownappdescription is "Full application with objects". Both are true and theos inittemplate descriptions advertise views/actions/extensions that no template emits #9737 pin governs the CLI side only. Noted, not filed — an observation, not a defect.content/docs/protocol/kernel/plugin-spec.mdxuses "plugin" in a third, broader sense ("the unit of distribution"). That is not the two-scaffolder collision this card is about, and it is left alone.packages/cli/README.mdis not covered by the retired-command docs-parity pin that holds the fourcontent/docspages. That is a real coverage edge; it is not filed as a card because this PR removes the one instance, and filing a gate-widening card is a decision for the PM rather than a finding I should open unilaterally.Generated by Claude Code