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 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