From 0618dae3f96fa385b53394203325f1d605155a78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 05:55:01 +0000 Subject: [PATCH 1/2] Initial plan From 2320d56ffb29c5202ea27f776affde55d33d5a72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 06:01:00 +0000 Subject: [PATCH 2/2] Add version schema utilities to shared protocol Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com> --- content/docs/references/shared/index.mdx | 1 + content/docs/references/shared/meta.json | 3 +- .../json-schema/shared/ReleaseChannel.json | 7 + .../json-schema/shared/SemanticVersion.json | 7 + .../json-schema/shared/VersionConstraint.json | 7 + .../json-schema/shared/VersionMetadata.json | 7 + .../spec/json-schema/shared/VersionRange.json | 7 + packages/spec/src/shared/index.ts | 1 + packages/spec/src/shared/version.test.ts | 277 ++++++++++++++++++ packages/spec/src/shared/version.zod.ts | 199 +++++++++++++ 10 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 packages/spec/json-schema/shared/ReleaseChannel.json create mode 100644 packages/spec/json-schema/shared/SemanticVersion.json create mode 100644 packages/spec/json-schema/shared/VersionConstraint.json create mode 100644 packages/spec/json-schema/shared/VersionMetadata.json create mode 100644 packages/spec/json-schema/shared/VersionRange.json create mode 100644 packages/spec/src/shared/version.test.ts create mode 100644 packages/spec/src/shared/version.zod.ts diff --git a/content/docs/references/shared/index.mdx b/content/docs/references/shared/index.mdx index 96d553be1..bbf0016b3 100644 --- a/content/docs/references/shared/index.mdx +++ b/content/docs/references/shared/index.mdx @@ -11,5 +11,6 @@ This section contains all protocol schemas for the shared layer of ObjectStack. + diff --git a/content/docs/references/shared/meta.json b/content/docs/references/shared/meta.json index 5ad22d319..032a67262 100644 --- a/content/docs/references/shared/meta.json +++ b/content/docs/references/shared/meta.json @@ -3,6 +3,7 @@ "pages": [ "http", "identifiers", - "mapping" + "mapping", + "version" ] } \ No newline at end of file diff --git a/packages/spec/json-schema/shared/ReleaseChannel.json b/packages/spec/json-schema/shared/ReleaseChannel.json new file mode 100644 index 000000000..aaa5ec45d --- /dev/null +++ b/packages/spec/json-schema/shared/ReleaseChannel.json @@ -0,0 +1,7 @@ +{ + "$ref": "#/definitions/ReleaseChannel", + "definitions": { + "ReleaseChannel": {} + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/shared/SemanticVersion.json b/packages/spec/json-schema/shared/SemanticVersion.json new file mode 100644 index 000000000..2a906e9e2 --- /dev/null +++ b/packages/spec/json-schema/shared/SemanticVersion.json @@ -0,0 +1,7 @@ +{ + "$ref": "#/definitions/SemanticVersion", + "definitions": { + "SemanticVersion": {} + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/shared/VersionConstraint.json b/packages/spec/json-schema/shared/VersionConstraint.json new file mode 100644 index 000000000..7784609f3 --- /dev/null +++ b/packages/spec/json-schema/shared/VersionConstraint.json @@ -0,0 +1,7 @@ +{ + "$ref": "#/definitions/VersionConstraint", + "definitions": { + "VersionConstraint": {} + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/shared/VersionMetadata.json b/packages/spec/json-schema/shared/VersionMetadata.json new file mode 100644 index 000000000..3ead4d68e --- /dev/null +++ b/packages/spec/json-schema/shared/VersionMetadata.json @@ -0,0 +1,7 @@ +{ + "$ref": "#/definitions/VersionMetadata", + "definitions": { + "VersionMetadata": {} + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/shared/VersionRange.json b/packages/spec/json-schema/shared/VersionRange.json new file mode 100644 index 000000000..a6f8e89b8 --- /dev/null +++ b/packages/spec/json-schema/shared/VersionRange.json @@ -0,0 +1,7 @@ +{ + "$ref": "#/definitions/VersionRange", + "definitions": { + "VersionRange": {} + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/shared/index.ts b/packages/spec/src/shared/index.ts index efa0e42b5..db1d9a953 100644 --- a/packages/spec/src/shared/index.ts +++ b/packages/spec/src/shared/index.ts @@ -6,3 +6,4 @@ export * from './identifiers.zod'; export * from './mapping.zod'; export * from './http.zod'; +export * from './version.zod'; diff --git a/packages/spec/src/shared/version.test.ts b/packages/spec/src/shared/version.test.ts new file mode 100644 index 000000000..28c8e5d61 --- /dev/null +++ b/packages/spec/src/shared/version.test.ts @@ -0,0 +1,277 @@ +import { describe, it, expect } from 'vitest'; +import { + SemanticVersionSchema, + VersionRangeSchema, + VersionConstraintSchema, + ReleaseChannelSchema, + VersionMetadataSchema, +} from './version.zod'; + +describe('Version Schemas', () => { + describe('SemanticVersionSchema', () => { + describe('Valid versions', () => { + it('should accept basic semantic versions', () => { + expect(() => SemanticVersionSchema.parse('0.0.0')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('0.1.0')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('0.0.1')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('10.20.30')).not.toThrow(); + }); + + it('should accept versions with prerelease', () => { + expect(() => SemanticVersionSchema.parse('1.0.0-alpha')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0-alpha.1')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0-beta.2')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0-rc.1')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0-0.3.7')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0-x.7.z.92')).not.toThrow(); + }); + + it('should accept versions with build metadata', () => { + expect(() => SemanticVersionSchema.parse('1.0.0+20130313144700')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0+exp.sha.5114f85')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0+21AF26D3-117B344092BD')).not.toThrow(); + }); + + it('should accept versions with prerelease and build metadata', () => { + expect(() => SemanticVersionSchema.parse('1.0.0-alpha+001')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0-beta+exp.sha.5114f85')).not.toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.0-rc.1+build.123')).not.toThrow(); + }); + + it('should accept large version numbers', () => { + expect(() => SemanticVersionSchema.parse('999.999.999')).not.toThrow(); + }); + }); + + describe('Invalid versions', () => { + it('should reject versions missing components', () => { + expect(() => SemanticVersionSchema.parse('1')).toThrow(); + expect(() => SemanticVersionSchema.parse('1.0')).toThrow(); + }); + + it('should reject versions with v prefix', () => { + expect(() => SemanticVersionSchema.parse('v1.0.0')).toThrow(); + expect(() => SemanticVersionSchema.parse('V1.0.0')).toThrow(); + }); + + it('should reject versions with leading zeros', () => { + expect(() => SemanticVersionSchema.parse('01.0.0')).toThrow(); + expect(() => SemanticVersionSchema.parse('1.01.0')).toThrow(); + expect(() => SemanticVersionSchema.parse('1.0.01')).toThrow(); + }); + + it('should reject versions with trailing dots', () => { + expect(() => SemanticVersionSchema.parse('1.0.0.')).toThrow(); + }); + + it('should reject non-numeric versions', () => { + expect(() => SemanticVersionSchema.parse('a.b.c')).toThrow(); + expect(() => SemanticVersionSchema.parse('1.a.0')).toThrow(); + }); + + it('should reject empty or invalid strings', () => { + expect(() => SemanticVersionSchema.parse('')).toThrow(); + expect(() => SemanticVersionSchema.parse('invalid')).toThrow(); + }); + }); + }); + + describe('VersionRangeSchema', () => { + it('should accept exact versions', () => { + expect(() => VersionRangeSchema.parse('1.0.0')).not.toThrow(); + }); + + it('should accept caret ranges', () => { + expect(() => VersionRangeSchema.parse('^1.0.0')).not.toThrow(); + expect(() => VersionRangeSchema.parse('^0.2.3')).not.toThrow(); + }); + + it('should accept tilde ranges', () => { + expect(() => VersionRangeSchema.parse('~1.0.0')).not.toThrow(); + expect(() => VersionRangeSchema.parse('~1.2')).not.toThrow(); + }); + + it('should accept comparison operators', () => { + expect(() => VersionRangeSchema.parse('>=1.0.0')).not.toThrow(); + expect(() => VersionRangeSchema.parse('>1.0.0')).not.toThrow(); + expect(() => VersionRangeSchema.parse('<=2.0.0')).not.toThrow(); + expect(() => VersionRangeSchema.parse('<2.0.0')).not.toThrow(); + }); + + it('should accept wildcards', () => { + expect(() => VersionRangeSchema.parse('*')).not.toThrow(); + expect(() => VersionRangeSchema.parse('1.x')).not.toThrow(); + expect(() => VersionRangeSchema.parse('1.0.*')).not.toThrow(); + }); + + it('should accept range expressions', () => { + expect(() => VersionRangeSchema.parse('>=1.0.0 <2.0.0')).not.toThrow(); + expect(() => VersionRangeSchema.parse('1.0.0 - 2.0.0')).not.toThrow(); + }); + + it('should reject empty strings', () => { + expect(() => VersionRangeSchema.parse('')).toThrow(); + }); + }); + + describe('VersionConstraintSchema', () => { + it('should accept valid constraints', () => { + const constraint1 = VersionConstraintSchema.parse({ + operator: '>=', + version: '1.0.0', + }); + expect(constraint1.operator).toBe('>='); + expect(constraint1.version).toBe('1.0.0'); + + const constraint2 = VersionConstraintSchema.parse({ + operator: '^', + version: '2.1.0', + }); + expect(constraint2.operator).toBe('^'); + expect(constraint2.version).toBe('2.1.0'); + }); + + it('should accept all valid operators', () => { + const operators = ['=', '>', '>=', '<', '<=', '^', '~']; + operators.forEach((op) => { + expect(() => + VersionConstraintSchema.parse({ + operator: op, + version: '1.0.0', + }) + ).not.toThrow(); + }); + }); + + it('should reject invalid operators', () => { + expect(() => + VersionConstraintSchema.parse({ + operator: '!=', + version: '1.0.0', + }) + ).toThrow(); + }); + + it('should reject invalid versions', () => { + expect(() => + VersionConstraintSchema.parse({ + operator: '>=', + version: 'invalid', + }) + ).toThrow(); + }); + }); + + describe('ReleaseChannelSchema', () => { + it('should accept all valid channels', () => { + const channels = ['stable', 'beta', 'alpha', 'nightly', 'canary']; + channels.forEach((channel) => { + expect(() => ReleaseChannelSchema.parse(channel)).not.toThrow(); + }); + }); + + it('should reject invalid channels', () => { + expect(() => ReleaseChannelSchema.parse('production')).toThrow(); + expect(() => ReleaseChannelSchema.parse('dev')).toThrow(); + expect(() => ReleaseChannelSchema.parse('test')).toThrow(); + }); + }); + + describe('VersionMetadataSchema', () => { + it('should accept minimal version metadata', () => { + const metadata = VersionMetadataSchema.parse({ + version: '1.0.0', + }); + expect(metadata.version).toBe('1.0.0'); + expect(metadata.channel).toBe('stable'); // default + }); + + it('should accept complete version metadata', () => { + const metadata = VersionMetadataSchema.parse({ + version: '1.2.3', + channel: 'beta', + buildNumber: '12345', + gitCommit: 'a1b2c3d4e5f', + publishedAt: '2024-01-15T10:30:00Z', + }); + + expect(metadata.version).toBe('1.2.3'); + expect(metadata.channel).toBe('beta'); + expect(metadata.buildNumber).toBe('12345'); + expect(metadata.gitCommit).toBe('a1b2c3d4e5f'); + expect(metadata.publishedAt).toBe('2024-01-15T10:30:00Z'); + }); + + it('should accept version metadata with custom metadata', () => { + const versionData = VersionMetadataSchema.parse({ + version: '1.2.3', + metadata: { + platform: 'linux', + arch: 'x64', + }, + }); + + expect(versionData.metadata).toEqual({ + platform: 'linux', + arch: 'x64', + }); + }); + + it('should use default channel when not specified', () => { + const metadata = VersionMetadataSchema.parse({ + version: '1.0.0', + }); + expect(metadata.channel).toBe('stable'); + }); + + it('should accept valid datetime for publishedAt', () => { + expect(() => + VersionMetadataSchema.parse({ + version: '1.0.0', + publishedAt: '2024-01-15T10:30:00Z', + }) + ).not.toThrow(); + + expect(() => + VersionMetadataSchema.parse({ + version: '1.0.0', + publishedAt: '2024-01-15T10:30:00.123Z', + }) + ).not.toThrow(); + }); + + it('should reject invalid datetime for publishedAt', () => { + expect(() => + VersionMetadataSchema.parse({ + version: '1.0.0', + publishedAt: 'invalid-date', + }) + ).toThrow(); + + expect(() => + VersionMetadataSchema.parse({ + version: '1.0.0', + publishedAt: '2024-01-15', + }) + ).toThrow(); + }); + + it('should reject invalid version', () => { + expect(() => + VersionMetadataSchema.parse({ + version: 'invalid', + }) + ).toThrow(); + }); + + it('should reject invalid channel', () => { + expect(() => + VersionMetadataSchema.parse({ + version: '1.0.0', + channel: 'invalid', + }) + ).toThrow(); + }); + }); +}); diff --git a/packages/spec/src/shared/version.zod.ts b/packages/spec/src/shared/version.zod.ts new file mode 100644 index 000000000..fa9afd3b2 --- /dev/null +++ b/packages/spec/src/shared/version.zod.ts @@ -0,0 +1,199 @@ +import { z } from 'zod'; + +/** + * Version Schemas + * + * Standardized version schemas for package versioning across ObjectStack. + * Supports semantic versioning (SemVer) and other common versioning patterns. + * + * Used by: + * - system/manifest.zod.ts (Package versions) + * - system/plugin.zod.ts (Plugin versions) + * - hub/marketplace.zod.ts (Marketplace versions) + * + * @see https://semver.org/ - Semantic Versioning 2.0.0 + */ + +// ========================================== +// Semantic Version (SemVer) +// ========================================== + +/** + * Semantic Version Schema (SemVer 2.0.0) + * + * Validates semantic version strings in the format: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD] + * + * **Format:** `X.Y.Z` where X, Y, and Z are non-negative integers + * - X = MAJOR version (incompatible API changes) + * - Y = MINOR version (backwards-compatible functionality) + * - Z = PATCH version (backwards-compatible bug fixes) + * + * Optional: + * - PRERELEASE: `-alpha.1`, `-beta.2`, `-rc.1` + * - BUILD: `+20130313144700`, `+exp.sha.5114f85` + * + * @example Valid versions + * - '1.0.0' + * - '0.1.0' + * - '2.1.3' + * - '1.0.0-alpha' + * - '1.0.0-alpha.1' + * - '1.0.0-0.3.7' + * - '1.0.0-x.7.z.92' + * - '1.0.0+20130313144700' + * - '1.0.0-beta+exp.sha.5114f85' + * + * @example Invalid versions + * - '1' (missing minor and patch) + * - '1.0' (missing patch) + * - 'v1.0.0' (no 'v' prefix allowed) + * - '1.0.0.' (trailing dot) + */ +export const SemanticVersionSchema = z + .string() + .regex( + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/, + { + message: 'Version must follow semantic versioning format (e.g., "1.0.0", "1.0.0-alpha.1", "1.0.0+build.123")', + } + ) + .describe('Semantic version (SemVer 2.0.0)'); + +export type SemanticVersion = z.infer; + +// ========================================== +// Version Range +// ========================================== + +/** + * Version Range Schema + * + * Supports common version range patterns used in package dependencies. + * + * **Patterns:** + * - Exact: `1.0.0` + * - Caret: `^1.0.0` (compatible with version, allows changes that do not modify the left-most non-zero digit) + * - Tilde: `~1.0.0` (allows patch-level changes) + * - Wildcard: `1.0.*` or `1.*` or `*` + * - Comparison: `>=1.0.0`, `>1.0.0`, `<=2.0.0`, `<2.0.0` + * - Range: `>=1.0.0 <2.0.0` or `1.0.0 - 2.0.0` + * + * @example Valid ranges + * - '1.0.0' (exact) + * - '^1.0.0' (caret - allows 1.x.x) + * - '~1.0.0' (tilde - allows 1.0.x) + * - '>=1.0.0' (greater than or equal) + * - '*' (any version) + * - '1.x' (any 1.x.x version) + */ +export const VersionRangeSchema = z + .string() + .min(1, { message: 'Version range cannot be empty' }) + .describe('Version range (e.g., "^1.0.0", ">=1.0.0 <2.0.0", "*")'); + +export type VersionRange = z.infer; + +// ========================================== +// Version Comparison +// ========================================== + +/** + * Version Constraint Schema + * + * Represents a version constraint with an operator and version. + * + * @example + * ```typescript + * { + * operator: '>=', + * version: '1.0.0' + * } + * ``` + */ +export const VersionConstraintSchema = z.object({ + /** + * Comparison operator + */ + operator: z.enum(['=', '>', '>=', '<', '<=', '^', '~']).describe('Comparison operator'), + + /** + * Version to compare against + */ + version: SemanticVersionSchema.describe('Version to compare against'), +}); + +export type VersionConstraint = z.infer; + +// ========================================== +// Release Channel +// ========================================== + +/** + * Release Channel Enum + * + * Defines the stability/maturity level of a release. + * Used for versioning and update strategies. + */ +export const ReleaseChannelSchema = z.enum([ + 'stable', // Production-ready releases + 'beta', // Feature-complete but may have bugs + 'alpha', // Early preview, unstable + 'nightly', // Daily builds from main branch + 'canary', // Bleeding edge, may be broken +]); + +export type ReleaseChannel = z.infer; + +// ========================================== +// Version Metadata +// ========================================== + +/** + * Version Metadata Schema + * + * Extended version information including channel, build info, and timestamps. + * + * @example + * ```typescript + * { + * version: '1.2.3', + * channel: 'stable', + * buildNumber: '12345', + * gitCommit: 'a1b2c3d', + * publishedAt: '2024-01-15T10:30:00Z' + * } + * ``` + */ +export const VersionMetadataSchema = z.object({ + /** + * Semantic version string + */ + version: SemanticVersionSchema.describe('Semantic version'), + + /** + * Release channel + */ + channel: ReleaseChannelSchema.default('stable').describe('Release channel'), + + /** + * Build number (optional) + */ + buildNumber: z.string().optional().describe('Build number'), + + /** + * Git commit SHA (optional) + */ + gitCommit: z.string().optional().describe('Git commit SHA'), + + /** + * Publication timestamp (ISO 8601) + */ + publishedAt: z.string().datetime().optional().describe('Publication timestamp (ISO 8601)'), + + /** + * Custom metadata + */ + metadata: z.record(z.string(), z.any()).optional().describe('Custom metadata'), +}); + +export type VersionMetadata = z.infer;