diff --git a/packages/agents-audit/src/spec-contract-visibility.test.ts b/packages/agents-audit/src/spec-contract-visibility.test.ts new file mode 100644 index 0000000..752ba0d --- /dev/null +++ b/packages/agents-audit/src/spec-contract-visibility.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest'; +import { validate, validateV4, version } from '@workspacejson/spec'; +import type { CoChangeEntry, FragilityEntry, WorkspaceJsonV4 } from '@workspacejson/spec'; + +/** + * META-244 regression guard: the CLI compiles against the REAL published + * `@workspacejson/spec` declarations, not a local copy of them. + * + * `types/ambient.d.ts` used to carry a handwritten + * `declare module '@workspacejson/spec'`. Ambient module declarations win over + * node_modules typings, so that stub shadowed the real package — and it omitted + * the entire v0.4 contract. Every symbol imported above exists ONLY in the real + * published package and was absent from the stub, so **reintroducing the shadow + * breaks this file at compile time** (TS2305 / TS2724), not at runtime. + * + * That is the point: this is a type-visibility test whose primary assertion is + * that it compiles at all. The runtime expectations below keep it honest under + * a test runner that strips types. + */ +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'); + }); + + it('exposes validateV4, which the removed ambient stub did not declare', () => { + expect(validateV4).toBeTypeOf('function'); + }); + + it('accepts a v0.4 artifact through the published validators', () => { + // Under the removed stub, `validate` was typed `data is WorkspaceJsonV3` + // only. The real declaration is `data is WorkspaceJsonV3 | WorkspaceJsonV4`, + // so a v0.4 artifact is a first-class member of the contract here. + const artifact: WorkspaceJsonV4 = { + manual: {}, + generated: { + specVersion: '0.4', + generatedAt: '2026-07-26T00:00:00.000Z', + by: { name: 'agents-audit', version: '0.4.4' }, + frameworkManifest: [], + fileIndex: {}, + coChange: [], + fragility: [], + }, + agents: {}, + health: { intelligenceState: 'INSUFFICIENT_DATA', observationCount: 0, confidence: 0 }, + }; + + expect(artifact.generated.specVersion).toBe('0.4'); + expect(validate(artifact)).toBe(true); + expect(validateV4(artifact)).toBe(true); + }); + + it('exposes the v0.4 evidence entry types', () => { + // CoChangeEntry and FragilityEntry exist only in the real package. Their + // shapes are asserted structurally so a silent contract change is visible + // here rather than discovered during META-195 producer work. + const coChange: CoChangeEntry = { + files: ['src/a.ts', 'src/b.ts'], + rate: 0.5, + occurrences: 2, + generated: false, + }; + const fragility: FragilityEntry = { + file: 'src/a.ts', + changeCount: 10, + revertCount: 2, + revertRate: 0.2, + fragilityScore: 0.4, + excluded: false, + }; + + // Set semantics: exactly two entries, order not meaningful. + expect(coChange.files).toHaveLength(2); + expect(fragility.revertRate).toBeCloseTo(fragility.revertCount / fragility.changeCount, 5); + }); + + it('still narrows v0.3 artifacts, so the migration contract is unchanged', () => { + // Guards against the opposite failure: consuming real types must not have + // widened or broken v0.3 handling, which the producer emits today. + const v3 = { + manual: {}, + generated: { + specVersion: '0.3', + generatedAt: '2026-07-26T00:00:00.000Z', + by: { name: 'agents-audit', version: '0.4.4' }, + frameworkManifest: [], + fileIndex: {}, + }, + agents: {}, + health: { intelligenceState: 'INSUFFICIENT_DATA', observationCount: 0, confidence: 0 }, + }; + + expect(validate(v3)).toBe(true); + expect(validateV4(v3)).toBe(false); + }); +}); diff --git a/scripts/check-architecture.mjs b/scripts/check-architecture.mjs index 384475e..e715761 100644 --- a/scripts/check-architecture.mjs +++ b/scripts/check-architecture.mjs @@ -90,6 +90,34 @@ for (const file of sourceFiles) { } } +// --------------------------------------------------------------------------- +// 2b. No ambient re-declaration of a standard-owned package (META-244). +// +// Ambient module declarations win over node_modules typings, so a handwritten +// `declare module '@workspacejson/spec'` silently shadows the real published +// contract — this repository shipped exactly that until META-244, and it hid +// the entire v0.4 surface from the compiler. workspacejson/standard owns those +// types; consume them, never restate them. +// --------------------------------------------------------------------------- +const AMBIENT_FIRST_PARTY = /declare\s+module\s+['"]@workspacejson\/[^'"]+['"]/; + +// Comments are stripped first: this rule is about what the compiler sees, and +// the note in types/ambient.d.ts explaining why the shadow was removed +// necessarily quotes the very syntax it forbids. +function stripComments(source) { + return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^[ \t]*\/\/.*$/gm, ""); +} + +for (const file of sourceFiles) { + if (SELF_REFERENTIAL.has(file)) continue; + if (!file.endsWith(".d.ts")) continue; + const match = stripComments(readFileSync(file, "utf8")).match(AMBIENT_FIRST_PARTY); + if (match) { + report("shadowed-standard-types", file, + `${match[0]} re-declares a standard-owned contract; consume the published declarations instead (META-244)`); + } +} + // --------------------------------------------------------------------------- // 3. No host-integration or site implementation in the CLI repository. // --------------------------------------------------------------------------- diff --git a/scripts/check-architecture.test.mjs b/scripts/check-architecture.test.mjs index ffccdeb..b88257f 100644 --- a/scripts/check-architecture.test.mjs +++ b/scripts/check-architecture.test.mjs @@ -48,6 +48,19 @@ const cases = [ properties: { manual: { type: "object" }, generated: { type: "object" } }, }, null, 2)), }, + { + name: "shadowed-standard-types: ambient re-declaration of @workspacejson/spec", + expect: "shadowed-standard-types", + mutate: (root) => write(join(root, "types/ambient.d.ts"), + readFileSync(join(root, "types/ambient.d.ts"), "utf8") + + `\ndeclare module '@workspacejson/spec' {\n export const version: string;\n}\n`), + }, + { + name: "shadowed-standard-types: ambient re-declaration of @workspacejson/rules", + expect: "shadowed-standard-types", + mutate: (root) => write(join(root, "types/rogue.d.ts"), + `declare module "@workspacejson/rules" {\n export type Finding = unknown;\n}\n`), + }, { name: "repository-boundary: host-integration code in the CLI repo", expect: "repository-boundary", diff --git a/types/ambient.d.ts b/types/ambient.d.ts index 7557921..19c3bdd 100644 --- a/types/ambient.d.ts +++ b/types/ambient.d.ts @@ -83,112 +83,18 @@ declare module 'commander' { } } -declare module '@workspacejson/spec' { - export const workspaceJsonSchema: { - readonly $schema: string; - readonly $id: string; - readonly title: string; - readonly type: string; - readonly required: readonly string[]; - readonly additionalProperties: boolean; - readonly properties: Record; - }; - - export const version: string; - - export function validate(data: unknown): data is WorkspaceJsonV3; - export function validateLegacy(data: unknown): boolean; - - export interface WorkspacePackage { - name?: string; - path: string; - agentsMd?: string; - dependencies?: string[]; - [key: string]: unknown; - } - - export interface WorkspaceConvention { - raw: string; - type: 'filename-case' | 'directory-layout' | 'naming' | 'structural' | 'other'; - canonical: string; - } - - export interface WorkspaceAgentFiles { - agentsMd?: string; - workspaceJson?: string; - } - - export interface WorkspaceGitSummary { - nonAgentsCommitCount30Days: number; - filesChangedLast30Days: string[]; - } - - export interface WorkspaceHygiene { - score: number; - grade: 'A' | 'B' | 'C' | 'D' | 'F'; - failCount: number; - warnCount: number; - scannedAt: string; - } - - export interface WorkspaceJson { - version: string; - generatedAt?: string; - repository?: string; - topology?: 'single-package' | 'monorepo' | 'polyglot-monorepo'; - ciProvider?: 'github-actions' | 'gitlab-ci' | 'circleci' | 'jenkins' | 'none' | 'unknown'; - agentFiles?: WorkspaceAgentFiles; - frameworks?: string[]; - conventions?: WorkspaceConvention[]; - packages?: WorkspacePackage[]; - gitSummary?: WorkspaceGitSummary; - hygiene?: WorkspaceHygiene; - metadata?: Record; - [key: string]: unknown; - } - - 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; - }; - } -} +// The `@workspacejson/spec` contract is NOT declared here. +// +// It previously was: a handwritten `declare module '@workspacejson/spec'` +// restating that package's type surface. Ambient module declarations win over +// node_modules typings, so the stub shadowed the real published declarations +// even though the dependency is a registry-backed pin — and it silently hid the +// entire v0.4 contract (WorkspaceJsonV4, validateV4, CoChangeEntry, +// FragilityEntry) from this repository's compiler. +// +// workspacejson/standard owns that contract. This repository consumes the real +// published declarations and must not keep a second editable copy (META-244, +// enforcing META-165). scripts/check-architecture.mjs rejects reintroduction. declare module 'ora' { export interface Spinner {