mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
201 lines
15 KiB
JavaScript
201 lines
15 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { execFileSync } from 'node:child_process'
|
||
import { createHash } from 'node:crypto'
|
||
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import ts from 'typescript'
|
||
import { repositoryPath } from './impact-model.mjs'
|
||
|
||
export const sha256 = value => createHash('sha256').update(value).digest('hex')
|
||
export const readJson = (directory, file) => JSON.parse(fs.readFileSync(localFile(directory, file), 'utf8'))
|
||
|
||
export function localFile(directory, file) {
|
||
assert.equal(repositoryPath(file), file, `Noncanonical contract path: ${file}`)
|
||
const absolute = path.join(directory, file)
|
||
const relative = path.relative(fs.realpathSync(directory), fs.realpathSync(absolute))
|
||
assert(relative && !relative.startsWith('..') && !path.isAbsolute(relative), `Redirected contract path: ${file}`)
|
||
assert(fs.statSync(absolute).isFile(), `Contract evidence is not a file: ${file}`)
|
||
return absolute
|
||
}
|
||
|
||
export function pointer(document, value) {
|
||
assert(/^\//.test(value), 'Expected a JSON pointer')
|
||
return value.slice(1).split('/').reduce((item, part) => {
|
||
const key = part.replaceAll('~1', '/').replaceAll('~0', '~')
|
||
assert(item && Object.hasOwn(item, key), `Missing version evidence pointer: ${value}`)
|
||
return item[key]
|
||
}, document)
|
||
}
|
||
|
||
export function candidateInputs(directory, model) {
|
||
const files = new Set(['package.json', 'yarn.lock', 'refactor/contract-policy.json', '.node-version'])
|
||
const walk = (folder) => {
|
||
if (!fs.existsSync(path.join(directory, folder)))
|
||
return
|
||
for (const entry of fs.readdirSync(path.join(directory, folder), { withFileTypes: true })) {
|
||
const file = `${folder}/${entry.name}`
|
||
assert(!entry.isSymbolicLink(), `Review redirected contract input: ${file}`)
|
||
if (entry.isDirectory())
|
||
walk(file)
|
||
else files.add(file)
|
||
}
|
||
}
|
||
for (const pkg of model.policy.packages) {
|
||
files.add(`packages/${pkg.name}/package.json`)
|
||
for (const folder of ['src', 'types', 'public']) walk(`packages/${pkg.name}/${folder}`)
|
||
}
|
||
for (const folder of ['scripts', 'test', 'types', 'refactor/scripts', 'refactor/fixtures']) walk(folder)
|
||
for (const version of model.policy.versions) files.add(version.file)
|
||
const inputs = [...files].sort().map(file => ({ file, sha256: sha256(fs.readFileSync(localFile(directory, file))) }))
|
||
return { digest: sha256(JSON.stringify(inputs)), inputs }
|
||
}
|
||
|
||
function validateTest(directory, item) {
|
||
assert(/^CT-[A-Z0-9-]+$/.test(item.id), 'Invalid stable contract test ID')
|
||
assert(item.command === 'yarn test:contracts' && item.runner === 'node', 'Unsupported contract runner/command')
|
||
const source = ts.createSourceFile(item.file, fs.readFileSync(localFile(directory, item.file), 'utf8'), ts.ScriptTarget.Latest, true)
|
||
assert.equal(source.parseDiagnostics.length, 0, `Malformed contract test: ${item.file}`)
|
||
const imported = source.statements.some(statement => ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === 'node:test'
|
||
&& (statement.importClause?.name?.text === 'test' || statement.importClause?.namedBindings?.elements?.some(element => element.name.text === 'test' && (!element.propertyName || element.propertyName.text === 'test'))))
|
||
assert(imported, `Contract case requires the Node test binding: ${item.id}`)
|
||
const matches = []
|
||
function visit(node) {
|
||
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'test' && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0]) && node.arguments[0].text === item.title)
|
||
matches.push(node)
|
||
ts.forEachChild(node, visit)
|
||
}
|
||
visit(source)
|
||
assert.equal(matches.length, 1, `Contract test title must identify exactly one literal test: ${item.id}`)
|
||
assert(item.scope?.length && item.limitations?.length, `Missing assertion scope/limits: ${item.id}`)
|
||
}
|
||
|
||
export function readContractModel(directory) {
|
||
const policy = readJson(directory, 'refactor/contract-policy.json')
|
||
assert.equal(policy.schemaVersion, 1)
|
||
const scripts = readJson(directory, 'package.json').scripts
|
||
assert.equal(scripts?.['test:contracts'], 'node refactor/scripts/run-contracts.mjs', 'Contract runner command changed; review its evidence adapter')
|
||
assert.equal(scripts?.['check:contracts'], 'node refactor/scripts/contracts.mjs', 'Contract index command changed')
|
||
const stages = scripts['ci:check']?.split(' && ') || []
|
||
assert(stages.includes('yarn test:contracts') && stages.indexOf('yarn check:contracts --report') > stages.indexOf('yarn test:contracts'), 'CI must run contract observations before checking their report')
|
||
const tasks = readJson(directory, 'refactor/tasks.json').tasks
|
||
const inventory = readJson(directory, 'refactor/package-inventory.json').packages
|
||
const families = [...fs.readFileSync(localFile(directory, 'refactor/compatibility.md'), 'utf8').matchAll(/^\| (API-\d{2}) \| ([^|]+) \|/gm)].map(match => ({ id: match[1], name: match[2].trim() }))
|
||
assert(families.length > 0)
|
||
assert.deepEqual(policy.families, families, 'Public contract families changed; update their coverage ownership')
|
||
const names = inventory.map(pkg => pkg.name).sort()
|
||
assert.deepEqual(policy.packages.map(pkg => pkg.name).sort(), names, 'Contract package ownership is incomplete or duplicated')
|
||
const actual = fs.readdirSync(path.join(directory, 'packages')).filter(name => fs.existsSync(path.join(directory, 'packages', name, 'package.json'))).sort()
|
||
assert.deepEqual(actual, names, 'New package must be registered in inventory and contracts')
|
||
const unique = (items, label) => assert.equal(new Set(items.map(item => item.id)).size, items.length, `Duplicate ${label} ID`)
|
||
unique(policy.versions, 'version')
|
||
unique(policy.cases, 'case')
|
||
assert(policy.cases.length > 0, 'Contract runner requires explicit cases; empty selection must not discover unrelated tests')
|
||
assert.equal(new Set(policy.cases.map(item => JSON.stringify([item.file, item.title]))).size, policy.cases.length, 'Duplicate file/title under different contract IDs')
|
||
for (const version of policy.versions) {
|
||
assert(names.includes(version.package), 'Version references unknown package')
|
||
const origin = pointer(readJson(directory, version.file), version.pointer)
|
||
assert.equal(origin.name, version.package, `Wrong package in version evidence: ${version.id}`)
|
||
assert.equal(origin.version, version.version, `Wrong version evidence: ${version.id}`)
|
||
if (version.kind === 'published-baseline') {
|
||
assert(/^[a-f\d]{64}$/.test(origin.sha256) && origin.integrity?.startsWith('sha512-'), `Published version needs archived integrity: ${version.id}`)
|
||
assert.equal(version.archiveSha256, origin.sha256, `Published archive identity changed: ${version.id}`)
|
||
}
|
||
else {
|
||
assert.equal(version.kind, 'workspace-baseline')
|
||
assert.equal(version.file, 'refactor/package-inventory.json', 'Workspace version must use the frozen inventory')
|
||
}
|
||
}
|
||
for (const pkg of policy.packages) {
|
||
assert.deepEqual(Object.keys(pkg.owners).sort(), families.map(family => family.id).sort(), `Missing contract owner: ${pkg.name}`)
|
||
for (const owner of Object.values(pkg.owners)) {
|
||
const task = tasks.find(task => task.id === owner)
|
||
assert(task && (task.scope.includes(pkg.name) || task.scope.includes('workspace')), `Invalid contract responsibility: ${pkg.name}/${owner}`)
|
||
}
|
||
assert(pkg.versionIds.length && pkg.versionIds.every(id => policy.versions.some(version => version.id === id && version.package === pkg.name)), `Missing package version basis: ${pkg.name}`)
|
||
assert.equal(pkg.supportWindow, 'not-inferred-from-baseline', 'A baseline must not become an invented supported version range')
|
||
assert(pkg.scopeReview?.length, 'Contract applicability must remain explicit')
|
||
}
|
||
for (const item of policy.cases) {
|
||
const pkg = policy.packages.find(pkg => pkg.name === item.package)
|
||
assert(pkg && item.contracts.length && item.contracts.every(id => families.some(family => family.id === id)), 'Unknown case package/contract')
|
||
assert(item.versionIds.length && item.versionIds.every(id => policy.versions.some(version => version.id === id && version.package === item.package)), 'Unknown test version basis')
|
||
validateTest(directory, item)
|
||
}
|
||
const historical = tasks.flatMap(task => task.evidence.filter(file => /\.json$/.test(file)).map(file => ({ task: task.id, scope: task.scope, file: `refactor/${file}` })))
|
||
for (const item of historical) localFile(directory, item.file)
|
||
return { policy, families, tasks, historical }
|
||
}
|
||
|
||
export function evaluateRun(model, report, currentDigest) {
|
||
assert.equal(report.schemaVersion, 1)
|
||
assert.equal(report.command, 'yarn test:contracts')
|
||
assert(/^[a-f\d]{40}$/.test(report.head) && report.environment?.node && report.environment?.platform, 'Run lacks candidate/environment provenance')
|
||
assert.equal(report.candidate.digest, sha256(JSON.stringify(report.candidate.inputs)), 'Candidate input fingerprint mismatch')
|
||
assert(report.candidate.inputs.length && report.candidate.inputs.every(input => /^[a-f\d]{64}$/.test(input.sha256)), 'Invalid candidate inputs')
|
||
for (const input of report.candidate.inputs) repositoryPath(input.file)
|
||
assert.equal(new Set(report.cases.map(item => item.id)).size, report.cases.length, 'Duplicate run case identity')
|
||
assert(report.events.length && ['passed', 'failed'].includes(report.outcome), 'Run is missing test events/outcome')
|
||
const summaries = report.events.filter(event => event.type === 'test:summary' && !event.file)
|
||
assert.equal(summaries.length, 1, 'Run requires one process summary')
|
||
assert.equal(report.outcome === 'passed', summaries[0].success === true, 'Run outcome contradicts process summary')
|
||
if (report.outcome === 'passed')
|
||
assert(!report.events.some(event => event.type === 'test:fail' && !event.todo), 'Passed run contains a failed test')
|
||
return report.cases.map((item) => {
|
||
const defined = model.policy.cases.find(candidate => candidate.id === item.id)
|
||
assert(defined, `Unknown reported test ID: ${item.id}`)
|
||
assert.equal(item.definition?.id, item.id, `Wrong recorded definition: ${item.id}`)
|
||
assert.equal(item.definitionSha256, sha256(JSON.stringify(item.definition)), `Reported test definition fingerprint changed: ${item.id}`)
|
||
assert.equal(item.definition.package, defined.package, `Stable test ID was reassigned to another package: ${item.id}`)
|
||
const matches = report.events.filter(event => ['test:pass', 'test:fail'].includes(event.type) && event.file === item.definition.file && event.name === item.definition.title)
|
||
assert.equal(matches.length, 1, `Missing or ambiguous executed test: ${item.id}`)
|
||
const event = matches[0]
|
||
const status = event.skip || event.todo ? 'skipped' : event.type === 'test:fail' ? 'failed' : 'passed'
|
||
assert.equal(item.status, status, `Reported test outcome mismatch: ${item.id}`)
|
||
return { id: item.id, package: item.definition.package, contracts: item.definition.contracts, versionIds: item.definition.versionIds, status, freshness: item.definitionSha256 !== sha256(JSON.stringify(defined)) ? 'historical-definition' : report.candidate.digest === currentDigest ? 'current-inputs' : 'historical-inputs', scope: item.definition.scope }
|
||
})
|
||
}
|
||
|
||
export function contractStatus(directory, model) {
|
||
const current = candidateInputs(directory, model)
|
||
const registry = readJson(directory, 'refactor/contract-runs.json')
|
||
assert.equal(registry.schemaVersion, 1)
|
||
assert.equal(new Set(registry.runs.map(run => run.file)).size, registry.runs.length, 'Duplicate registered report')
|
||
const observations = []
|
||
for (const ref of registry.runs) {
|
||
const bytes = fs.readFileSync(localFile(directory, ref.file))
|
||
assert.equal(sha256(bytes.toString('utf8').replaceAll('\r\n', '\n')), ref.sha256LF, `Contract report changed: ${ref.file}`)
|
||
const report = JSON.parse(bytes)
|
||
observations.push({ file: ref.file, outcome: report.outcome, cases: evaluateRun(model, report, current.digest) })
|
||
}
|
||
const latest = 'refactor/.cache/ci/contracts-run.json'
|
||
if (fs.existsSync(path.join(directory, latest))) {
|
||
const report = readJson(directory, latest)
|
||
observations.push({ file: latest, outcome: report.outcome, cases: evaluateRun(model, report, current.digest) })
|
||
}
|
||
const rows = model.policy.packages.flatMap(pkg => model.families.map((family) => {
|
||
const cases = model.policy.cases.filter(item => item.package === pkg.name && item.contracts.includes(family.id))
|
||
const results = observations.flatMap(run => run.cases.filter(item => item.package === pkg.name && item.contracts.includes(family.id)).map(item => ({ ...item, report: run.file, runOutcome: run.outcome })))
|
||
return { package: pkg.name, contract: family.id, owner: pkg.owners[family.id], versionIds: pkg.versionIds, supportWindow: pkg.supportWindow, state: results.length ? 'partial-observations' : cases.length ? 'planned-not-observed' : 'tests-not-indexed', cases: cases.map(item => item.id), observations: results, remaining: 'Family-wide acceptance, version combinations and environment scope are not implied by individual test observations' }
|
||
}))
|
||
return { schemaVersion: 1, candidateDigest: current.digest, rows, observations, historicalRecords: model.historical.map(item => ({ ...item, state: 'recorded-not-normalized' })), limitation: 'Index status is not package compatibility or npm release approval' }
|
||
}
|
||
|
||
export function renderContracts(model) {
|
||
const lines = ['# 公开契约覆盖索引', '', '> 由 contract-policy.json 生成;`yarn check:contracts --write` 更新。动态候选/报告状态见 `yarn check:contracts --report`。', '', '覆盖12类兼容契约与22包的验证归属;不是每个公开成员的穷尽测试证明。没有固定测试ID的行仍需逐成员补齐。', '版本均为精确冻结对照点,不能推导连续支持区间。历史报告保留原状态,只有精确用例和候选指纹对应时才列为观察结果。', '', '| 包 | 契约 | 责任任务 | 固定测试ID |', '| --- | --- | --- | --- |']
|
||
for (const pkg of model.policy.packages) {
|
||
for (const family of model.families) {
|
||
const cases = model.policy.cases.filter(item => item.package === pkg.name && item.contracts.includes(family.id))
|
||
lines.push(`| ${pkg.name} | ${family.id} ${family.name} | ${pkg.owners[family.id]} | ${cases.map(item => item.id).join(', ') || '待索引;不代表没有历史测试'} |`)
|
||
}
|
||
}
|
||
lines.push('', '## 精确测试及限制', '')
|
||
for (const item of model.policy.cases)
|
||
lines.push(`- ${item.id}: [${item.file}](../${item.file}) — ${item.title}。${item.scope}。限制:${item.limitations}。命令:\`${item.command}\`。版本依据:${item.versionIds.join(', ')}。`)
|
||
lines.push('', '## 版本依据', '')
|
||
for (const version of model.policy.versions)
|
||
lines.push(`- ${version.id}: ${version.package}@${version.version} (${version.kind});[来源](../${version.file}) JSON pointer \`${version.pointer}\`。`)
|
||
return `${lines.join('\n')}\n`
|
||
}
|
||
|
||
export const currentHead = directory => execFileSync('git', ['rev-parse', 'HEAD'], { cwd: directory, encoding: 'utf8' }).trim()
|