From cb4b579cd3e0028a56bc9a25970c2955e0ae3073 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Tue, 18 Aug 2026 22:14:11 -0400 Subject: [PATCH 1/2] feat(cli)!: move standard authority to the canonical 0.5.0 release (META-354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repoint the producer's standard dependencies from the old-org publication lineage to the canonical one: @workspacejson/spec and @workspacejson/rules 0.4.4 -> 0.5.0. 0.4.4 was published from workspace-json/agents-audit; 0.5.0 is published from workspacejson/standard. The dependency edge, not just the version range, now points at the canonical publisher. This is an authority-migration release. No mining, ranking, provenance, retrieval or command-surface changes ride along. Minor rather than patch because dist/index.d.ts carries WorkspaceJsonV4 in an exported signature, so spec 0.5.0's CoChangeEntry union propagates to consumers of this package's types: reading `.rate` without narrowing stops compiling. The standard classified that same propagation as a minor; this package inherits it instead of hiding it behind a patch. agents-audit deliberately stays on 0.4.4. It is frozen, locked for judging, not published from this repository, and its published artifact declares no dependency on this package. The two packages now resolve different versions of the standard on purpose. Behavior: * Artifacts are byte-identical to the previously pinned build across every fixture tried — this repository, a minimal clean repo, an empty repo — modulo the generatedAt and hygiene.scannedAt timestamps. * The agents-audit frozen-source parity harness still reports exactly its ratified baseline of four expected differences. * computeHygieneScore is HygieneScore | null as of rules 0.5.0. Where it returns null the producer now omits generated.hygiene rather than substituting a value; the field is not in the schema's generated.required set, and every placeholder would be a measurement claim that was not made. This path is currently unreachable: the producer's rule set emits at least one finding for every input tried, including an empty repository, so computeHygieneScore never receives the empty array that yields null. The META-244 type-visibility guard is re-anchored. It worked by importing symbols present only in the real published package, with `version`'s literal type as the discriminator against a handwritten ambient stub. spec 0.5.0 widens `version` to `string` — exactly what the stub declared — so that half no longer discriminates. The guard now also imports validateStoredKey and StoredKeyResult, which are new in 0.5.0 and were never in the stub, restoring the compile-time property. --- README.md | 15 ++++-- packages/cli/CHANGELOG.md | 54 +++++++++++++++++++ packages/cli/package.json | 8 +-- packages/cli/src/producer/generate.ts | 43 +++++++++++---- .../cli/src/spec-contract-visibility.test.ts | 31 +++++++++-- pnpm-lock.yaml | 32 +++++++++-- 6 files changed, 156 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 7e47b04..a7fb3c1 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,11 @@ consumed here as released packages. > **Status.** The architecture below landed in META-247 and is the ratified > target shape. > -> **Registry snapshot, verified 2026-08-04:** both packages are published — -> `@workspacejson/cli@0.5.2` and `agents-audit@0.4.4`. +> **Registry snapshot, verified 2026-08-18:** the published versions are +> `@workspacejson/cli@0.5.2` and `agents-audit@0.4.4`. The manifest table below +> declares `0.6.0` for the CLI, which is the authority-migration release +> prepared but not yet tagged; the registry does not carry it until +> `publish-cli.yml` runs on its `cli-v0.6.0` tag. > > The versions shown in the table below are the versions declared by this > repository's package manifests. `pnpm run check:package-docs` keeps those @@ -21,14 +24,16 @@ consumed here as released packages. > separately under META-293. > > Package semver is independent of the specification profile: `@workspacejson/cli` -> at `0.5.x` produces specification **v0.4** artifacts and is not evidence that -> schema v0.5 shipped. +> at `0.6.x` produces specification **v0.4** artifacts and is not evidence that +> schema v0.6 shipped. It consumes `@workspacejson/spec` and +> `@workspacejson/rules` at `0.5.0`, whose document profile is likewise still +> `generated.specVersion: "0.4"`. ## Packages | Directory | Package | Version | Role | | -- | -- | -- | -- | -| [`packages/cli/`](./packages/cli/) | `@workspacejson/cli` | `0.5.2` | the neutral producer and its `workspacejson` binary | +| [`packages/cli/`](./packages/cli/) | `@workspacejson/cli` | `0.6.0` | the neutral producer and its `workspacejson` binary | | [`packages/agents-audit-compat/`](./packages/agents-audit-compat/) | `agents-audit` | `0.4.4` | frozen compatibility bridge; preserves the historical command and API | | [`packages/mining-core/`](./packages/mining-core/) | `@workspacejson/mining-core` | `0.0.0`, private | L0 commit-graph mining core — extraction, path identity, completeness semantics (META-297 Phases 1–2) | diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index c027beb..d8e023e 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,59 @@ # Changelog — `@workspacejson/cli` +## 0.6.0 + +### Minor Changes + +- Move the producer's standard authority to the canonical `workspacejson/standard` + release: `@workspacejson/spec` and `@workspacejson/rules` `0.4.4` -> `0.5.0`. + + This is an authority-migration release. No capability, mining, ranking, + provenance or command-surface changes ship with it. + + **Why the packages moved, not just the versions.** `0.4.4` of both packages was + published from `workspace-json/agents-audit`; `0.5.0` is published from + `workspacejson/standard`. The dependency edge, not only the version range, now + points at the canonical publisher. + + **Why this is a minor rather than a patch.** Nothing in this package's own API + changed. But `dist/index.d.ts` carries `WorkspaceJsonV4` in an exported + signature (`GenerateResult.content`, `writeWorkspaceAtomically`), so `spec`'s + `0.5.0` source-level break propagates to anyone consuming those types. + `CoChangeEntry` is now a union whose members declare the other form's field as + `?: never`, so reading `entry.rate` off it without narrowing stops compiling: + + ```ts + const r: number = result.content.generated.coChange[0].rate; // was fine, now a type error + ``` + + `@workspacejson/spec` and `@workspacejson/rules` classified that propagation as a + minor for exactly this reason, and this package inherits it rather than hiding + it behind a patch. + + **Artifact output is unchanged.** Every `.agents/workspace.json` this producer + emits is byte-identical to the one the previously pinned build emits, modulo the + `generatedAt` and `hygiene.scannedAt` timestamps. That was verified by running + the pinned build and this one over the same inputs and diffing the normalized + artifacts; the frozen-source parity harness for the `agents-audit` bridge also + reports its ratified baseline of four expected differences, unchanged. + + **`agents-audit` deliberately stays on `0.4.4`.** The compatibility bridge is + frozen and locked for judging (OWNERSHIP.md), is not published from this + repository, and its published artifact declares no dependency on this package. + Moving its pins would put a frozen, parity-gated surface at risk for no benefit, + so the two packages now resolve different versions of the standard on purpose. + + **One guarded behavior change, currently unreachable.** `computeHygieneScore` + returns `HygieneScore | null` as of `rules@0.5.0`, returning `null` when a scan + observed nothing. Where that happens the producer now omits `generated.hygiene` + entirely — the field is not in the schema's `generated.required` set, and + absence is the only truthful option, since every placeholder value is a + measurement claim that was not made. This path is not reachable through the + current producer: its rule set emits at least one finding for every input tried, + including an empty repository, so `computeHygieneScore` never receives the empty + array that produces `null`. The handling is in place because the type demands + it, not because output changed. + ## 0.5.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 3c92198..4eaff69 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@workspacejson/cli", - "version": "0.5.2", - "description": "The workspace.json producer \u2014 scans a repository and generates .agents/workspace.json deterministically, preserving human-authored manual evidence.", + "version": "0.6.0", + "description": "The workspace.json producer — scans a repository and generates .agents/workspace.json deterministically, preserving human-authored manual evidence.", "license": "Apache-2.0", "author": "workspace.json contributors", "homepage": "https://workspacejson.dev", @@ -58,8 +58,8 @@ "prepublishOnly": "pnpm build && pnpm test && node ../../scripts/verify-package-tarball.mjs" }, "dependencies": { - "@workspacejson/rules": "0.4.4", - "@workspacejson/spec": "0.4.4", + "@workspacejson/rules": "0.5.0", + "@workspacejson/spec": "0.5.0", "commander": "^12.1.0", "ora": "^8.0.1", "picocolors": "^1.0.1" diff --git a/packages/cli/src/producer/generate.ts b/packages/cli/src/producer/generate.ts index 9ac54b4..f706371 100644 --- a/packages/cli/src/producer/generate.ts +++ b/packages/cli/src/producer/generate.ts @@ -352,14 +352,35 @@ export async function generateWorkspaceJson( } : { agentFiles: { workspaceJson: '.agents/workspace.json' } }), }, - hygiene: { - score: score.value, - grade: score.grade, - failCount: score.breakdown.failCount, - warnCount: score.breakdown.warnCount, - scannedAt: - (existing?.generated.hygiene as { scannedAt?: string } | undefined)?.scannedAt ?? now, - }, + // `computeHygieneScore` returns null when the scan observed nothing — + // no findings, and no file-count denominator to say anything was + // examined (ADR-003 A-002, `@workspacejson/rules@0.5.0`). Under 0.4.4 + // that same input returned `{ value: 100, grade: 'A' }`, so a scan that + // looked at nothing certified this repository as flawless and that value + // reached the published artifact. + // + // The block is therefore OMITTED rather than filled with a substitute. + // `generated.hygiene` is not in the schema's `generated.required` set, so + // absence is valid; and absence is the only truthful option, because + // every available placeholder — a zeroed block, or the old 100/A — is a + // measurement claim we did not make. Absent, not wrong. + // + // This is the one artifact-visible consequence of the 0.4.4 -> 0.5.0 + // authority migration. It changes bytes only for a repository that + // produced zero findings; wherever any evidence exists the arithmetic is + // unchanged, which is what the parity fixtures assert. + ...(score === null + ? {} + : { + hygiene: { + score: score.value, + grade: score.grade, + failCount: score.breakdown.failCount, + warnCount: score.breakdown.warnCount, + scannedAt: + (existing?.generated.hygiene as { scannedAt?: string } | undefined)?.scannedAt ?? now, + }, + }), // Commit-history evidence is PRESERVED, never rebuilt, by ordinary // generation — see history-carry-forward.ts for why this one part of the // producer-owned section is carried rather than regenerated. @@ -390,7 +411,11 @@ export async function generateWorkspaceJson( const unchanged = existing !== undefined && isMateriallyCurrent(existing, workspace); if (unchanged) { workspace.generated.generatedAt = existing!.generated.generatedAt; - } else { + } else if (workspace.generated.hygiene !== undefined) { + // Guarded because the block is absent when the scan observed nothing; see + // the omission note above. Stamping `scannedAt` onto a block that does not + // exist would resurrect it as a bare timestamp, which reads as "scanned, + // perfectly clean" — precisely the false claim the omission removes. (workspace.generated.hygiene as { scannedAt: string }).scannedAt = now; } if (!options.dryRun && !options.check && !unchanged) { diff --git a/packages/cli/src/spec-contract-visibility.test.ts b/packages/cli/src/spec-contract-visibility.test.ts index 752ba0d..ac67bbb 100644 --- a/packages/cli/src/spec-contract-visibility.test.ts +++ b/packages/cli/src/spec-contract-visibility.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { validate, validateV4, version } from '@workspacejson/spec'; -import type { CoChangeEntry, FragilityEntry, WorkspaceJsonV4 } from '@workspacejson/spec'; +import { validate, validateStoredKey, validateV4, version } from '@workspacejson/spec'; +import type { CoChangeEntry, FragilityEntry, StoredKeyResult, WorkspaceJsonV4 } from '@workspacejson/spec'; /** * META-244 regression guard: the CLI compiles against the REAL published @@ -19,9 +19,30 @@ import type { CoChangeEntry, FragilityEntry, WorkspaceJsonV4 } from '@workspacej */ describe('@workspacejson/spec contract visibility', () => { it('exposes the published version as a value, not a local guess', () => { - // The removed stub declared `version: string`. The real package declares the - // literal "0.4.4" — so this also pins which contract we compiled against. - expect(version).toBe('0.4.4'); + // Pins which contract we compiled against. This assertion alone no longer + // distinguishes the real package from the removed stub: the stub declared + // `version: string`, and as of 0.5.0 so does the real package, because the + // literal was replaced by a value read at build time. The discriminator + // moved to the stored-key surface below — see that test for why. + expect(version).toBe('0.5.0'); + }); + + it('exposes the stored-key surface, which no stub ever declared', () => { + // This is the load-bearing half of the META-244 guard after 0.5.0. + // + // The guard works by importing symbols that exist ONLY in the real + // published package, so reintroducing a handwritten + // `declare module '@workspacejson/spec'` fails at COMPILE time (TS2305 / + // TS2724) rather than silently shadowing the real typings. `version` used + // to carry that weight via its literal type; 0.5.0 widened it to `string`, + // which is exactly what the stub declared, so it can no longer tell them + // apart. + // + // `validateStoredKey` and `StoredKeyResult` are new in 0.5.0 and were never + // present in the stub, so they restore the compile-time property the + // literal used to provide. + const accepted: StoredKeyResult = validateStoredKey('src/a.ts'); + expect(accepted.valid).toBe(true); }); it('exposes validateV4, which the removed ambient stub did not declare', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce23924..395ab21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,11 +71,11 @@ importers: packages/cli: dependencies: '@workspacejson/rules': - specifier: 0.4.4 - version: 0.4.4 + specifier: 0.5.0 + version: 0.5.0 '@workspacejson/spec': - specifier: 0.4.4 - version: 0.4.4 + specifier: 0.5.0 + version: 0.5.0 commander: specifier: ^12.1.0 version: 12.1.0 @@ -718,11 +718,20 @@ packages: resolution: {integrity: sha512-ZTCAC7LJ+gX5+RuYzzPmgSGLw8Q9k2EmytccaV2CB0QglIDOmBsDh+7nYhTQVB4FchRrWoAG1k5Yq48Wh7kh1g==} engines: {node: '>=20.0.0'} + '@workspacejson/rules@0.5.0': + resolution: {integrity: sha512-UlJUnDdc1In4oAMCNMFbFnCAVUGSb1HR0MeP3Pa6Db3uHrH/OvXPyQCqGXLDe9ii3D+6Ua/OHAJMeocKv2bL1Q==} + engines: {node: '>=20.0.0'} + '@workspacejson/spec@0.4.4': resolution: {integrity: sha512-7lA2PDkhaKFEetrantRWecKrgGh6ev1HLHLhG8Wr7S4GvYm1xwTnOq+lqFg8pZCgnEOV2H2jADw1jQyHwuHTew==} engines: {node: '>=20.0.0'} hasBin: true + '@workspacejson/spec@0.5.0': + resolution: {integrity: sha512-KpsUxvLXFHHHKY6F58tWBnqsx5REJjK99Kum1+ATU4b8oUGlStfVkWyphNQ+nFZU3hy/ckNLZTxs4mpOeWGQLA==} + engines: {node: '>=20.0.0'} + hasBin: true + acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -2300,10 +2309,25 @@ snapshots: - babel-plugin-macros - supports-color + '@workspacejson/rules@0.5.0': + dependencies: + '@workspacejson/spec': 0.5.0 + ajv: 8.20.0 + fast-glob: 3.3.3 + remark: 15.0.1 + remark-parse: 11.0.0 + simple-git: 3.36.0 + transitivePeerDependencies: + - supports-color + '@workspacejson/spec@0.4.4': dependencies: ajv: 8.20.0 + '@workspacejson/spec@0.5.0': + dependencies: + ajv: 8.20.0 + acorn-walk@8.3.5: dependencies: acorn: 8.17.0 From eb7843b1f9e2dae91ba22f837431275ec2ccbaff Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Tue, 18 Aug 2026 22:49:56 -0400 Subject: [PATCH 2/2] test: keep package-docs version-drift guard non-vacuous --- scripts/check-package-docs.test.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/check-package-docs.test.mjs b/scripts/check-package-docs.test.mjs index 1d00cd5..8ea02d8 100644 --- a/scripts/check-package-docs.test.mjs +++ b/scripts/check-package-docs.test.mjs @@ -55,7 +55,11 @@ const cases = [ mutate: (root) => { const p = join(root, "packages/cli/package.json"); const manifest = JSON.parse(read(p)); - manifest.version = "0.6.0"; + // Deliberately choose a value that cannot equal the repository's current + // release version. This red test previously hard-coded 0.6.0; once 0.6.0 + // became the real manifest version, the mutation became a no-op and the + // test falsely accused the guard of accepting a contradiction. + manifest.version = "9.9.9"; write(p, `${JSON.stringify(manifest, null, 2)}\n`); }, }, @@ -213,4 +217,4 @@ for (const testCase of legitimate) { } console.log(`\nPackage documentation guard red tests: ${passed} passed, ${failed} failed.`); -if (failed > 0) process.exit(1); +if (failed > 0) process.exit(1); \ No newline at end of file