Skip to content

Commit 2cac363

Browse files
Samclaude
andauthored
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. ⚠️ **This head carries a base merge.** PR #19473 landed on `origin/main` and touched `packages/runtime/src/domains/packages.ts` (+96/−8) — carrier #9 of this diff — so the branch went `dirty` and `origin/main` was merged in at `32b5831c4e48ac9b1626322cf185b286a09073aa`. ⇒ **the base every line number in this body is measured against is `32b5831c`, not `fbc12be3`**, ⚠️ **and every pointer moved, not only `packages.ts`.** Each carrier file gained an `import … from './version-grammar'` line, so each carrier sits one line lower than it did at `fbc12be3`; `plugin.zod.ts` and `plugin-loader.ts` moved +4 because their comment blocks grew as well, and `packages.ts` moved `1680 → 1770` (the merge plus the resolved comment block). The tables above now carry BOTH columns, every cell verified line-by-line against this head with a required-substring check. An earlier revision of this body put the `fbc12be3` numbers in a column headed `line` beside one headed `after`, which resolved to the wrong line on the head that ships. `origin/main` has since reached `8015dc85` (one commit, #19471), which is ⛔ **not** an ancestor of this head and touches none of the 14 files here — `git diff --stat 32b5831 8015dc8` over the 14 paths is empty. The queue rebuilds on current `main` regardless. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4045781 commit 2cac363

14 files changed

Lines changed: 330 additions & 29 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/core': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
feat(spec): one declaration per version grammar — eight regex carriers of "the version of a package or plugin" now reference three exported constants
8+
9+
Clause-②: yes (widening)
10+
11+
**No accept set moves, and that is the whole point of this change.** Eight sites
12+
spelled a version regex out as a literal of their own. Five of those spellings
13+
were byte-identical to each other, two more were byte-identical to each other,
14+
and the eighth stood alone — three accept sets written eight times, growing on
15+
their own: three of the eight were published schema declarations with no parse
16+
caller at all, added by authors who copied a neighbour's literal. Each site now
17+
references the constant carrying the pattern it already enforced, byte for byte.
18+
A ninth in-repo carrier of the same concept spelled no regex at all:
19+
`PackageManifestSchema.version` is a bare `z.string()`, and it stays one here.
20+
21+
`@objectstack/spec/kernel` gains three exported patterns:
22+
23+
- `MAJOR_MINOR_PATCH_VERSION_PATTERN` — three numeric segments and nothing
24+
else. Referenced by `ManifestSchema.version`,
25+
`MetadataPluginManifestSchema.version`, `PluginRegistryEntrySchema.version`,
26+
`PluginMetadataSchema.version`, and the `PATCH /api/v1/packages/:id` door in
27+
`@objectstack/runtime`.
28+
- `SEMVER_SHAPED_VERSION_PATTERN` — `major.minor.patch` with an optional
29+
`-prerelease` and an optional `+build` suffix, identifiers in either ASCII
30+
case. Referenced by `PluginSchema.version` and by
31+
`PluginLoader.isSemverShapedVersion` in `@objectstack/core`. Those two
32+
converged on one spelling under the widen-never-narrow ruling and were held
33+
equal by hand until now; they reference one declaration and can no longer
34+
drift apart.
35+
- `SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN` — the same with the suffix
36+
identifiers restricted to lowercase ASCII. Referenced by
37+
`PackageVersionSchema.version`.
38+
39+
⛔ **The three are not interchangeable** — they are three different accept sets,
40+
and referencing the wrong one moves a published accept set. None of the three is
41+
a SemVer 2.0.0 conformance check and none is named as one: two accept forms
42+
SemVer forbids (leading zeroes in the numeric core, empty and leading-zero
43+
identifiers), one refuses forms it requires. For ordering or precedence,
44+
`dependency-resolver.ts` in `@objectstack/core` is still the module to extend.
45+
46+
**Nothing an author can write changes.** Every regex is byte-identical to the
47+
literal it replaces — verified per carrier by sha256 over the extracted literal
48+
— and every existing suite passes unedited. Those two together are the
49+
neutrality proof, and they are the whole of it. `PackageManifestSchema.version`
50+
keeps its bare `z.string()`; it is deliberately untouched here. No `.describe()`
51+
text, refusal message or JSON Schema `pattern` moves. Regenerating the spec's
52+
artifacts moved `api-surface/kernel.json` and `export-origins/kernel.json` and
53+
nothing else, each gaining the three constant names — ⛔ read that as a check
54+
that nothing unexpected regenerated, never as evidence about the accept set: the
55+
artifacts that stayed byte-unchanged do not record a `.regex()` pattern in the
56+
first place. A new pin,
57+
`src/kernel/version-grammar.test.ts`, records each grammar's verdict on twelve
58+
witness strings so the next deliberate move to any of them is one visible edit
59+
to one matrix.

‎packages/core/src/plugin-loader.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Logger } from '@objectstack/spec/contracts';
55
import { parseSignature } from './security/plugin-artifact-signature.js';
66
import { serviceNotRegisteredError } from './service-not-registered.js';
77
import { assertPluginContract } from './plugin-contract.js';
8+
import { SEMVER_SHAPED_VERSION_PATTERN } from '@objectstack/spec/kernel';
89

910
/**
1011
* Service Lifecycle Types
@@ -484,22 +485,24 @@ export class PluginLoader {
484485
* `/^\d+\.\d+\.\d+$/` admitted it too, because `\d+` always has — and
485486
* #16365's ruling (widen, never narrow: nothing that loads today stops
486487
* loading) froze the accept set. So #17070 moved the CLAIM instead of the
487-
* grammar: the regex below is byte-for-byte what it has been, and this
488+
* grammar: the grammar below is byte-for-byte what it has been, and this
488489
* method's name and this docblock are what changed.
489490
*
490491
* ⚠️ Need real SemVer 2.0.0 conformance — ordering, precedence, or a
491492
* standards-compliant verdict? This is not that predicate; do not reach for
492493
* it. `dependency-resolver.ts` parses and COMPARES versions and is the
493494
* module to extend.
494495
*
495-
* ⭐ This regex is `PluginSchema.version`'s spelling character for character
496-
* (`@objectstack/spec`, `kernel/plugin.zod.ts`) — the convergence #16365
497-
* created, and a property `plugin-loader.test.ts` asserts rather than
498-
* narrates. Change one spelling and you must change both.
496+
* ⭐ This predicate and `PluginSchema.version` (`@objectstack/spec`,
497+
* `kernel/plugin.zod.ts`) reference ONE declaration — the exported
498+
* `SEMVER_SHAPED_VERSION_PATTERN` (`@objectstack/spec/kernel`) — so the
499+
* convergence #16365 created can no longer be undone by editing one side.
500+
* Each used to spell the grammar out and the two were held equal character
501+
* for character by hand; `plugin-loader.test.ts` asserts the resulting
502+
* accept set rather than narrating it.
499503
*/
500504
private isSemverShapedVersion(version: string): boolean {
501-
const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/;
502-
return semverRegex.test(version);
505+
return SEMVER_SHAPED_VERSION_PATTERN.test(version);
503506
}
504507

505508
private async verifyPluginSignature(plugin: PluginMetadata): Promise<void> {

‎packages/runtime/src/domains/packages.ts‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,21 @@ import { stripReadDecorations } from '@objectstack/spec/kernel';
7575
// REFERENCE at the install door below. `ManifestSchema.shape.version` and
7676
// `ManifestSchema.shape.id` are the very field schemas
7777
// `PackageInstallRequestSchema` binds through `manifest: ManifestSchema` — not
78-
// copies of them. ⛔ A hand-written semver regex here would be the THIRD
79-
// judgment of that one key on this one surface (the `PATCH /packages/:id` door
80-
// further down already keeps its own copy), and the version-grammar canon is an
81-
// open question on its own card: asking the declaration means whatever that
82-
// canon decides reaches this door with no edit to this file. The same holds for
83-
// the id — `MANIFEST_ID_PATTERN` is declared ONCE in `kernel/manifest.zod.ts`
84-
// and shared with `PackageSchema.manifestId`, so ⛔ no reverse-domain regex is
85-
// spelled here either.
78+
// copies of them. ⛔ A hand-written semver regex here would be a SECOND grammar
79+
// for the version key on this one surface: the `PATCH /packages/:id` door
80+
// further down judges the same key and keeps no copy of its own either — it
81+
// references `MAJOR_MINOR_PATCH_VERSION_PATTERN`, the same constant
82+
// `ManifestSchema`'s own `version` field references (`@objectstack/spec`
83+
// `kernel/version-grammar.ts`). Both doors therefore read ONE declaration, and
84+
// a change to that declaration reaches both of them with no edit to this file.
85+
// The same holds for the id — `MANIFEST_ID_PATTERN` is declared ONCE in
86+
// `kernel/manifest.zod.ts` and shared with `PackageSchema.manifestId`, so ⛔ no
87+
// reverse-domain regex is spelled here either.
8688
//
8789
// `manifestIdRefusal` is imported for exactly one limb: the fallback when a
8890
// failed parse somehow carries no issue. Even that limb then prints the
8991
// DECLARATION's own sentence rather than a second one invented here.
90-
import { ManifestSchema, manifestIdRefusal } from '@objectstack/spec/kernel';
92+
import { ManifestSchema, MAJOR_MINOR_PATCH_VERSION_PATTERN, manifestIdRefusal } from '@objectstack/spec/kernel';
9193
// [#17672] The repo's ONE message for a single-valued query parameter supplied
9294
// more than once, from the module whose header is the authority on the rule
9395
// (`packages/rest/src/query-multiplicity.ts`). Imported, never restated: this
@@ -1765,7 +1767,7 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin
17651767
if (patch.name !== undefined && patch.name === '') {
17661768
return { handled: true, response: deps.error('name must not be empty', 400) };
17671769
}
1768-
if (patch.version !== undefined && !/^\d+\.\d+\.\d+$/.test(patch.version)) {
1770+
if (patch.version !== undefined && !MAJOR_MINOR_PATCH_VERSION_PATTERN.test(patch.version)) {
17691771
return { handled: true, response: deps.error('version must be semantic (e.g. 1.0.0)', 400) };
17701772
}
17711773
if (patch.name === undefined && patch.description === undefined && patch.version === undefined) {

‎packages/spec/api-surface/kernel.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"ListPackagesResponse (type)",
155155
"ListPackagesResponseParsed (type)",
156156
"ListPackagesResponseSchema (const)",
157+
"MAJOR_MINOR_PATCH_VERSION_PATTERN (const)",
157158
"MANIFEST_ID_EXAMPLES (const)",
158159
"MANIFEST_ID_PATTERN (const)",
159160
"METADATA_READ_DECORATIONS (const)",
@@ -377,6 +378,8 @@
377378
"SBOMParsed (type)",
378379
"SBOMSchema (const)",
379380
"SEED_WRITE_EXECUTION_CONTEXT (const)",
381+
"SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN (const)",
382+
"SEMVER_SHAPED_VERSION_PATTERN (const)",
380383
"SandboxConfig (type)",
381384
"SandboxConfigParsed (type)",
382385
"SandboxConfigSchema (const)",

‎packages/spec/export-origins/kernel.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
"ListPackagesResponse": "src/kernel/package-registry.zod.ts#ListPackagesResponse (type)",
154154
"ListPackagesResponseParsed": "src/kernel/package-registry.zod.ts#ListPackagesResponseParsed (type)",
155155
"ListPackagesResponseSchema": "src/kernel/package-registry.zod.ts#ListPackagesResponseSchema (const)",
156+
"MAJOR_MINOR_PATCH_VERSION_PATTERN": "src/kernel/version-grammar.ts#MAJOR_MINOR_PATCH_VERSION_PATTERN (const)",
156157
"MANIFEST_ID_EXAMPLES": "src/kernel/manifest.zod.ts#MANIFEST_ID_EXAMPLES (const)",
157158
"MANIFEST_ID_PATTERN": "src/kernel/manifest.zod.ts#MANIFEST_ID_PATTERN (const)",
158159
"METADATA_READ_DECORATIONS": "src/kernel/metadata-read-decorations.ts#METADATA_READ_DECORATIONS (const)",
@@ -375,6 +376,8 @@
375376
"SBOMParsed": "src/kernel/plugin-security.zod.ts#SBOMParsed (type)",
376377
"SBOMSchema": "src/kernel/plugin-security.zod.ts#SBOMSchema (const)",
377378
"SEED_WRITE_EXECUTION_CONTEXT": "src/kernel/execution-context.zod.ts#SEED_WRITE_EXECUTION_CONTEXT (const)",
379+
"SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN": "src/kernel/version-grammar.ts#SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN (const)",
380+
"SEMVER_SHAPED_VERSION_PATTERN": "src/kernel/version-grammar.ts#SEMVER_SHAPED_VERSION_PATTERN (const)",
378381
"SandboxConfig": "src/kernel/plugin-security-advanced.zod.ts#SandboxConfig (type)",
379382
"SandboxConfigParsed": "src/kernel/plugin-security-advanced.zod.ts#SandboxConfigParsed (type)",
380383
"SandboxConfigSchema": "src/kernel/plugin-security-advanced.zod.ts#SandboxConfigSchema (const)",

‎packages/spec/src/kernel/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,4 @@ export * from './plugin-security.zod';
140140
export * from './execution-context.zod';
141141
export * from './metadata-create-seeds';
142142
export * from './functional-completeness';
143+
export * from './version-grammar';

‎packages/spec/src/kernel/manifest.zod.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { z } from 'zod';
44
import { CORE_PLUGIN_TYPES } from './plugin.zod';
5+
import { MAJOR_MINOR_PATCH_VERSION_PATTERN } from './version-grammar';
56
import { retiredKey } from '../shared/retired-key';
67
import { strictObject } from '../shared/strict-object';
78
import { formatSuggestion } from '../shared/suggestions.zod';
@@ -411,7 +412,7 @@ export const ManifestSchema = strictObject({
411412
* @example "1.0.0"
412413
* @example "2.1.0"
413414
*/
414-
version: z.string().regex(/^\d+\.\d+\.\d+$/).describe('Package version (semantic versioning)'),
415+
version: z.string().regex(MAJOR_MINOR_PATCH_VERSION_PATTERN).describe('Package version (semantic versioning)'),
415416

416417
/**
417418
* Type of the package in the ObjectStack ecosystem.

‎packages/spec/src/kernel/metadata-plugin.zod.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { z } from 'zod';
44
import { retiredKey } from '../shared/retired-key';
55
import { MetadataManagerConfigSchema } from './metadata-loader.zod';
6+
import { MAJOR_MINOR_PATCH_VERSION_PATTERN } from './version-grammar';
67
import { ActionSchema } from '../ui/action.zod';
78

89
/**
@@ -646,7 +647,7 @@ export const MetadataPluginManifestSchema = lazySchema(() => z.object({
646647
name: z.literal('ObjectStack Metadata Service').describe('Plugin name'),
647648

648649
/** Plugin version */
649-
version: z.string().regex(/^\d+\.\d+\.\d+$/).describe('Plugin version'),
650+
version: z.string().regex(MAJOR_MINOR_PATCH_VERSION_PATTERN).describe('Plugin version'),
650651

651652
/** Plugin type */
652653
type: z.literal('standard').describe('Plugin type'),

‎packages/spec/src/kernel/plugin-registry.zod.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { z } from 'zod';
44
import { PluginCapabilityManifestSchema } from './plugin-capability.zod';
5+
import { MAJOR_MINOR_PATCH_VERSION_PATTERN } from './version-grammar';
56

67
/**
78
* # Plugin Registry Protocol
@@ -155,7 +156,7 @@ export const PluginRegistryEntrySchema = lazySchema(() => z.object({
155156
/**
156157
* Current version
157158
*/
158-
version: z.string().regex(/^\d+\.\d+\.\d+$/),
159+
version: z.string().regex(MAJOR_MINOR_PATCH_VERSION_PATTERN),
159160

160161
/**
161162
* Plugin display name

‎packages/spec/src/kernel/plugin-validator.zod.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { z } from 'zod';
4+
import { MAJOR_MINOR_PATCH_VERSION_PATTERN } from './version-grammar';
45

56
/**
67
* Plugin Validator Protocol
@@ -141,7 +142,7 @@ export const PluginMetadataSchema = lazySchema(() => z.object({
141142
/**
142143
* Plugin version (semver)
143144
*/
144-
version: z.string().regex(/^\d+\.\d+\.\d+$/).optional().describe('Semantic version (e.g., 1.0.0)'),
145+
version: z.string().regex(MAJOR_MINOR_PATCH_VERSION_PATTERN).optional().describe('Semantic version (e.g., 1.0.0)'),
145146

146147
/**
147148
* Plugin dependencies (array of plugin names)

0 commit comments

Comments
 (0)