fix(spec): state the grammar PluginSchema.version enforces instead of claiming SemVer - #17463
Conversation
…of claiming SemVer
`PluginSchema.version` (`kernel/plugin.zod.ts`) was described `"Semantic
Version"`, bare, and `PluginLoader.isValidSemanticVersion` (`packages/core`)
carried that name — while the one regex they share, character for character,
accepts eight strings SemVer 2.0.0 forbids:
§2 leading zeroes in the numeric core — 01.1.1, 1.01.1, 1.1.01
§9 empty / leading-zero prerelease ids — 1.0.0-0123, 1.0.0-alpha..1,
1.0.0-alpha.., 1.0.0-.
§10 degenerate build metadata — 1.0.0+.
Neither regex moves here, in either direction, and all eight keep parsing.
The accept set is frozen: the leading-zero half predates the widening that
gave this key its suffix groups — the original /^\d+\.\d+\.\d+$/ admitted
01.1.1 too, because \d+ always has — so narrowing to the official SemVer
regex would refuse plugin objects that load today, which the ruling on this
key forbids. With one side of the declared/enforced pair frozen, the repair
is on the other side: the claim.
- the describe() states the shape (major.minor.patch, optional -prerelease
and +build) and disclaims the standard it exceeds, following
ManifestSchema.version, which already spells (major.minor.patch) rather
than leaning on the word SemVer
- isValidSemanticVersion becomes isSemverShapedVersion, because a predicate
named for SemVer that answers a wider grammar gets misused by the next
caller no matter what its docblock says. The symbol is private and
package-internal — measured against the built dist/index.d.ts: TS2305 on
a named import, TS2341 on member access, while a public member on the
same class compiles — so nothing published is removed
- all eight forms are pinned as ACCEPTED on both declarations, so a future
edit that "corrects" the grammar fails on purpose, and the describe() is
pinned against reverting to the bare claim
content/docs/references/kernel/plugin.mdx is regenerated (build-docs.ts);
packages/spec/json-schema/ is gitignored and carries the same string.
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 2 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 — 140 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 8768c04b2d22309051b068d63a53005028e7dc51 && git checkout 8768c04b2d22309051b068d63a53005028e7dc51
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fd62a66b7c48be167deb74467ca93e6ad3223f38 af7c0898231436415321f071a32859d032d75383 && git checkout -B drift-repro fd62a66b7c48be167deb74467ca93e6ad3223f38 && git merge --no-ff af7c0898231436415321f071a32859d032d75383
node scripts/docs-audit/affected-docs.mjs --json fd62a66b7c48be167deb74467ca93e6ad3223f38
|
Seat review —
|
| # | check | reading |
|---|---|---|
| ① | at-tier verdict for this head | PASS (this comment). Clause-② does not fire — declaration is no and ② confirms both carriers agree |
| ② | check-clause2-carriers --pair 17463 |
exit 0 — declaration readable in the fixed spelling, both carriers agree, no widening tell |
| ③ | check-governed-merges --test, final 8-path list |
exit 0 (not governed). Lit control AGENTS.md ⇒ exit 3; empty-list control ⇒ exit 1. The predicate discriminates |
| ④ | checks on af7c08982314 |
34 distinct check NAMES newest-run-per-name: 32 success, 2 skipped, 0 not-green; combined commit status success |
③ was re-measured after I caught the first run reporting EXIT=$? through a pipe — that reads tail's status, not node's. The codes above are from unpiped runs.
Both gates were run from a clean worktree at origin/main 0ee32edef5, not from the shared checkout: both check-governed-merges.mjs and check-clause2-carriers.mjs differ from origin/main in that checkout, which sits on another agent's branch.
Advisory, not blocking
content/docs/references/kernel/plugin.mdx is routed merge=os-regen in .gitattributes. That driver has been measured exiting 0 while silently dropping one side (#17334's round). mergeable_state is clean so no merge is expected, but the landing verification for this PR will probe that file's changed line specifically rather than trusting the merge event.
Proceeding to flip ready and enqueue.
Generated by Claude Code
Fixes #17070
Clause-②: no — measured, not assumed. The fence this round turned on was whether renaming
isValidSemanticVersionREMOVES a published export. It does not: the symbol isprivateand package-internal. See "The fence" below for the compiler's own verdict, with its lit control.Produced by an
os-devseat in Claude Code sessionsession_01MkQhmuuJAVDjmeWNixwDDH, which inherits theClaim:on #17070 and posted no second claim.What this changes
Two declarations called themselves a semantic version and accepted eight strings SemVer 2.0.0 forbids. Neither regex moves here. All eight keep parsing. What changed is what the code claims.
PluginSchema.version(packages/spec/src/kernel/plugin.zod.ts).describe('Semantic Version')major.minor.patch, the optional-prerelease/+buildsuffixes, and that it is looser than SemVer 2.0.0PluginLoader's predicate (packages/core/src/plugin-loader.ts)isValidSemanticVersionisSemverShapedVersion, with a docblock naming all eight forms and pointing a caller wanting real conformance elsewhere01.1.1is older than every card here — the original/^\d+\.\d+\.\d+$/admitted it too, because\d+always has — and #16365 ruled widen-never-narrow. With the accept set frozen, the only side of the declared/enforced pair still free to move is the claim. Prose followsManifestSchema.version, which already spells(major.minor.patch)explicitly instead of leaning on the word SemVer.The fence — is the renamed symbol published?
Measured against the built
packages/core/dist/index.d.ts, using the compiler as its own predicate rather than reading theprivatemodifier:Lit control on the same probe:
loader.loadPlugin(a genuinely public member of the same published class) compiles with no error, so the module resolved and the probe is real. Dark controlisValidSemanticVersionZZZreads 0 tree-wide. The name does appear in the emitted.d.ts— asprivate isValidSemanticVersion;, with no signature, which no consumer can call. Package-internal ⇒ the rename removes nothing published ⇒Clause-②: no.In-tree sweep before the rename: 6 occurrences (dark control 0), all in
plugin-loader.ts,plugin-contract.ts,plugin-contract-enforcement.test.ts, plus one in a landed changeset left untouched as a historical record. The claim estimated 9; the smaller true count does not change the verdict.Premises, falsified first
All three held, on
origin/mainrather than on a shared checkout that turned out to be sitting on an unrelated branch.PluginSchema.versionrefuses the prerelease and build-metadata forms SemVer defines, while the loader that actually runs accepts them #16365 has landed and still says what the card reports.PluginSchema.versiononorigin/maincarries the widened regex and the#16365comment block.sha256/16 = f0503f4f0c703a40, length 54. They have not diverged.true.Enforced, not narrated
The pins are proved to be able to fail, by mutation, from the committed state — each mutation proved on disk before its run, each restore proved by
git diff HEADbeing empty:.describe()to the bare'Semantic Version'1 failed / 43 passed10 failed / 23 passed, including all eight new formsTests no tests) — a broken mutation, not a failing pin. A red for the wrong reason is NOT MEASURED, so both were redone with an exact replacement, and the table above is the second run.Verification
pnpm --filter @objectstack/spec test— 469 files, 13220 passed.pnpm --filter @objectstack/core test— 51 files, 1292 passed.typecheckgreen on both packages. Gates run green:check:generated,check:docs,check:authorable-surface,check:api-surface,check:spec-changes,check:upgrade-guide,check:migration-registry,check:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:spec-parsed-alias,check:closing-keyword-parity,check:empty-changeset,check:changeset-no-major,check:adr-0087-registration,check:type-check-coverage,check:pm-widening-tells, and the docs family. All ataf7c0898.Lint is a declared narrowing, measured on three counts: eslint's own config reports 6 of the 8 changed paths as linted and 2 as ignored (the
.mdxand the changeset);--format jsoncounts 6 files, 0 errors, 0 warnings; and type-aware linting is disabled in this config, so this diff cannot move the verdict on any untouched file. The repo-widepnpm lintsweep is CI's.Deliberate non-changes
HEADblobs, not eyeballed. No accept set moves in either direction, so no ADR-0087 conversion entry is owed and none of the three fenced hot paths is touched.Invalid semantic version: ...are left alone. They are sound in the direction that matters: the grammar is a strict superset of SemVer, so everything it rejects really is invalid SemVer. The message only ever speaks about rejections, and never asserts that an accepted value is SemVer. Changing it would also break a pin for no gain.#16365changeset still names the old symbol. It records what that change did at the time it did it; rewriting history in a pending release note is not this card's business.⭐ File face grew by one path — reporting, not widening
The claim's declared face is the two source files, the call sites, their tests and a changeset.
content/docs/references/kernel/plugin.mdxis a ninth path, and it is auto-generated fromplugin.zod.tsbybuild-docs.ts— moving thedescribe()moves it, andcheck:docsreds without it.packages/spec/json-schema/carries the same string but is gitignored, so it produces no diff. Flagged for the seat rather than absorbed silently.验收备注
ManifestSchema.version's own TSDoc@example "2.1.0-beta.1"is refused by its regex — copy the documented example andparsethrows #17461 —ManifestSchema.version's own TSDoc@example "2.1.0-beta.1"is refused by its regex, andmanifest.test.tspins the same class invalid. Found while reading that key as the precedent for this change. Category (a): a documented example that fails when copied verbatim. Deduped against open cards first.checkVersionCompatibilitystill carries the commentIn a real implementation, this would check against kernel versionand compares nothing. Pre-existing, not a defect against any declared contract. Carrier: none — no open PR or queued card touches this method.Generated by Claude Code