feat(invariants): implement package runtime checks

This commit is contained in:
Tianyi Cui
2026-07-20 00:38:37 +08:00
parent 36e99e737b
commit 941b0411d8
125 changed files with 2317 additions and 1161 deletions

View File

@@ -1,44 +0,0 @@
/** Generate or verify package-owned invariant companion baselines. */
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
GENERATED_INVARIANT_MARKER,
collectPackageInvariantViolations,
formatPackageInvariantViolation,
packageInvariantOwners,
renderBaselineInvariant,
} from './package-invariants.ts'
const root = resolve(import.meta.dirname, '..')
const check = process.argv.includes('--check')
if (!check) {
let generated = 0
for (const owner of packageInvariantOwners(root)) {
const path = resolve(root, owner.sourcePath)
let current: string | undefined
try {
current = readFileSync(path, 'utf8')
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
}
if (current !== undefined && !current.includes(GENERATED_INVARIANT_MARKER)) continue
const expected = renderBaselineInvariant(owner)
if (current === expected) continue
writeFileSync(path, expected)
generated += 1
}
console.log(`gen-package-invariants: wrote ${generated} generated baseline companion(s).`)
}
const violations = collectPackageInvariantViolations(root)
if (violations.length > 0) {
console.error('verify-package-invariants: violations found:')
for (const violation of violations) {
console.error(` ${formatPackageInvariantViolation(root, violation)}`)
}
process.exit(1)
}
console.log(`verify-package-invariants: ${packageInvariantOwners(root).length} package companion(s) conform.`)

View File

@@ -4,8 +4,6 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
collectPackageInvariantViolations,
packageInvariantOwners,
renderBaselineInvariant,
} from './package-invariants.ts'
const roots: string[] = []
@@ -14,6 +12,18 @@ afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function handwrittenInvariant(packageName: string): string {
return `
export const name = 'probe-invariant'
export const inject = ['invariants']
const install = (_ctx: unknown, fail: (message: string) => never) => {
if (typeof ${JSON.stringify(packageName)} !== 'string') fail('package name must remain a string')
}
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install))
`
}
function fixture(options: {
packageName?: string
source?: string
@@ -47,8 +57,7 @@ function fixture(options: {
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
references: options.invariantReference === false ? [] : [{ path: '../../support/invariants' }],
}, null, 2)}\n`)
const owner = packageInvariantOwners(root)[0]!
writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? renderBaselineInvariant(owner))
writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? handwrittenInvariant(packageName))
writeFileSync(
join(dir, 'tsdown.config.ts'),
options.buildEntry === false ? "export default { entry: ['lib/types/index.js'] }\n" : "export default { entry: ['lib/types/index.js', 'lib/types/invariant.js'] }\n",
@@ -56,8 +65,43 @@ function fixture(options: {
return root
}
function addConformingPackage(root: string, slug: string, packageName: string, source: string): void {
const dir = join(root, `packages/core/${slug}`)
mkdirSync(join(dir, 'src'), { recursive: true })
writeFileSync(join(dir, 'package.json'), `${JSON.stringify({
name: packageName,
exports: {
'./invariant': {
types: './lib/types/invariant.d.ts',
default: './lib/invariant.js',
},
},
files: ['lib/invariant.js'],
peerDependencies: { '@deepseek-ai/dsh-invariants': '^0.0.1' },
devDependencies: { '@deepseek-ai/dsh-invariants': 'workspace:^' },
}, null, 2)}\n`)
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
references: [{ path: '../../support/invariants' }],
}, null, 2)}\n`)
writeFileSync(join(dir, 'src/invariant.ts'), source)
writeFileSync(join(dir, 'tsdown.config.ts'), "export default { entry: ['lib/types/invariant.js'] }\n")
}
function nameObservedInvariant(packageName: string, pluginName: string): string {
return `
import { observePluginInvariant } from '@deepseek-ai/dsh-invariants'
export const name = 'probe-invariant'
export const inject = ['invariants']
const install = (ctx: never, fail: (message: string) => never) => {
observePluginInvariant(ctx, fail, { name: ${JSON.stringify(pluginName)} })
}
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install))
`
}
describe('package invariant gate', () => {
it('accepts a generated owner companion with publication metadata', () => {
it('accepts a hand-owned checking companion with publication metadata', () => {
expect(collectPackageInvariantViolations(fixture())).toEqual([])
})
@@ -82,9 +126,10 @@ describe('package invariant gate', () => {
export const name = 'probe-invariant'
export const inject = ['invariants']
const selected = process.env.PACKAGE_NAME
export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) => {
ctx.invariants.register('@deepseek-ai/dsh-foreign', () => {})
return ctx.invariants.register(selected!, () => {})
const install = (_ctx: unknown, fail: (message: string) => never) => { fail('probe') }
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => {
ctx.invariants.register('@deepseek-ai/dsh-foreign', install)
return ctx.invariants.register(selected!, install)
}
`
const violations = collectPackageInvariantViolations(fixture({ source }))
@@ -94,11 +139,52 @@ export const apply = (ctx: { invariants: { register(name: string, install: () =>
]))
})
it('rejects edits to a generated baseline', () => {
const root = fixture()
const path = join(root, 'packages/core/probe/src/invariant.ts')
writeFileSync(path, `${renderBaselineInvariant(packageInvariantOwners(root)[0]!)}// stale\n`)
it('rejects generated markers and empty or reporter-free installers', () => {
const generated = fixture({
source: `/** @generated scripts/gen-package-invariants.ts */\n${handwrittenInvariant('@deepseek-ai/dsh-probe')}`,
})
expect(collectPackageInvariantViolations(generated).map(violation => violation.message))
.toContain('invariant companions must be hand-owned and may not carry @generated markers')
const empty = fixture({
source: `
export const name = 'probe-invariant'
export const inject = ['invariants']
const install = () => {}
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install))
`,
})
expect(collectPackageInvariantViolations(empty).map(violation => violation.message))
.toEqual(expect.arrayContaining([
'install function must contain a package-owned invariant check',
'install function must accept the bound failure reporter as its second parameter',
]))
const unused = fixture({
source: `
export const name = 'probe-invariant'
export const inject = ['invariants']
const install = (_ctx: unknown, _fail: (message: string) => never) => { void 0 }
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install))
`,
})
expect(collectPackageInvariantViolations(unused).map(violation => violation.message))
.toContain('install function must use its bound failure reporter')
})
it('rejects duplicate name-based plugin observers across packages', () => {
const root = fixture({
source: nameObservedInvariant('@deepseek-ai/dsh-probe', 'shared-runtime-name'),
})
addConformingPackage(
root,
'probe-two',
'@deepseek-ai/dsh-probe-two',
nameObservedInvariant('@deepseek-ai/dsh-probe-two', 'shared-runtime-name'),
)
expect(collectPackageInvariantViolations(root).map(violation => violation.message))
.toContain('generated baseline is stale; run pnpm run gen-package-invariants')
.toContain('name-based plugin invariant "shared-runtime-name" is already owned by "@deepseek-ai/dsh-probe-two"')
})
})

View File

@@ -1,16 +1,13 @@
/**
* Package-invariant companion discovery, generation, and structural checks.
* Package-invariant companion discovery and structural checks.
* The runtime registry stays product-independent; this gate makes ownership
* exhaustive across packages without centralizing package checks.
*/
import { existsSync, globSync, readFileSync } from 'node:fs'
import { basename, dirname, relative, resolve, sep } from 'node:path'
import { dirname, relative, resolve, sep } from 'node:path'
import ts from 'typescript'
/** Marker identifying baseline companions owned by this generator. */
export const GENERATED_INVARIANT_MARKER = '@generated scripts/gen-package-invariants.ts'
interface PackageManifest {
name?: string
exports?: Record<string, { types?: string; default?: string } | string | undefined>
@@ -53,53 +50,26 @@ export function packageInvariantOwners(root: string): PackageInvariantOwner[] {
})
}
/** Render the generated ownership-only companion for a package without custom checks. */
export function renderBaselineInvariant(owner: PackageInvariantOwner): string {
const serviceImport = owner.packageName === '@deepseek-ai/dsh-invariants'
? './index.ts'
: '@deepseek-ai/dsh-invariants'
const pluginName = `${basename(owner.dir)}-invariant`
return `/**
* Generated invariant ownership companion for \`${owner.packageName}\`.
* Replace this file with package-owned checks while preserving its registration.
*
* ${GENERATED_INVARIANT_MARKER}
* @module ${owner.packageName}/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '${serviceImport}'
const PACKAGE_NAME = '${owner.packageName}'
/** Cordis companion plugin name. */
export const name = '${pluginName}'
/** Services required before the companion can register. */
export const inject = ['invariants']
/** Reserve this package's invariant ownership until it adds relational checks. */
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
`
}
/** Return all violations of the package-invariant companion contract. */
export function collectPackageInvariantViolations(root: string): PackageInvariantViolation[] {
const violations: PackageInvariantViolation[] = []
const observedPluginNames = new Map<string, PackageInvariantOwner>()
for (const owner of packageInvariantOwners(root)) {
const manifest = readManifest(resolve(root, owner.manifestPath))
checkManifest(owner, manifest, violations)
checkBuild(owner, root, violations)
checkSource(owner, root, violations)
for (const pluginName of checkSource(owner, root, violations)) {
const existing = observedPluginNames.get(pluginName)
if (existing === undefined) {
observedPluginNames.set(pluginName, owner)
} else {
addViolation(
violations,
owner.sourcePath,
`name-based plugin invariant ${JSON.stringify(pluginName)} is already owned by ${JSON.stringify(existing.packageName)}`,
)
}
}
}
return violations
}
@@ -181,19 +151,18 @@ function checkSource(
owner: PackageInvariantOwner,
root: string,
violations: PackageInvariantViolation[],
): void {
): string[] {
const absolutePath = resolve(root, owner.sourcePath)
if (!existsSync(absolutePath)) {
addViolation(violations, owner.sourcePath, 'missing package-owned invariant companion')
return
return []
}
const sourceText = readFileSync(absolutePath, 'utf8')
if (sourceText.includes(GENERATED_INVARIANT_MARKER)
&& sourceText !== renderBaselineInvariant(owner)) {
if (sourceText.includes('@generated')) {
addViolation(
violations,
owner.sourcePath,
'generated baseline is stale; run pnpm run gen-package-invariants',
'invariant companions must be hand-owned and may not carry @generated markers',
)
}
@@ -237,6 +206,87 @@ function checkSource(
addViolation(violations, owner.sourcePath, `must named-export ${exportedName}`)
}
}
checkInstaller(owner, sourceFile, violations)
return nameOnlyObservedPlugins(sourceFile)
}
function nameOnlyObservedPlugins(sourceFile: ts.SourceFile): string[] {
const names: string[] = []
const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)
&& ts.isIdentifier(node.expression)
&& node.expression.text === 'observePluginInvariant') {
const contract = node.arguments[2]
if (contract !== undefined && ts.isObjectLiteralExpression(contract)) {
let hasExactPlugin = false
let name: string | undefined
for (const property of contract.properties) {
if (!ts.isPropertyAssignment(property)) continue
const key = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
? property.name.text
: undefined
if (key === 'plugin') hasExactPlugin = true
if (key === 'name') name = stringValue(property.initializer, new Map())
}
if (!hasExactPlugin && name !== undefined) names.push(name)
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return names
}
function checkInstaller(
owner: PackageInvariantOwner,
sourceFile: ts.SourceFile,
violations: PackageInvariantViolation[],
): void {
let initializer: ts.Expression | undefined
for (const statement of sourceFile.statements) {
if (!ts.isVariableStatement(statement)) continue
for (const declaration of statement.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)
&& declaration.name.text === 'install'
&& declaration.initializer !== undefined) initializer = declaration.initializer
}
}
const installer = initializer === undefined ? undefined : installerFunction(initializer)
if (installer === undefined) {
addViolation(violations, owner.sourcePath, 'must declare a local install function for package-owned checks')
return
}
if (ts.isBlock(installer.body) && installer.body.statements.length === 0) {
addViolation(violations, owner.sourcePath, 'install function must contain a package-owned invariant check')
}
const reporter = installer.parameters[1]?.name
if (reporter === undefined || !ts.isIdentifier(reporter)) {
addViolation(violations, owner.sourcePath, 'install function must accept the bound failure reporter as its second parameter')
return
}
if (!usesIdentifier(installer.body, reporter.text)) {
addViolation(violations, owner.sourcePath, 'install function must use its bound failure reporter')
}
}
function usesIdentifier(node: ts.Node, name: string): boolean {
return ts.isIdentifier(node) && node.text === name
|| node.getChildren().some(child => usesIdentifier(child, name))
}
function installerFunction(
initializer: ts.Expression,
): ts.ArrowFunction | ts.FunctionExpression | undefined {
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) return initializer
if (ts.isCallExpression(initializer)
&& ts.isPropertyAccessExpression(initializer.expression)
&& ts.isIdentifier(initializer.expression.expression)
&& initializer.expression.expression.text === 'Object'
&& initializer.expression.name.text === 'assign') {
const target = initializer.arguments[0]
if (target !== undefined && (ts.isArrowFunction(target) || ts.isFunctionExpression(target))) return target
}
return undefined
}
function topLevelStringConstants(sourceFile: ts.SourceFile): ReadonlyMap<string, string> {

View File

@@ -0,0 +1,21 @@
/** Verify package-owned invariant source and publication contracts. */
import { resolve } from 'node:path'
import {
collectPackageInvariantViolations,
formatPackageInvariantViolation,
packageInvariantOwners,
} from './package-invariants.ts'
const root = resolve(import.meta.dirname, '..')
const violations = collectPackageInvariantViolations(root)
if (violations.length > 0) {
console.error('verify-package-invariants: violations found:')
for (const violation of violations) {
console.error(` ${formatPackageInvariantViolation(root, violation)}`)
}
process.exit(1)
}
console.log(`verify-package-invariants: ${packageInvariantOwners(root).length} hand-owned package companion(s) conform.`)