Skip to content

fix(docs): plugin-spec's package.json example silently installs a 2.x CLI — both @objectstack/* ranges repaired to ^17.0.0 - #18186

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-17378-plugin-spec-cli-range
Sep 14, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-17378-plugin-spec-cli-range

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #17378

Clause-②: no
This diff is one hand-written docs page. It adds no schema key, no closed-set member, no published export and no registry entry — nothing an author can write gets wider.

The defect

content/docs/protocol/kernel/plugin-spec.mdx publishes the protocol's own plugin-packaging package.json example. Its dependency block declared:

"dependencies":    { "@objectstack/core": "^2.0.0" }
"devDependencies": { "@objectstack/cli":  "^2.0.0", "typescript": "^5.3.0" }

Both packages ship at 17.4.0 (packages/cli/package.json, packages/core/package.json, confirmed against the registry: npm view @objectstack/cli version → 17.4.0, dist-tags.latest → 17.4.0; same for @objectstack/core). A caret pins the major, so ^2.0.0 admits no 17.x — the card's arithmetic (17.4.0 is not in ^2.0.0) holds on today's tree.

What an author actually gets — measured, not assumed

The card deliberately did not claim whether a 2.x was ever published, because it does not change the verdict. It is worth recording anyway, because it names the failure mode:

  • @objectstack/cli — 160 published versions, including 2.0.0 … 2.0.7.
  • @objectstack/core — 157 published versions, including 2.0.0 … 2.0.7.

So the block resolves. It is not an install error — it is worse than one: an author copying the page silently installs @objectstack/cli@2.0.7, fifteen majors behind, and then runs the page's own "package": "os plugin build" script on that 2.x CLI. A failed install is loud; this is quiet.

Both packages are public — no private: true, publishConfig.access: public, files: ["dist", "README.md", "CHANGELOG.md"] — and @objectstack/cli is the right devDependency for this example, since it is the package whose bin provides the os binary the block's own scripts invoke.

The range chosen, and the reading behind it

^17.0.0 — the current major's floor, caret.

  1. Both other hand-written carriers in this repo already teach exactly that, and the card named them as the correct ones: packages/create-objectstack/src/templates/blank/package.json:26 and skills/objectstack-platform/SKILL.md:607, both "@objectstack/cli": "^17.0.0" beside "typescript": "^5.3.0". Picking anything else would make this page the only carrier teaching a third style.
  2. There is an authority, and it is not a constant — it is a function. packages/cli/src/commands/init.ts resolves every scaffolded @objectstack/* range through getCliVersion() / pkgVersion(), which caret-pins to the running CLI's own version, over a comment that states the rule: every @objectstack/* package in this monorepo is published on one shared release version. That is caret-on-the-current-major with a live floor. A hand-written page cannot carry a live floor, so it carries the major's — which is what the two carriers above do.
  3. ^17.4.0 was rejected: it admits exactly the same set for a reader installing today, and rots on every minor release, while ^17.0.0 only moves when the major does. The page now says so in one sentence so the next reader knows what to update and when.

The prose under the block already named SCAFFOLD_TYPESCRIPT_RANGE as the authority for the typescript floor — the shape #16756 established on the adjacent line. This PR extends the same sentence to name getCliVersion() for the @objectstack/* ranges, so the page teaches one range style rather than two.

⭐ Declared: the fix covers TWO lines in the block, not the one on the card

The card names @objectstack/cli at what was then line 707. @objectstack/core, one line above inside the same package.json example, carried the identical defect — ^2.0.0 against a published 17.4.0 — and the card's triage comment anticipated exactly this ("a census keyed to one literal repairs one key and leaves its neighbours ... re-derive the whole block against publishable versions").

Repairing only the cli line would have left the block still uninstallable-as-intended, so the ruling's invariant ("the published example resolves to today's line") would not have been restored. Both lines are in the same JSON object, same defect class, same mechanical shape, and no other claim holds that section. Nothing else in the file was touched.

⛔ NOT touched — reported, not swept

Five further ^2.0.0 occurrences exist on this page. None is the same defect and none was changed:

line text why it is out
60, 436 '@objectstack/core': '^2.0.0' plugin manifest dependencies, a Record of packageId to versionRange resolved by the kernel, not by npm. Different surface, different resolver.
71, 462 '@objectstack/ui': '^2.0.0' same manifest surface, under peerDependencies, which the page's own callout marks proposal-only ("the schema declares neither, so nothing resolves them").
937 '@objectstack/core': '2.0.0', // Not '^2.0.0' a deliberate exact-pin-vs-caret teaching example; the number is the lesson's prop, and changing it is a content decision, not a mechanical repair.

One further finding surfaced while measuring those and is reported, not filed and not fixed: @objectstack/ui returns 404 from the registry (npm view @objectstack/ui version → E404 ... is not in this registry), so those two manifest lines name a package nobody can install. It sits on a surface the page already labels proposal-only, so it is a different card from this one.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no path list — derived from git): 40 families, all run, exit codes captured to disk before any pipe. 40/40 exit 0.
  • Reconciled: dispatch-gates --ran → 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero — every family recorded an exit code and none is 3).
  • First sweep produced three exit 3 PREREQUISITE NOT MET results (@objectstack/lint, @objectstack/formula, @objectstack/client-react unbuilt). Those are NOT MEASURED, not failures: the prerequisites were built and the whole sweep was re-derived and re-run. The 40/40 above is that second sweep.
  • Builds ran under scripts/pm/os-verify-lock.sh (slot issue-17378), both VERDICT command-exit 0.
  • eslint, narrowed and the narrowing proven. Targeted run: eslint content/docs/protocol/kernel/plugin-spec.mdx --no-inline-config --format json → exit 0, 1 result entry, 0 errors, 1 warning, and that warning is File ignored because no matching configuration was supplied. ① Population read from eslint's own config: every files: selector in eslint.config.mjs is a TS/JS glob (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} and narrower); .mdx matches none, and the programmatic ESLint#isPathIgnored on this path answers true. ② Count read from --format json, as above. ③ Invariance: eslint.config.mjs states, and grep confirms, that no config block sets parserOptions.project and no typed @typescript-eslint rule is enabled — with type-aware linting off, this diff cannot move the verdict on any untouched file. This PR's eslint result is therefore identical to main's, by construction.
  • Counted with grep -o | wc -l throughout, each count paired with a firing control: the two replaced literals were 1 and 1 before and 0 and 0 after, the two single-quoted manifest occurrences stayed 2 throughout (they must not move), and the card's nonsense probe "@objectstack/zzzz" returns 0.
  • Control-byte self-scan over the changed file: grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' → no match (exit 1), with the class proven to fire on an injected \x01.
  • Build Docs and Test Core are path-scheduled CI jobs with no local invocation; declared to CI, NOT MEASURED here.

Changeset

skip-changeset. Measured on the publish surface rather than asserted: apps/docs is private: true, and enumerating the files[] array of every package.json in the workspace finds no published package shipping any content/ path (firing control on the same probe: packages/cli's files[] reads back ["dist","README.md","CHANGELOG.md"], so the probe is reading real arrays). This diff publishes nothing.

Acceptance notes


Generated by Claude Code

…t `^2.0.0`, a range today's 17.4.0 cannot satisfy

`content/docs/protocol/kernel/plugin-spec.mdx`'s published plugin `package.json`
example declared `"@objectstack/core": "^2.0.0"` and `"@objectstack/cli":
"^2.0.0"`. Both packages publish at `17.4.0`, and `^2.0.0` pins the major, so an
author copying the block resolves `2.0.7` — the last 2.x on npm, fifteen majors
behind — and then runs the page's own `os plugin build` on a 2.x CLI. Both lines
now read `^17.0.0`, matching the repo's two other hand-written carriers
(`packages/create-objectstack/src/templates/blank/package.json`,
`skills/objectstack-platform/SKILL.md`).

The prose under the block already named `SCAFFOLD_TYPESCRIPT_RANGE` as the
authority for the `typescript` floor; it now names `getCliVersion()` in the same
file as the authority for the `@objectstack/*` ranges, so the page teaches one
range style rather than two.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 14, 2026
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 14, 2026
@claude
claude Bot marked this pull request as ready for review September 14, 2026 12:43
@claude
claude Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 076bb97 Sep 14, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-17378-plugin-spec-cli-range branch September 14, 2026 12:55
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… record what the kernel actually reads (objectstack-ai#18415)

`Part of objectstack-ai#18188` — the PM decides closure.

## 1. The load-bearing reading: does the kernel resolve a manifest
`dependencies` entry at plugin load?

**No. The kernel never parses the version range.** Measured two ways,
call site first (read-only, as the card's acceptance prefers), then
executed.

### 1a. Call sites — every consumer of `manifest.dependencies` in this
repo

`ManifestSchema.dependencies`
(`packages/spec/src/kernel/manifest.zod.ts:439`) is
`z.record(z.string(), z.string())` — a map of package id to version
range, where the value is typed only as "some string".

It has **two** consumers outside tests, and **both read
`Object.keys(...)` only**:

| consumer | line | what it takes |
| --- | --- | --- |
| `resolveArtifactPackageOrder` —
`packages/core/src/artifact-packages.ts` | 282 |
`Object.keys(manifest.dependencies ?? {})`, handed to
`resolvePluginOrder` as that node's `optionalDependencies` |
| `resolveWritePackageScope` —
`packages/metadata-protocol/src/protocol.ts` | 5346 |
`Object.keys(declared)`, walked transitively for the write-scope closure
|

Neither ever reads a value. `artifact-packages.ts` says so in its own
header (lines 105–124): "Why declared dependencies enter as
`optionalDependencies`" — an id that names a package inside the artifact
is a real edge, one that does not is simply not an edge here, "hoisted
ahead when composed, silently skipped when absent".

The machinery the page's *Version Constraints* section describes —
`SemanticVersionManager.satisfies()` and
`DependencyResolver.detectConflicts()` in
`packages/core/src/dependency-resolver.ts` — **has no production call
site at all**:

```
SUBJECT       grep -rn 'dependency-resolver\.js' --include=*.ts packages/ apps/
              -> packages/core/src/index.ts:126          (barrel re-export)
              -> packages/core/src/dependency-resolver.test.ts:2   (its own unit test)
              = 0 production importers

FIRING CTRL   grep -rn 'plugin-order\.js'      (same directory, same barrel, same export form)
              -> index.ts:12, kernel-base.ts:11, artifact-packages.ts:132, kernel.ts:15, plugin-registration.ts:41
              = 4 production importers  -> the instrument fires

DARK CTRL     grep -rn 'dependency-resolver-NOSUCH\.js'
              = 0                                        -> the instrument is not just printing hits
```

The kernel's own load-order surface is a *different* field:
`OrderablePlugin.dependencies` (`packages/core/src/plugin-order.ts:50`)
is `string[]` — plugin names, no version ranges in it at all.

### 1b. Executed, with a firing control on the value and one on the key

Ran against the built `@objectstack/core`
(`packages/core/dist/index.js`), calling `resolveArtifactPackageOrder`
directly:

```
S1  caret ^2.0.0  on @objectstack/core            -> OK   [com.acme.a]
S2  caret ^17.0.0 on @objectstack/core            -> OK   [com.acme.a]
C-VALUE  'NOT-A-VERSION-RANGE-@@@' on the same key -> OK   [com.acme.a]
C-KEY    the SAME garbage value, but the key names
         a sibling package IN the artifact         -> OK   [com.acme.b, com.acme.a]   (declared order was a, b)
C-DARK   no `dependencies` at all                  -> OK   [com.acme.a, com.acme.b]   (declared order preserved)
```

- **C-VALUE** is the firing control on the value: a string that is not a
version range in any grammar resolves exactly like `^2.0.0` does.
Nothing parses the value.
- **C-KEY** is the firing control on the key, and it discriminates: the
same unparseable value on a key that *is* in the artifact flips the
order from the declared `[a, b]` to `[b, a]`. So the probe can observe
`dependencies` having an effect — it has one, through its **keys**, and
none through its **values**.
- **C-DARK** confirms the flip in C-KEY came from the dependency edge
and not from something else in the call.

Also already pinned in the existing suite:
`packages/objectql/src/artifact-load-path.test.ts:235` — *"leaves a
dependency on a package OUTSIDE the artifact to the installer"* — which
declares `'@steedos/plugin-auth': '^2.0.0'` and asserts the artifact
resolves. Run: `pnpm --filter @objectstack/objectql exec vitest run
--maxWorkers=2 src/artifact-load-path.test.ts` :: **exit 0**, 14/14
passed.

### 1c. What that means for the card's grading

⇒ **This is an inert wrong number, not a load-time failure.** Copying
the example does not fail at plugin load. It is a documentation-accuracy
defect, not class (a). The card's own text says exactly this branch
belongs "in a PR's acceptance notes rather than in a card" — that
judgment is the PM's, not mine; the reading is recorded here either way.

## 2. Every number changed, and the reading behind it

⚠️ Because the kernel accepts **any** string here, the kernel reading
cannot justify a *particular* number. The justifying reading for the
numbers is the **npm registry plus this checkout**, and the page now
says which of the two governs.

| site | before | after | reading |
| --- | --- | --- | --- |
| `:60` manifest example 1, `dependencies` | `'^2.0.0'` | `'^17.0.0'` |
`packages/core/package.json` version = **17.4.0**; npm `latest` for
`@objectstack/core` = **17.4.0** (157 versions published). `^17.0.0` is
the spelling PR objectstack-ai#18186 already put on the npm half of this same page at
`:703`, so both halves now agree. |
| `:436` *Dependency Types* §1, `dependencies` | `'^2.0.0'` |
`'^17.0.0'` | same |
| `:942` the pin-vs-caret worked example | `'2.0.0', // Not '^2.0.0'` |
`'17.4.0', // Not '^17.0.0'` | same; see §3 |

Registry control: the same two-legged query returned a full packument
for `@objectstack/core` (firing) and `Not found` for `@objectstack/ui`
(see §4) — the probe discriminates.

## 3. What I decided about line 942, and why

**Changed the number, kept the argument, and added one paragraph so the
reason is one the runtime actually delivers.**

The section is *Best Practices → 3. Pin Core Dependencies*. Its argument
is **exact pin beats caret range**; the number is the vehicle, not the
point. Changing `'2.0.0'` to `'17.4.0'` and the comment `// Not
'^2.0.0'` to `// Not '^17.0.0'` preserves that argument *form for form*
— an exact version on the left, the caret form named in the comment as
the thing not to write — while removing the fifteen-major-stale number a
reader would copy. Acceptance criterion 3 holds: the prose still argues
"pin exactly", and the demonstration still demonstrates an exact pin
against a caret.

But the old reason — "to avoid surprises" — is one §1 shows the kernel
does not deliver: the range is never checked at load, so pinning it
prevents no surprise *there*. Leaving that sentence untouched would have
left the page advertising an enforcement that does not exist (AGENTS.md
Prime Directive objectstack-ai#10's corollary). So the reason now points where it is
true: the installer, and the same dependency in `package.json`, which
npm really does resolve.

The added paragraph and the new `Callout` under *Version Constraints*
are the same idiom the page already uses five times over
(`optionalDependencies` / `peerDependencies` are "proposal-only"; there
is no `definePlugin()`; no `onBoot`, `onUpgrade` or `onUninstall` hook).
Nothing was invented for this PR.

## 4. The `@objectstack/ui` sites at `:71` and `:462` — **unchanged**,
and here is what I found

Both sit in **`peerDependencies`**, which this page's own callout at
`:425-429` already declares proposal-only: "the schema declares neither,
so nothing resolves them."

Two readings on the package itself:

- **Not in this checkout.** No `package.json` under `packages/`, `apps/`
declares the name `@objectstack/ui`. Firing control: the identical query
for `@objectstack/core` returns `packages/core/package.json`. Dark
control: a nonsense name returns 0.
- **Not on npm.** `GET https://registry.npmjs.org/@objectstack%2fui` ->
`{"error":"Not found"}`. Firing control on the same endpoint:
`@objectstack/core` -> `latest: 17.4.0`, 157 versions.

So `^2.0.0` for `@objectstack/ui` is a range on a package that exists
**neither here nor on the public registry**, in a block the page itself
says nothing resolves. There is no reading that says what the right
number would be, so per the dispatch fence I changed nothing and report
it instead. It is a live question for whoever owns that name.

## 5. Changeset: `skip-changeset`, measured against `files[]`

Diff is one file: `content/docs/protocol/kernel/plugin-spec.mdx`.

```
package.json scanned:                                  77
non-private (publishable):                             70
publishable packages with NO files[] field:             0   (so npm's ship-everything default is nowhere in play)
SUBJECT   files[] arrays mentioning "content":          0
FIRING CONTROL  files[] arrays mentioning "dist":      70   (70/70 -> the reader is reading the arrays)
```

`content/docs/**` is rendered by `apps/docs`, whose `package.json` is
`"private": true` and declares no `files[]` — it publishes nothing.
**Zero published bytes move.** Asserted from the `files[]` readings
above, not from the path name.

## 6. Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack content/docs/protocol/kernel/plugin-spec.mdx`
from this worktree. **39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN**,
reconciled with `--ran` carrying every exit code:

```
✓ dispatch-gates --ran: 39 derived famil(ies) accounted for — 39 run,
  0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3).
```

**38 of 39 exit 0.** Repo-wide `pnpm lint` (`eslint .
--no-inline-config`, no narrowing) :: **exit 0** at `7cbd43373`, whose
tree `bf6b9c0f5c708647871a55046be254192c1a8b8d` is byte-identical to
this PR's head `d4f2919c2` (the amend was message-only).

### The one red, and why it is not this diff

`pnpm check:cross-package-test-inputs` :: **exit 1**, flagging
`packages/cli/test/init-created-files-summary.e2e.test.ts` descending
into `packages/spec/dist/`. Neither path is in this diff. Proven by
ablation, not by argument:

1. `git checkout 7358c1c --
content/docs/protocol/kernel/plugin-spec.mdx` — ablation verified on
disk: the added marker went to **0** occurrences and the base
`'@objectstack/core': '^2.0.0',` came back to **2**.
2. Re-ran the gate with the diff removed :: **exit 1**, same `FAIL:`
line. The red survives the ablation ⇒ it is not caused by this change.
3. Restored with `git checkout HEAD -- …`, verified two ways: `git diff
HEAD` empty for that path, and `git hash-object` =
`f948028b39a1dd0e1f3959bf1c1c628674f3c8ae` = the HEAD blob.

CI corroborates: **"Lint & Repo Gates" completed `success`** on
`7358c1c5b`, this branch's merge base. The red is a property of a
locally-built tree (the gate walks `packages/spec/dist/`, which only
exists here because I built it for the §1b probe), not of `main`.

`pnpm --filter @objectstack/spec run check:skill-examples` first exited
1 with a **PREREQUISITE NOT MET** refusal — `packages/client-react/dist`
held no declarations. That is not a verdict. After `pnpm --filter
'@objectstack/client-react...' build` it exits **0**: 258 prose examples
type-check across 3 surfaces.

## Acceptance notes (⛔ not filed, noted here)

- **`content/docs/protocol/kernel/plugin-spec.mdx:470-471`** still
introduces the version-constraint grammar through
`SemanticVersionManager.satisfies()`, a function with no production call
site. The new callout directly above it now says so, so the page no
longer misleads — but the sentence itself is a candidate for a rewrite
that stops leading with a dead symbol. Successor: whoever next edits
this page's *Dependency Management* section.
- **`pnpm check:cross-package-test-inputs` reds on any tree where
`packages/spec/dist/` is built.** Green on CI at the same commit, so
this is a local-run-only asymmetry, not a defect in the code. Successor:
any developer who builds `packages/spec` and then runs the derived gate
list locally — which is every dev on a docs card like this one.

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_

Co-authored-by: Claude <noreply@anthropic.com>
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

1 participant