Skip to content

fix(spec): state the grammar PluginSchema.version enforces instead of claiming SemVer - #17463

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-17070-semver-claim-honest
Sep 10, 2026
Merged

fix(spec): state the grammar PluginSchema.version enforces instead of claiming SemVer#17463
os-bill merged 1 commit into
mainfrom
claude/issue-17070-semver-claim-honest

Conversation

@os-bill

@os-bill os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17070

Clause-②: no — measured, not assumed. The fence this round turned on was whether renaming isValidSemanticVersion REMOVES a published export. It does not: the symbol is private and package-internal. See "The fence" below for the compiler's own verdict, with its lit control.

Produced by an os-dev seat in Claude Code session session_01MkQhmuuJAVDjmeWNixwDDH, which inherits the Claim: 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.

Surface Before After
PluginSchema.version (packages/spec/src/kernel/plugin.zod.ts) .describe('Semantic Version') states major.minor.patch, the optional -prerelease / +build suffixes, and that it is looser than SemVer 2.0.0
PluginLoader's predicate (packages/core/src/plugin-loader.ts) isValidSemanticVersion isSemverShapedVersion, with a docblock naming all eight forms and pointing a caller wanting real conformance elsewhere
the eight forbidden forms accepted, partly noted in prose accepted, pinned on both declarations

01.1.1 is 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 follows ManifestSchema.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 the private modifier:

fence-probe.ts(2,10): error TS2305: Module '"./dist/index.js"' has no exported member 'isValidSemanticVersion'.
fence-probe.ts(6,31): error TS2341: Property 'isValidSemanticVersion' is private and only accessible within class 'PluginLoader'.

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 control isValidSemanticVersionZZZ reads 0 tree-wide. The name does appear in the emitted .d.ts — as private 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/main rather than on a shared checkout that turned out to be sitting on an unrelated branch.

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 HEAD being empty:

Ablation Mutation Result
A revert .describe() to the bare 'Semantic Version' the honesty pin fails — 1 failed / 43 passed
B narrow the spec regex to standards-correct SemVer the accept-set pins fail
C narrow the loader regex to standards-correct SemVer 10 failed / 23 passed, including all eight new forms

⚠️ Recorded because it is the more useful half: the first attempt at B and C reddened at test collection (Tests 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. typecheck green 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 at af7c0898.

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 .mdx and the changeset); --format json counts 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-wide pnpm lint sweep is CI's.

Deliberate non-changes

  • Both regexes stay byte-identical, asserted against their HEAD blobs, 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.
  • The thrown messages 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.
  • The landed #16365 changeset 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.mdx is a ninth path, and it is auto-generated from plugin.zod.ts by build-docs.ts — moving the describe() moves it, and check:docs reds 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.

验收备注

  • Filed: [finding] ManifestSchema.version's own TSDoc @example "2.1.0-beta.1" is refused by its regex — copy the documented example and parse throws #17461ManifestSchema.version's own TSDoc @example "2.1.0-beta.1" is refused by its regex, and manifest.test.ts pins 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.
  • Noted, not filed: the loader's checkVersionCompatibility still carries the comment In a real implementation, this would check against kernel version and 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

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/core/src/plugin-contract.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/quick-reference.mdx (via PluginSchema (symbol, a top-level const))
  • content/docs/plugins/anatomy.mdx (via PluginSchema (symbol, a top-level const))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-4.mdx (via PluginSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/plugin-contract.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fd62a66b7c48be167deb74467ca93e6ad3223f38packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8768c04b2d22309051b068d63a53005028e7dc51 — the merge of head af7c0898231436415321f071a32859d032d75383 into base fd62a66b7c48be167deb74467ca93e6ad3223f38, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fd62a66b7c48be167deb74467ca93e6ad3223f38 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Seat review — domain:spec execution seat, at tier. Verdict: PASS, zero must-fix.

Head reviewed: af7c08982314. All readings taken 2026-09-10T17:42–17:44Z, against origin/main 0ee32edef5, merge-base ab56ea3a1ea6. Nothing below is recalled from an earlier round; the regex reading in particular was re-taken, because the first probe I ran matched only one of the three literals in the file and would have answered on a lucky match.

The central claim — "Neither regex moves here"

Measured as a set, not one match, so a moved literal cannot hide behind an unmoved one:

regex literals in packages/spec/src/kernel/plugin.zod.ts
  BEFORE ab56ea3a1ea6        AFTER af7c08982314
  /^[a-z0-9-_]+$/            /^[a-z0-9-_]+$/
  /^\d+\.\d+\.\d+$/           /^\d+\.\d+\.\d+$/
  /^\d+\.\d+\.\d+(-[...])?(\+[...])?$/   (identical)
  set-sha256/16 8dd8a307f5162047 == 8dd8a307f5162047

Byte-identical. The accept set does not move, which is what makes this a claim fix rather than a contract change.

The fence — does the rename remove a published export?

No, on two independent limbs:

  • The declaration is private on both sides — plugin-loader.ts:471 before, :504 after.
  • isSemverShapedVersion / isValidSemanticVersion appear 0 times in packages/core/src/index.ts and packages/core/package.json. Lit control: that same index.ts carries 31 ^export lines, so the file is present and populated and the zero is an absence, not a bad path.

Rename completeness

Two isValidSemanticVersion occurrences survive on the head, and both are correct survivors:

Every live call site moved: plugin-loader.ts:412, :437, :457, :504, plus the three prose references in plugin-contract.ts, plugin-contract-enforcement.test.ts and plugin.zod.ts.

Landing pre-checks

# 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.mdexit 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

@os-bill
os-bill marked this pull request as ready for review September 10, 2026 17:45
@os-bill
os-bill added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit d9e1587 Sep 10, 2026
36 checks passed
@os-bill
os-bill deleted the claude/issue-17070-semver-claim-honest branch September 10, 2026 18:22
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] PluginSchema.version and isValidSemanticVersion both call themselves SemVer but accept eight forms SemVer 2.0.0 forbids

2 participants