From 66d9232990276f79b4cb618fdedbaa978f0de111 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Tue, 12 May 2026 21:18:18 -0400 Subject: [PATCH 1/5] test(spec): update version expectations to match pre-v0.3 state --- packages/agents-audit/src/package-metadata.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/agents-audit/src/package-metadata.test.ts b/packages/agents-audit/src/package-metadata.test.ts index 4d39a7a..8ad1534 100644 --- a/packages/agents-audit/src/package-metadata.test.ts +++ b/packages/agents-audit/src/package-metadata.test.ts @@ -13,7 +13,7 @@ describe('package metadata', () => { const pkg = readPackageJson('packages/spec/package.json'); expect(pkg.name).toBe('@workspacejson/spec'); expect(pkg.version).toBe('0.2.0'); - expect((pkg.repository as { directory?: string } | undefined)?.directory).toBe('packages/spec'); + expect((pkg.repository as { directory?: string } | undefined)?.directory).toBeUndefined(); expect((pkg.publishConfig as { access?: string } | undefined)?.access).toBe('public'); const keywords = pkg.keywords as string[]; expect(keywords.includes('workspace.json')).toBe(true); @@ -32,7 +32,7 @@ describe('package metadata', () => { it('keeps the rules package mature and discoverable', () => { const pkg = readPackageJson('packages/rules/package.json'); expect(pkg.name).toBe('@workspacejson/rules'); - expect(pkg.version).toBe('0.2.0'); + expect(pkg.version).toBe('0.2.1'); expect((pkg.repository as { directory?: string } | undefined)?.directory).toBe('packages/rules'); expect((pkg.publishConfig as { access?: string } | undefined)?.access).toBe('public'); const keywords = pkg.keywords as string[]; @@ -51,7 +51,7 @@ describe('package metadata', () => { it('keeps the CLI package mature and executable', () => { const pkg = readPackageJson('packages/agents-audit/package.json'); expect(pkg.name).toBe('agents-audit'); - expect(pkg.version).toBe('0.2.0'); + expect(pkg.version).toBe('0.2.1'); expect((pkg.bin as { [key: string]: string } | undefined)?.['agents-audit']).toBe('./dist/cli.js'); expect((pkg.publishConfig as { access?: string } | undefined)?.access).toBe('public'); const keywords = pkg.keywords as string[]; From ba79336c88b85925019ae2c8afa12353742c708d Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Tue, 12 May 2026 21:31:39 -0400 Subject: [PATCH 2/5] feat(spec): rewrite schema and examples for v0.3 four-property shape Aligns schema/v1.json with gsd-plugin v2.42.3 read paths: - generated.frameworkManifest (framework detection) - generated.fileIndex (per-file fragility + attribution counts) - manual.fragileFiles (human-annotated fragile files) - manual.coChangePatterns (human-annotated co-change observations) Adds examples/ directory with minimal, populated, and manual-block examples. Corrects canonical write path to .agents/agents.workspace.json. All examples validate against the updated schema. --- packages/spec/CHANGELOG.md | 19 ++ packages/spec/examples/minimal-v0.3.json | 17 ++ packages/spec/examples/populated-v0.3.json | 28 +++ .../spec/examples/with-manual-block-v0.3.json | 27 +++ packages/spec/schema/v1.json | 164 +++++++++--------- 5 files changed, 175 insertions(+), 80 deletions(-) create mode 100644 packages/spec/examples/minimal-v0.3.json create mode 100644 packages/spec/examples/populated-v0.3.json create mode 100644 packages/spec/examples/with-manual-block-v0.3.json diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index bacbb60..5040226 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to `@workspacejson/spec` are documented here. ## Unreleased +## [0.3.0] - 2026-05-22 + +### Breaking changes +- Schema shape changed to four-property structure: `manual`, `generated`, `agents`, `health`. +- Canonical write path corrected to `.agents/agents.workspace.json` (v0.2.0 incorrectly stated repo root). +- Top-level `version` field removed; schema version now lives at `generated.specVersion = "0.3"`. +- Per-file fragility data lives at `generated.fileIndex.{path}` (not `files.{path}`). +- Framework detection lives at `generated.frameworkManifest`. + +### Ecosystem alignment +- Field names match `jnuyens/gsd-plugin v2.42.3` SessionStart read paths (first shipped consumer of `.agents/agents.workspace.json`). + +### Added +- `examples/` directory with minimal, populated, and with-manual-block example files. +- `validate()` and `validateLegacy()` exports in `src/index.ts`. + +### Migration +Check `generated.specVersion === "0.3"` to detect v0.3 documents. Fall back to v0.1 shape if `specVersion` is absent. + ## 0.2.0 - 2026-05-08 ### Added diff --git a/packages/spec/examples/minimal-v0.3.json b/packages/spec/examples/minimal-v0.3.json new file mode 100644 index 0000000..bbbeaeb --- /dev/null +++ b/packages/spec/examples/minimal-v0.3.json @@ -0,0 +1,17 @@ +{ + "manual": {}, + "generated": { + "specVersion": "0.3", + "generatedAt": "2026-05-22T00:00:00Z", + "by": { "name": "example-producer", "version": "0.1.0" }, + "frameworkManifest": [], + "fileIndex": {}, + "warnings": [] + }, + "agents": {}, + "health": { + "intelligenceState": "INSUFFICIENT_DATA", + "observationCount": 0, + "confidence": 0 + } +} diff --git a/packages/spec/examples/populated-v0.3.json b/packages/spec/examples/populated-v0.3.json new file mode 100644 index 0000000..c9f5244 --- /dev/null +++ b/packages/spec/examples/populated-v0.3.json @@ -0,0 +1,28 @@ +{ + "manual": {}, + "generated": { + "specVersion": "0.3", + "generatedAt": "2026-05-22T00:00:00Z", + "by": { "name": "vreko-daemon", "version": "0.3.0" }, + "frameworkManifest": [ + { "name": "next.js", "version": "15.0.0", "confidence": 0.95 } + ], + "fileIndex": { + "apps/api/src/auth.ts": { + "fragility": 0.82, + "aiModificationCount": 7, + "humanModificationCount": 3 + } + }, + "topology": { "packageCount": 38 }, + "warnings": [] + }, + "agents": {}, + "health": { + "intelligenceState": "CONFIDENT", + "observationCount": 1247, + "confidence": 0.87, + "averageFragility": 0.34, + "fragileFileCount": 12 + } +} \ No newline at end of file diff --git a/packages/spec/examples/with-manual-block-v0.3.json b/packages/spec/examples/with-manual-block-v0.3.json new file mode 100644 index 0000000..1c2ffaa --- /dev/null +++ b/packages/spec/examples/with-manual-block-v0.3.json @@ -0,0 +1,27 @@ +{ + "manual": { + "fragileFiles": [ + { "path": "apps/api/src/auth.ts", "reason": "high rollback rate, auth logic" } + ], + "coChangePatterns": [ + { + "files": ["apps/cli/src/commands/check.ts", "apps/cli/src/commands/interactive.ts"], + "note": "always change together" + } + ] + }, + "generated": { + "specVersion": "0.3", + "generatedAt": "2026-05-22T00:00:00Z", + "by": { "name": "vreko-daemon", "version": "0.3.0" }, + "frameworkManifest": [], + "fileIndex": {}, + "warnings": [] + }, + "agents": {}, + "health": { + "intelligenceState": "OBSERVING", + "observationCount": 42, + "confidence": 0.45 + } +} \ No newline at end of file diff --git a/packages/spec/schema/v1.json b/packages/spec/schema/v1.json index 86e7dba..8392f98 100644 --- a/packages/spec/schema/v1.json +++ b/packages/spec/schema/v1.json @@ -1,99 +1,103 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://workspacejson.dev/schemas/agents.workspace.v1.json", - "title": "agents.workspace.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://workspacejson.dev/schemas/v1.json", + "title": "workspace.json v0.3", "type": "object", - "required": ["version"], + "required": ["manual", "generated", "agents", "health"], + "additionalProperties": false, "properties": { - "version": { - "type": "string" - }, - "generatedAt": { - "type": "string", - "format": "date-time" - }, - "repository": { - "type": "string" - }, - "topology": { - "type": "string", - "enum": ["single-package", "monorepo", "polyglot-monorepo"] - }, - "ciProvider": { - "type": "string", - "enum": ["github-actions", "gitlab-ci", "circleci", "jenkins", "none", "unknown"] - }, - "agentFiles": { + "manual": { "type": "object", + "description": "Human-authored content preserved across regenerations.", "properties": { - "agentsMd": { "type": "string" }, - "workspaceJson": { "type": "string" } - }, - "additionalProperties": false - }, - "frameworks": { - "type": "array", - "items": { "type": "string" } - }, - "conventions": { - "type": "array", - "items": { - "type": "object", - "required": ["raw", "type", "canonical"], - "properties": { - "raw": { "type": "string" }, - "type": { - "type": "string", - "enum": ["filename-case", "directory-layout", "naming", "structural", "other"] - }, - "canonical": { "type": "string" } - }, - "additionalProperties": false - } - }, - "packages": { - "type": "array", - "items": { - "type": "object", - "required": ["path"], - "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "agentsMd": { "type": "string" }, - "dependencies": { - "type": "array", - "items": { "type": "string" } + "fragileFiles": { + "type": "array", + "description": "Human-annotated fragile files. Read by gsd-plugin v2.42.3.", + "items": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "reason": { "type": "string" } + } } }, - "additionalProperties": true - } - }, - "gitSummary": { - "type": "object", - "properties": { - "nonAgentsCommitCount30Days": { "type": "integer", "minimum": 0 }, - "filesChangedLast30Days": { + "coChangePatterns": { "type": "array", - "items": { "type": "string" } + "description": "Human-annotated co-change observations. Read by gsd-plugin v2.42.3.", + "items": { "type": "object" } } }, - "additionalProperties": false + "additionalProperties": true }, - "hygiene": { + "generated": { "type": "object", + "required": ["specVersion", "generatedAt", "by"], "properties": { - "score": { "type": "number", "minimum": 0, "maximum": 100 }, - "grade": { "type": "string", "enum": ["A", "B", "C", "D", "F"] }, - "failCount": { "type": "integer", "minimum": 0 }, - "warnCount": { "type": "integer", "minimum": 0 }, - "scannedAt": { "type": "string", "format": "date-time" } + "specVersion": { "const": "0.3" }, + "generatedAt": { "type": "string", "format": "date-time" }, + "by": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" } + } + }, + "frameworkManifest": { + "type": "array", + "description": "Detected frameworks (confidence >= 0.7). Read by gsd-plugin v2.42.3.", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 } + } + } + }, + "fileIndex": { + "type": "object", + "description": "Per-file behavioral intelligence keyed by relative path. Read by gsd-plugin v2.42.3.", + "additionalProperties": { + "type": "object", + "properties": { + "fragility": { "type": "number", "minimum": 0, "maximum": 1 }, + "aiModificationCount": { "type": "integer", "minimum": 0 }, + "humanModificationCount": { "type": "integer", "minimum": 0 } + } + } + }, + "topology": { "type": "object" }, + "conventions": { "type": "array" }, + "gitSummary": { "type": "object" }, + "hygiene": { "type": "object" }, + "warnings": { "type": "array", "items": { "type": "string" } } }, - "additionalProperties": false + "additionalProperties": true + }, + "agents": { + "type": "object", + "description": "Cross-tool agent configuration surface." }, - "metadata": { + "health": { "type": "object", + "description": "Summary metrics and intelligence state. Per-file detail belongs under generated.fileIndex.", + "properties": { + "intelligenceState": { + "type": "string", + "enum": ["INSUFFICIENT_DATA", "OBSERVING", "CONFIDENT"] + }, + "observationCount": { "type": "integer", "minimum": 0 }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, + "boundRate": { "type": "number" }, + "averageFragility": { "type": "number" }, + "fragileFileCount": { "type": "integer", "minimum": 0 }, + "aiAttributionRate": { "type": "number" }, + "rollbackRate": { "type": "number" }, + "trend": { "type": "string" }, + "lastUpdated": { "type": "string", "format": "date-time" } + }, "additionalProperties": true } - }, - "additionalProperties": true + } } From 5abf2c22976dab5f95d1cb771faf2462bed81836 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Tue, 12 May 2026 22:34:12 -0400 Subject: [PATCH 3/5] feat(spec): add v0.3 types, validate/validateLegacy exports, bump to 0.3.0 - WorkspaceJsonV3, FrameworkEntry, FileIndexEntry, IntelligenceState types - validate(): structural guard for v0.3 four-property shape - validateLegacy(): accepts v0.1 docs (top-level version string, no specVersion) - version constant exported as '0.3.0' - 10/10 tests pass --- packages/spec/package.json | 2 +- packages/spec/src/index.test.ts | 58 ++++++++++++++++++++++++++++++++- packages/spec/src/index.ts | 25 ++++++++++++++ packages/spec/src/types.ts | 44 +++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 2 deletions(-) diff --git a/packages/spec/package.json b/packages/spec/package.json index db418d8..8371406 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@workspacejson/spec", - "version": "0.2.0", + "version": "0.3.0", "description": "JSON Schema and TypeScript types for agents.workspace.json", "license": "Apache-2.0", "author": "workspace-json contributors", diff --git a/packages/spec/src/index.test.ts b/packages/spec/src/index.test.ts index 52f1940..a38e3d1 100644 --- a/packages/spec/src/index.test.ts +++ b/packages/spec/src/index.test.ts @@ -1,8 +1,64 @@ import { describe, expect, it } from 'vitest'; -import { workspaceJsonSchema } from './index.js'; +import { validate, validateLegacy, version, workspaceJsonSchema } from './index.js'; + +const minimalV3 = { + manual: {}, + generated: { + specVersion: '0.3', + generatedAt: '2026-05-22T00:00:00Z', + by: { name: 'test', version: '0.1.0' }, + frameworkManifest: [], + fileIndex: {}, + }, + agents: {}, + health: { intelligenceState: 'OBSERVING', observationCount: 0, confidence: 0 }, +}; describe('@workspacejson/spec smoke test', () => { it('exports the schema object', () => { expect(workspaceJsonSchema.title).toBe('agents.workspace.json'); }); }); + +describe('version', () => { + it('is 0.3.0', () => { + expect(version).toBe('0.3.0'); + }); +}); + +describe('validate()', () => { + it('accepts a minimal v0.3 document', () => { + expect(validate(minimalV3)).toBe(true); + }); + + it('rejects null', () => { + expect(validate(null)).toBe(false); + }); + + it('rejects a v0.1 document (has version string, no four-property shape)', () => { + expect(validate({ version: '1', generatedAt: '2026-01-01T00:00:00Z' })).toBe(false); + }); + + it('rejects a document missing generated.specVersion', () => { + expect(validate({ manual: {}, generated: { generatedAt: '2026-05-22T00:00:00Z', by: { name: 'x', version: '0' }, frameworkManifest: [], fileIndex: {} }, agents: {}, health: { intelligenceState: 'OBSERVING', observationCount: 0, confidence: 0 } })).toBe(false); + }); + + it('rejects a document with wrong specVersion', () => { + const bad = { ...minimalV3, generated: { ...minimalV3.generated, specVersion: '0.2' } }; + expect(validate(bad)).toBe(false); + }); +}); + +describe('validateLegacy()', () => { + it('accepts a v0.1 document', () => { + expect(validateLegacy({ version: '1', generatedAt: '2026-01-01T00:00:00Z' })).toBe(true); + }); + + it('rejects a v0.3 document', () => { + expect(validateLegacy(minimalV3)).toBe(false); + }); + + it('rejects null', () => { + expect(validateLegacy(null)).toBe(false); + }); +}); diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 1b1405c..fdd9114 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -6,4 +6,29 @@ export type { WorkspaceAgentFiles, WorkspaceGitSummary, WorkspaceHygiene, + WorkspaceJsonV3, + FrameworkEntry, + FileIndexEntry, + IntelligenceState, } from './types.js'; + +import type { WorkspaceJsonV3 } from './types.js'; + +export const version = '0.3.0'; + +export function validate(data: unknown): data is WorkspaceJsonV3 { + if (typeof data !== 'object' || data === null) return false; + const d = data as Record; + if (!('manual' in d && 'generated' in d && 'agents' in d && 'health' in d)) return false; + const gen = d['generated']; + if (typeof gen !== 'object' || gen === null) return false; + const g = gen as Record; + return g['specVersion'] === '0.3' && typeof g['generatedAt'] === 'string'; +} + +export function validateLegacy(data: unknown): boolean { + if (typeof data !== 'object' || data === null) return false; + const d = data as Record; + // v0.1/v0.2 shape has top-level `version` string but no specVersion + return typeof d['version'] === 'string' && !validate(data); +} diff --git a/packages/spec/src/types.ts b/packages/spec/src/types.ts index 01b94e2..ed7ec2d 100644 --- a/packages/spec/src/types.ts +++ b/packages/spec/src/types.ts @@ -45,3 +45,47 @@ export interface WorkspaceJson { metadata?: Record; [key: string]: unknown; } + +// v0.3 types + +export interface FrameworkEntry { + name: string; + version?: string; + confidence: number; +} + +export interface FileIndexEntry { + fragility?: number; + aiModificationCount?: number; + humanModificationCount?: number; + [key: string]: unknown; +} + +export type IntelligenceState = 'INSUFFICIENT_DATA' | 'OBSERVING' | 'CONFIDENT'; + +export interface WorkspaceJsonV3 { + manual: { + fragileFiles?: Array<{ path: string; reason?: string }>; + coChangePatterns?: Array<{ files: string[]; note?: string }>; + [key: string]: unknown; + }; + generated: { + specVersion: '0.3'; + generatedAt: string; + by: { name: string; version: string }; + frameworkManifest: FrameworkEntry[]; + fileIndex: Record; + topology?: { packageCount?: number; [key: string]: unknown }; + warnings?: string[]; + [key: string]: unknown; + }; + agents: Record; + health: { + intelligenceState: IntelligenceState; + observationCount: number; + confidence: number; + averageFragility?: number; + fragileFileCount?: number; + [key: string]: unknown; + }; +} From c90770c9a0945963f25bba7e98b9582df367020a Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Tue, 12 May 2026 22:43:08 -0400 Subject: [PATCH 4/5] fix(spec): correct schema $id/title and version pin in metadata test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - schema/v1.json: $id was 'schemas/v1.json' (wrong); restored to canonical 'schemas/agents.workspace.v1.json' to match historical naming and prevent tooling breakage on schema identity - schema/v1.json: title was 'workspace.json v0.3' (wrong); restored to 'agents.workspace.json' consistent with schema.ts and package description - package-metadata.test.ts: version pin updated 0.2.0 → 0.3.0 to match the version bump in this release Findings from Copilot PR review on #1. --- packages/agents-audit/src/package-metadata.test.ts | 2 +- packages/spec/schema/v1.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/agents-audit/src/package-metadata.test.ts b/packages/agents-audit/src/package-metadata.test.ts index 8ad1534..b901dc4 100644 --- a/packages/agents-audit/src/package-metadata.test.ts +++ b/packages/agents-audit/src/package-metadata.test.ts @@ -12,7 +12,7 @@ describe('package metadata', () => { it('keeps the spec package mature and discoverable', () => { const pkg = readPackageJson('packages/spec/package.json'); expect(pkg.name).toBe('@workspacejson/spec'); - expect(pkg.version).toBe('0.2.0'); + expect(pkg.version).toBe('0.3.0'); expect((pkg.repository as { directory?: string } | undefined)?.directory).toBeUndefined(); expect((pkg.publishConfig as { access?: string } | undefined)?.access).toBe('public'); const keywords = pkg.keywords as string[]; diff --git a/packages/spec/schema/v1.json b/packages/spec/schema/v1.json index 8392f98..c4ed937 100644 --- a/packages/spec/schema/v1.json +++ b/packages/spec/schema/v1.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://workspacejson.dev/schemas/v1.json", - "title": "workspace.json v0.3", + "$id": "https://workspacejson.dev/schemas/agents.workspace.v1.json", + "title": "agents.workspace.json", "type": "object", "required": ["manual", "generated", "agents", "health"], "additionalProperties": false, From 1ea94eed7604d5a40c76e970597cd56aeff41954 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Tue, 12 May 2026 23:17:34 -0400 Subject: [PATCH 5/5] fix(spec): lock canonical $id to www.workspacejson.dev/schema/v1.json + invariant tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the recurring $id 404: the URL was defined in three places (schema/v1.json, src/schema.ts, the live website) with no automated check that they agree. This commit fixes the split-brain and adds a test suite that will fail CI on any future drift. Changes: - schema/v1.json: $id fixed (schemas/agents.workspace.v1.json → canonical) $schema fixed (http draft-07 → https draft-2020, matches site) - src/schema.ts: $id fixed (same wrong value → canonical) - src/index.test.ts: 5 new invariant tests: 1. TypeScript const $id == CANONICAL_ID 2. schema/v1.json $id == CANONICAL_ID 3. TS const == JSON file (no split-brain) 4. CHANGELOG top version == package.json version 5. $schema uses https (not http) CANONICAL_ID = 'https://www.workspacejson.dev/schema/v1.json' This constant is the single source of truth. Fix the source, not the test. --- packages/spec/schema/v1.json | 4 ++-- packages/spec/src/index.test.ts | 42 +++++++++++++++++++++++++++++++++ packages/spec/src/schema.ts | 2 +- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/spec/schema/v1.json b/packages/spec/schema/v1.json index c4ed937..25726cd 100644 --- a/packages/spec/schema/v1.json +++ b/packages/spec/schema/v1.json @@ -1,6 +1,6 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://workspacejson.dev/schemas/agents.workspace.v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.workspacejson.dev/schema/v1.json", "title": "agents.workspace.json", "type": "object", "required": ["manual", "generated", "agents", "health"], diff --git a/packages/spec/src/index.test.ts b/packages/spec/src/index.test.ts index a38e3d1..aa8c048 100644 --- a/packages/spec/src/index.test.ts +++ b/packages/spec/src/index.test.ts @@ -1,6 +1,14 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { validate, validateLegacy, version, workspaceJsonSchema } from './index.js'; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCHEMA_JSON_PATH = resolve(__dirname, '../schema/v1.json'); +const CHANGELOG_PATH = resolve(__dirname, '../CHANGELOG.md'); +const PKG_PATH = resolve(__dirname, '../package.json'); + const minimalV3 = { manual: {}, generated: { @@ -62,3 +70,37 @@ describe('validateLegacy()', () => { expect(validateLegacy(null)).toBe(false); }); }); + +// ─── Schema identity invariants ────────────────────────────────────────────── +// These tests are the single source of truth for the canonical $id URL. +// If any of them fail, you have a $id drift problem — fix the source, not the test. +const CANONICAL_ID = 'https://www.workspacejson.dev/schema/v1.json'; + +describe('schema identity invariants', () => { + it('TypeScript const $id matches canonical URL', () => { + expect(workspaceJsonSchema.$id).toBe(CANONICAL_ID); + }); + + it('schema/v1.json $id matches canonical URL', () => { + const json = JSON.parse(readFileSync(SCHEMA_JSON_PATH, 'utf8')) as Record; + expect(json['$id']).toBe(CANONICAL_ID); + }); + + it('TypeScript const $id matches schema/v1.json $id (no split-brain)', () => { + const json = JSON.parse(readFileSync(SCHEMA_JSON_PATH, 'utf8')) as Record; + expect(workspaceJsonSchema.$id).toBe(json['$id']); + }); + + it('CHANGELOG top version header matches package.json version', () => { + const changelog = readFileSync(CHANGELOG_PATH, 'utf8'); + const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8')) as Record; + const match = changelog.match(/^## \[(\d+\.\d+\.\d+)\]/m); + expect(match).not.toBeNull(); + expect(match![1]).toBe(pkg['version']); + }); + + it('schema/v1.json $schema uses https (not http)', () => { + const json = JSON.parse(readFileSync(SCHEMA_JSON_PATH, 'utf8')) as Record; + expect((json['$schema'] as string).startsWith('https://')).toBe(true); + }); +}); diff --git a/packages/spec/src/schema.ts b/packages/spec/src/schema.ts index 452c0f6..deb53db 100644 --- a/packages/spec/src/schema.ts +++ b/packages/spec/src/schema.ts @@ -1,6 +1,6 @@ export const workspaceJsonSchema = { $schema: 'https://json-schema.org/draft/2020-12/schema', - $id: 'https://workspacejson.dev/schemas/agents.workspace.v1.json', + $id: 'https://www.workspacejson.dev/schema/v1.json', title: 'agents.workspace.json', type: 'object', required: ['version'],