Skip to content

docs(kernel): move plugin manifest dependency examples off ^2.0.0 and record what the kernel actually reads - #18415

Merged
os-try-charles merged 1 commit into
mainfrom
claude/issue-18188-manifest-example-version-ranges
Sep 16, 2026
Merged

os-try-charles merged 1 commit into
mainfrom
claude/issue-18188-manifest-example-version-ranges

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Part of #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
resolveArtifactPackageOrderpackages/core/src/artifact-packages.ts 282 Object.keys(manifest.dependencies ?? {}), handed to resolvePluginOrder as that node's optionalDependencies
resolveWritePackageScopepackages/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.tshas 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 #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 #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 :462unchanged, 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 7358c1c5b -- 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.ai/code/session_017ef78bLdybu3AffehKkhfk


Generated by Claude Code

…hat the kernel reads

The PLUGIN MANIFEST examples on plugin-spec.mdx still pinned
`@objectstack/core` at `^2.0.0`; `@objectstack/core`'s current version is
17.4.0 and the npm `package.json` example on the same page was already moved
to `^17.0.0`. Move the two manifest `dependencies` sites to `^17.0.0` so both
halves of the page agree, and move the pin-vs-caret worked example to an exact
`17.4.0` against `^17.0.0`, which keeps that lesson arguing and demonstrating
exactly what it argued before.

Also record what the kernel actually does with these strings, because the page
implied a resolution it does not perform. `manifest.dependencies` has two
consumers and both take `Object.keys(...)` only: `resolveArtifactPackageOrder`
(packages/core/src/artifact-packages.ts) feeds the ids to `resolvePluginOrder`
as `optionalDependencies`, and `resolveWritePackageScope`
(packages/metadata-protocol/src/protocol.ts) walks the ids transitively.
`SemanticVersionManager` and `DependencyResolver` are exported from
`@objectstack/core` but have no call site outside their own unit test, so the
range value is never parsed at plugin load.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk

Copy link
Copy Markdown
Collaborator Author

PM review — ACCEPT

Reviewed against GitHub and against the tree at origin/main 588475c30, ⛔ not against the report's narrative. Every number below I re-derived myself; where my population differed from the dev's I say so.

The load-bearing leg, re-derived independently

The card's whole grading turned on one question — does the kernel resolve a manifest dependencies range at plugin load? — and the dev's answer falsifies half the card's own framing. I re-derived it from scratch:

reading my re-derivation
consumers of manifest.dependencies outside tests exactly 2, and I enumerated by a different route (grep \.dependencies across packages/ + apps/, excluding the three npm-ish keys) — same two files
packages/core/src/artifact-packages.ts:281-283 optionalDependencies: Object.keys((manifest as {…}).dependencies ?? {})keys only, read verbatim
packages/metadata-protocol/src/protocol.ts:5346-5348 const declared = manifest?.dependencies; … for (const dep of Object.keys(declared))keys only, read verbatim
SemanticVersionManager / DependencyResolver symbol occurrences only dependency-resolver.ts (8) and dependency-resolver.test.ts (61). The sole other mentions of the module are index.ts:126 (barrel re-export) and plugin-loader.ts:492 (a doc comment). ⇒ 0 production call sites
firing control plugin-order (same dir, same barrel, same export form) 13 files, incl. kernel.ts, kernel-base.ts, plugin-registration.ts, rest/package-routes.ts — the instrument fires
dark control 0

Confirmed. The value is never parsed at load; the ids are. ⭐ And the right call was made with it: rather than silently correcting a number under prose that promised an enforcement which does not happen, the PR adds the reading to the page. A docs card that comes back having falsified its own premise, and repairs the premise rather than the symptom, is worth more than the three-number edit it was dispatched for.

⚠️ One thing I checked that the PR's new callout must survive, and does. packages/spec/src/kernel/dependency-resolution.zod.ts:19 carries a Resolution Flow doc comment whose step 1 is literally "Parse manifest.dependencies (SemVer ranges)", and four packages/spec migration entries assert "Real dependency resolution runs off top-level manifest.dependencies". Those are schema prose and migration prose, not consumers — they say the same thing the page used to say. The callout is scoped to "at plugin load" and explicitly hands the range to "the installer and human readers", so it does not collide with an install-time protocol that may yet be built. ⛔ Recorded as a live residue, not as a defect in this diff.

The diff, read file-by-file

1 file, +23 −4. :60 and :436 carets → ^17.0.0; the pin-vs-caret worked example → 17.4.0 / // Not '^17.0.0', which keeps the lesson arguing exact pin over caret, form for form. @objectstack/core current version 17.4.0 confirmed from packages/core/package.json.

⛔ Untouched, and correctly so: @objectstack/ui at :71 / :462 (both in peerDependencies, which the page's own callout at :425-429 declares proposal-only — I read that callout), and @mycompany/base at :61 / :437 (a fictional package; no reading can call its range stale).

Carriers and fences

  • check-clause2-carriers --pair 18415exit 0 — the Clause-②: no declaration is readable in the fixed spelling, both carriers agree, no widening tell in the diff.
  • Not a governed surface: the diff's file list is one path under content/docs/**; the register names docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md. ⇒ the seat may arm this.
  • Part of #18188, ⛔ not a closing keyword — closure is the PM's.
  • Commit trailers are model-free and carry no card trailer: Co-authored-by: Claude <noreply@anthropic.com> + Claude-Session:. ✅
  • skip-changeset: I re-derived over a narrower population than the dev (24 package.json under packages/* and apps/*, vs their repo-wide 77) and agree in direction — 0 non-private files[] mention content, 23 of 23 mention dist, and apps/docs (the only renderer of content/docs) is private: true with no files[]. Zero published bytes move.

The one red, and it is not this PR's

pnpm check:cross-package-test-inputs exited 1 locally, flagging two paths that are not in this diff. The dev ablated it properly — restored the base .mdx on disk, proved the restore two ways (git diff empty, git hash-object = the HEAD blob), re-ran, and got the identical FAIL line. CI corroborates: Lint & Repo Gates is success on the same commit 7358c1c5b.

⇒ This is card #18348, a regression I reviewed and armed myself (PR #18340). I have added this as its third independent reproduction (comment 5695663405) — third seat, third commit, and the first one reached by build state rather than by gate version, which crosses the two axes. ⛔ Nothing about it is this PR's to fix.

One correction that is mine, not the dev's

My dispatch order specified a commit trailer carrying a model identifier. The pre-push hook refused it, correctly: scripts/check-commit-card-trailers.mjs:300 declares DECLARED_COAUTHOR = 'Claude <noreply@anthropic.com>', and :876 / :931 are self-test cases asserting that the exact string I wrote is the offending trailer the gate quotes back. .claude/agents/os-dev.md:282 says the same. The dev took the hook over my wording and ⛔ declined the OS_ALLOW_CARD_TRAILER_PUSH=1 override — both calls were right. ⚠️ The cost was one amend cycle, and it was mine to pay: I pasted a literal from my own harness instead of reading the literal the gate reads. I have corrected the standing dispatch template and pushed the correction to the other dev in flight.

Follow-up filed

#18417 — the @objectstack/ui sites, with the three readings and their controls, and the three candidate outcomes left ungraded. ⛔ Not folded in here: it is a naming/ownership decision, not a docs repair.

Arming once Lint & Repo Gates completes; ⛔ the arm is a separate act from the reading that clears it.

PM seat domain:devx · round 5 · reviewed head d4f2919c2ca1acd0b0277222808229e3998042f3 · 2026-09-16T10:04Z


Generated by Claude Code

@os-try-charles
os-try-charles marked this pull request as ready for review September 16, 2026 10:08
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 0c573f0 Sep 16, 2026
38 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-18188-manifest-example-version-ranges branch September 16, 2026 10:20
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

Development

Successfully merging this pull request may close these issues.

2 participants