Repository navigation
Commit 2cac363
feat(spec): one declaration per version grammar — eight carriers reference three exported constants (#19478)
Part of #18697 — construction round **R1 of 2 (phase 1 only)**. This
round does not close the card; R2 does.
Branch: `claude/issue-18697-version-grammar-canon-phase1`
Clause-②: yes (widening)
Reason: the criterion has two limbs and this round hits the second one.
The ACCEPT SET does not move — every regex written here is
byte-identical to the literal it replaces, verified per carrier by
sha256 over the extracted literal (G1 `0dc7272048754554`, G2
`f0503f4f0c703a40`, G3 `3e3cb2710d62909a`, each hash equal across its
whole group and equal to the constant), so nothing an author can write
changes. But the PUBLIC SURFACE grows: three new exported constants on
`@objectstack/spec/kernel`, landing as three rows in
`packages/spec/api-surface/kernel.json` at :157, :381 and :382. A new
exported symbol is `yes` on its own, and that second limb is what this
declares. The ruling's `Clause-②: yes` therefore already holds for R1;
R2 carries its own accept-set widening on top of it.
## What this does
Eight in-repo carriers of "the version of a package or plugin" each
spelled a version regex out as a literal of their own. Three accept sets
written eight times, still growing on their own: three of the eight were
published schema declarations with no parse caller at all. A ninth
carrier of the same concept, `PackageManifestSchema.version`, spelled no
regex at all — it is a bare `z.string()` and is deliberately left
unconstrained by this round. Each now references the constant carrying
the pattern it already enforced.
`@objectstack/spec/kernel` gains three exported patterns
(`packages/spec/src/kernel/version-grammar.ts`, a new file):
| constant | grammar |
|:--|:--|
| `MAJOR_MINOR_PATCH_VERSION_PATTERN` | three numeric segments and
nothing else |
| `SEMVER_SHAPED_VERSION_PATTERN` | plus an optional prerelease and an
optional build suffix, identifiers in either ASCII case |
| `SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN` | the same, suffix
identifiers restricted to lowercase ASCII |
10 carriers → 3 referenced declarations + 1 deliberately unconstrained.
## Per-carrier before → after, with the byte-identity assertion
Every "before" literal was extracted from the file BY LINE WITH A
REQUIRED MARKER on that line (a moved line throws rather than reading
the wrong thing) and hashed. Within each group every hash is equal, and
the constant's own literal hashes to the same value.
**G1 — `MAJOR_MINOR_PATCH_VERSION_PATTERN`, sha256/16
`0dc7272048754554`, 17 bytes**
```
/^\d+\.\d+\.\d+$/
```
| carrier | line at `fbc12be3` | line on this head | after |
|:--|:--|:--|:--|
| `packages/spec/src/kernel/manifest.zod.ts` `ManifestSchema.version` |
414 | **415** | `z.string().regex(MAJOR_MINOR_PATCH_VERSION_PATTERN)` |
| `packages/spec/src/kernel/metadata-plugin.zod.ts`
`MetadataPluginManifestSchema.version` | 649 | **650** | same |
| `packages/spec/src/kernel/plugin-registry.zod.ts`
`PluginRegistryEntrySchema.version` | 158 | **159** | same |
| `packages/spec/src/kernel/plugin-validator.zod.ts`
`PluginMetadataSchema.version` | 144 | **145** | same |
| `packages/runtime/src/domains/packages.ts` `PATCH
/api/v1/packages/:id` | 1680 | **1770** |
`!MAJOR_MINOR_PATCH_VERSION_PATTERN.test(patch.version)` |
**G2 — `SEMVER_SHAPED_VERSION_PATTERN`, sha256/16 `f0503f4f0c703a40`, 54
bytes**
```
/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/
```
| carrier | line at `fbc12be3` | line on this head | after |
|:--|:--|:--|:--|
| `packages/spec/src/kernel/plugin.zod.ts` `PluginSchema.version` | 212
| **216** | `z.string().regex(SEMVER_SHAPED_VERSION_PATTERN)` |
| `packages/core/src/plugin-loader.ts` `isSemverShapedVersion` | 501 |
**505** | `return SEMVER_SHAPED_VERSION_PATTERN.test(version)` |
**G3 — `SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN`, sha256/16
`3e3cb2710d62909a`, 48 bytes**
```
/^\d+\.\d+\.\d+(-[a-z0-9.-]+)?(\+[a-z0-9.-]+)?$/
```
| carrier | line at `fbc12be3` | line on this head | after |
|:--|:--|:--|:--|
| `packages/spec/src/marketplace/package-version.zod.ts`
`PackageVersionSchema.version` | 146 | **147** |
`.regex(SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN)` |
**G4 — untouched, on purpose.**
`packages/spec/src/marketplace/package-version.zod.ts:81` on this head
(`:80` at `fbc12be3`) — `PackageManifestSchema.version` — keeps its bare
`z.string()`. Giving it a grammar here would be an accept-set move and
would break this round's premise.
**Tenth carrier, out of reach from here.** `objectui`
`PackageFormDialog.tsx:39` / `:227` is a second repository and gets its
own card at this PR's ACCEPT.
## Why the fences held
- **No `.describe()` text, refusal string or JSON Schema `pattern`
moved.** The constant is the regex only; each carrier keeps its own
description. Moving the prose is phase 2's job, because the prose only
becomes true when the grammar moves.
- **Nothing was exported from the ROOT entry.** The constants are on
`./kernel`; `marketplace/package-version.zod.ts` references
`SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN` cross-entry via
`../kernel/version-grammar`, the precedent at
`marketplace/marketplace.zod.ts:4`.
`packages/spec/api-surface/root.json` is untouched, so PR #19373's hold
on that file is intact.
- **No `packages/spec/api-surface/*.json` was hand-edited**, and
`packages/spec/CHANGELOG.md` and `packages/spec/package.json` were never
opened.
- **The #16365 record in `plugin.zod.ts` is intact.** One sentence in it
became false — the one saying this key's regex is
`PluginLoader.isSemverShapedVersion`'s spelling character for character
— and it is corrected minimally, in place: the two now reference one
declaration and can no longer drift apart. The same correction is made
to that method's own docblock in `plugin-loader.ts`, where "Change one
spelling and you must change both" no longer describes anything. Nothing
about phase 2 is announced in either.
## Regenerated artifacts — gains only, on the allow-listed shards
`pnpm --filter @objectstack/spec check:generated` named exactly two
stale artifacts out of fifteen; both were regenerated with their own
generators, never by hand.
```
M packages/spec/api-surface/kernel.json (+3, -0)
M packages/spec/export-origins/kernel.json (+3, -0)
```
That is the whole of `git status` after regeneration.
`api-surface/root.json`, every `automation.json`,
`dropped-refinements.baseline.json`, `declaration-map/*` and
`json-schema.manifest/*` all came back byte-unchanged — which is itself
the neutrality reading: a pure literal-to-constant collapse produces the
identical Zod schema, so a moved `dropped-refinements` line would have
been evidence the refactor was not accept-set neutral.
`check:authorable-surface` and `check:declaration-map` pass with no
regeneration at all.
The six added rows are the three constant names in the two kernel
shards, and nothing is removed.
## The neutrality proof, and the instrument R2 will move
**Not one existing test expectation was edited.**
`packages/spec/src/kernel/plugin.test.ts` (which pins eight
forbidden-by-SemVer forms as ACCEPTED),
`packages/spec/src/kernel/manifest.test.ts` and
`packages/core/src/plugin-loader.test.ts` all pass unchanged — that is
the proof this cannot move a verdict.
New: `packages/spec/src/kernel/version-grammar.test.ts`, describe block
**`version grammars — accept-set pin`** — a 3 × 12 matrix pinning each
constant's verdict on `1.2.3`, `2.0.0-beta.1`, `1.0.0-Beta.1`,
`1.0.0+20230101`, `01.1.1`, `1.0.0-0123`, `1.0.0-alpha..1`, `1.0.0+.`,
`v1.0.0`, `1.0`, `latest` and the empty string, plus a source-bytes pin
per constant, a no-stateful-flag assertion, and the strict-containment
ordering (G1 inside G3 inside G2).
**Two ablations, each mutated through an anchor that had to hit, each
proved on disk, each restored byte-identically (`blob == HEAD`, `git
diff HEAD` empty):**
1. Widening `MAJOR_MINOR_PATCH_VERSION_PATTERN` by an optional `-beta`
group turns `manifest.test.ts > ManifestSchema > Basic Properties >
should enforce semantic versioning` **red** (1 failed / 65 passed). That
is the carrier wiring proved live: `ManifestSchema.version` really reads
the constant.
2. Widening it by an optional `-ABLATED` group turns the new pin's
**source-bytes** assertion red while the 3 × 12 matrix stays green — the
witness strings do not cover that shape. Reported because it is the
honest reading of what each half of the pin buys: the matrix pins
verdicts, the bytes pin catches an equivalent-looking rewrite the
witnesses cannot see. Both halves earn their place.
Cross-package wiring is visible in the built output too:
`packages/core/dist/index.js` and `packages/runtime/dist/index.js` both
reference the imported constant.
## Verification
| what | command | result |
|:--|:--|:--|
| build closure | `turbo run build --filter=@objectstack/runtime...
--filter=@objectstack/core... --filter=@objectstack/spec...
--concurrency=2` | 30/30 tasks successful |
| spec suite | `pnpm --filter @objectstack/spec test` | 507 files, 14837
tests passed |
| core suite | `pnpm --filter @objectstack/core test` | 51 files, 1321
tests passed |
| runtime suite | `pnpm --filter @objectstack/runtime test` | 271 files,
3761 passed / 1 skipped |
| typecheck | `turbo run typecheck` for spec, core, runtime | 32/32
tasks successful |
| derived gates | `node scripts/pm/dispatch-gates.mjs --commands` then
`--ran` | **89 derived, 86 run green, 3 NOT MEASURED, 0 UNRUN** |
The three NOT MEASURED are `check:dual-build-cjs-loads`, `check:i18n`
and `check:type-check-debt`, each exiting **3 — PREREQUISITE NOT MET**,
all three for the same reason: they read built output for the whole
workspace and only the spec/core/runtime closure was built here. Neither
red nor green; nothing was measured. CI builds the full closure and will
answer them. A fourth, `check-plugin-teardown-shape.mjs --self-test`,
also exited 3 on first run — its positive-control fixture is pinned to a
commit this shallow clone could not reach; after `git fetch --depth=200`
of that object it re-ran at **exit 0, 48 cases pass**.
## Enqueue: C5 fired, it was correct, and the declaration was corrected
Measured, not predicted:
```
node scripts/pm/check-widening-tells.mjs --declaration no --diff PR.DIFF -> exit 4
✗ T3 packages/spec/api-surface/kernel.json:157 — a new row in a published entry point's export listing
✗ T3 packages/spec/api-surface/kernel.json:381 — a new row in a published entry point's export listing
✗ T3 packages/spec/api-surface/kernel.json:382 — a new row in a published entry point's export listing
```
This PR was first pushed declaring `Clause-②: no`, on the reading that
an accept-set-neutral collapse is not a clause-② change. That reading
answered one conjunct of two. T3 was right: the public surface really
does grow by three exports, and the lane's criterion is
「放宽接受集**或扩大公开面**的卡,不论多小,即条款②」 — a new exported symbol is `yes` on its
own, whatever the accept set does.
The author escalated rather than flipping the declaration, and the
`domain:spec` seat re-declared `Clause-②: yes (widening)` in correction
comment 5754511685, superseding its earlier correction. **The question
is closed.** The instrument was not weakened and no card was filed
against it — the literals this diff removed were inline regexes inside
schemas, never on the public surface, so T3 measured exactly what it is
defined to measure. The accept-set half of the claim is untouched by any
of this and is still proved above, byte for byte.
## Contract review of record (row C6)
`domain:spec` owes an at-tier contract review on **every** round it
delivers, `Clause-②: yes` **or** `no`, and until that record exists this
PR is not landable. The record is written by an isolated
`CONTRACT_REVIEW_TIER` reviewer, not by the author. Its
`Implemented-by:` names
`claude/issue-18697-version-grammar-canon-phase1`.
## Changeset
`.changeset/18697-version-grammar-canon-phase1.md` — `@objectstack/spec:
minor` (new exported public constants), `@objectstack/core: patch`,
`@objectstack/runtime: patch`. No BREAKING banner: that belongs to R2.
## Base
Branched from `origin/main` `fbc12be318de0713e82e1f38ab804b5b63478e64`;
the dispatch order read `72eeabd3`, which had already moved when the
worktree was created.
1 parent 4045781 commit 2cac363
14 files changed
Lines changed: 330 additions & 29 deletions
File tree
- .changeset
- packages
- core/src
- runtime/src/domains
- spec
- api-surface
- export-origins
- src
- kernel
- marketplace
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
484 | 485 | | |
485 | 486 | | |
486 | 487 | | |
487 | | - | |
| 488 | + | |
488 | 489 | | |
489 | 490 | | |
490 | 491 | | |
491 | 492 | | |
492 | 493 | | |
493 | 494 | | |
494 | 495 | | |
495 | | - | |
496 | | - | |
497 | | - | |
498 | | - | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
499 | 503 | | |
500 | 504 | | |
501 | | - | |
502 | | - | |
| 505 | + | |
503 | 506 | | |
504 | 507 | | |
505 | 508 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
86 | 88 | | |
87 | 89 | | |
88 | 90 | | |
89 | 91 | | |
90 | | - | |
| 92 | + | |
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
| |||
1765 | 1767 | | |
1766 | 1768 | | |
1767 | 1769 | | |
1768 | | - | |
| 1770 | + | |
1769 | 1771 | | |
1770 | 1772 | | |
1771 | 1773 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| 157 | + | |
157 | 158 | | |
158 | 159 | | |
159 | 160 | | |
| |||
377 | 378 | | |
378 | 379 | | |
379 | 380 | | |
| 381 | + | |
| 382 | + | |
380 | 383 | | |
381 | 384 | | |
382 | 385 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
| 156 | + | |
156 | 157 | | |
157 | 158 | | |
158 | 159 | | |
| |||
375 | 376 | | |
376 | 377 | | |
377 | 378 | | |
| 379 | + | |
| 380 | + | |
378 | 381 | | |
379 | 382 | | |
380 | 383 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
| 143 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
411 | 412 | | |
412 | 413 | | |
413 | 414 | | |
414 | | - | |
| 415 | + | |
415 | 416 | | |
416 | 417 | | |
417 | 418 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
646 | 647 | | |
647 | 648 | | |
648 | 649 | | |
649 | | - | |
| 650 | + | |
650 | 651 | | |
651 | 652 | | |
652 | 653 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
155 | 156 | | |
156 | 157 | | |
157 | 158 | | |
158 | | - | |
| 159 | + | |
159 | 160 | | |
160 | 161 | | |
161 | 162 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
141 | 142 | | |
142 | 143 | | |
143 | 144 | | |
144 | | - | |
| 145 | + | |
145 | 146 | | |
146 | 147 | | |
147 | 148 | | |
| |||
0 commit comments