Files
ArtPlayer/refactor/scripts/risk-register.mjs
T

95 lines
6.7 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import lockfile from '@yarnpkg/lockfile'
import { hash, refactorDir } from './releases.mjs'
const root = path.resolve(refactorDir, '..')
const read = name => JSON.parse(fs.readFileSync(path.join(refactorDir, name), 'utf8'))
export function referencedFindings() {
const files = ['baselines/lifecycle-coverage.md', 'baselines/dom-coverage.md', 'baselines/consumer-coverage.md', 'baselines/performance-coverage.md']
return [...new Set(files.flatMap(name => fs.readFileSync(path.join(refactorDir, name), 'utf8').match(/BASE-(?:LIFE|DOM|DEMO|TYPE|DIST|PERF)-\d{2}/g) || []))].sort()
}
function evidenceExists(name) {
const absolute = path.resolve(root, name)
assert(absolute.startsWith(root + path.sep) && fs.existsSync(absolute), `Missing or invalid evidence: ${name}`)
}
export function validateRisks(register) {
const tasks = new Set(read('tasks.json').tasks.map(task => task.id))
const ids = new Set(register.items.map(item => item.id))
assert.equal(ids.size, register.items.length, 'Duplicate risk ID')
for (const id of referencedFindings()) assert(ids.has(id), `Unregistered baseline finding: ${id}`)
for (const item of register.items) {
assert(['reproduced', 'source-observed', 'unverified'].includes(item.confirmation))
assert(['open', 'resolved', 'accepted-with-scope'].includes(item.status))
assert(item.owners.length && item.owners.every(id => tasks.has(id)), `Unowned risk: ${item.id}`)
assert(item.evidence.length && item.compatibleResolution && item.closureCriteria, `Incomplete risk: ${item.id}`)
item.evidence.forEach(evidenceExists)
if (item.status !== 'open') {
assert(item.resolutionEvidence?.length && item.resolutionRationale, `Premature closure: ${item.id}`)
item.resolutionEvidence.forEach(evidenceExists)
}
}
}
export function assetFingerprints(inputs) {
const walk = name => {
const absolute = path.resolve(root, name)
evidenceExists(name)
return fs.statSync(absolute).isDirectory() ? fs.readdirSync(absolute).flatMap(child => walk(`${name}/${child}`)) : [name]
}
return inputs.flatMap(walk).sort().map(name => {
const bytes = fs.readFileSync(path.join(root, name))
const text = /\.(?:[cm]?[jt]s|css|less|json|html|svg|txt|md)$/.test(name)
return { path: name, algorithm: text ? 'sha256-lf' : 'sha256', sha256: hash(text ? bytes.toString('utf8').replaceAll('\r\n', '\n') : bytes) }
})
}
export function validateThirdParty(inventory, register) {
const tasks = new Set(read('tasks.json').tasks.map(task => task.id))
const risks = new Set(register.items.map(item => item.id))
const initial = read('package-inventory.json').packages.map(pkg => pkg.name).sort()
assert.deepEqual(inventory.packages.map(pkg => pkg.name).sort(), initial, 'External dependency package coverage missing')
const lock = lockfile.parse(fs.readFileSync(path.join(root, 'yarn.lock'), 'utf8')).object
for (const pkg of inventory.packages) {
const current = JSON.parse(fs.readFileSync(path.join(root, 'packages', pkg.name, 'package.json'), 'utf8'))
assert.deepEqual(pkg.declaredRuntimeDependencies, current.dependencies || {}, `Update dependency inventory: ${pkg.name}`)
assert.deepEqual(pkg.developmentDependencies, current.devDependencies || {}, `Update development dependency inventory: ${pkg.name}`)
assert.deepEqual(pkg.resolvedRuntimeDependencies.map(dependency => dependency.name).sort(), Object.keys(current.dependencies || {}).sort(), 'Missing resolved dependency')
for (const dependency of pkg.resolvedRuntimeDependencies) {
assert.equal(dependency.range, current.dependencies[dependency.name], 'Dependency range changed')
const installed = JSON.parse(fs.readFileSync(path.join(root, 'node_modules', dependency.name, 'package.json'), 'utf8'))
assert.equal(dependency.lockedVersion, lock[`${dependency.name}@${dependency.range}`]?.version, 'Dependency lock changed; update provenance')
assert.equal(dependency.installedVersion, installed.version, 'Installed dependency changed')
assert.equal(dependency.manifestLicense, installed.license || null, 'Dependency license metadata changed')
}
}
assert.deepEqual(inventory.vendored.map(vendor => vendor.riskId).sort(), [...risks].filter(id => id.startsWith('VENDOR-')).sort(), 'Missing vendored group')
assert.deepEqual(inventory.integrations.map(sdk => sdk.riskId).sort(), [...risks].filter(id => id.startsWith('SDK-')).sort(), 'Missing SDK integration')
for (const vendor of inventory.vendored) {
assert(risks.has(vendor.riskId) && vendor.owners.length && vendor.owners.every(id => tasks.has(id)))
assert(vendor.sourceStatus && vendor.licenseStatus && vendor.updatePolicy)
assert.deepEqual(assetFingerprints(vendor.inputs), vendor.fingerprints, `Vendored files changed without reviewed provenance: ${vendor.id}`)
}
for (const sdk of inventory.integrations) {
assert(risks.has(sdk.riskId) && sdk.owners.length && sdk.owners.every(id => tasks.has(id)))
assert(sdk.sourceFiles.length && sdk.validationStatus && sdk.contract)
sdk.sourceFiles.forEach(evidenceExists)
}
}
export function renderRisks(register) {
const labels = { reproduced: '已复现', 'source-observed': '源码/产物事实', unverified: '待取证' }
const escape = text => text.replaceAll('|', '\\|').replaceAll('\n', ' ')
return `# 重构风险与差异索引\n\n由 risks.json 生成;运行 node refactor/scripts/risk-register.mjs --write 更新。状态 open 表示尚缺关闭证据,不等于每项都是已复现缺陷。处理方案、关闭条件与证据见 [机器台账](risks.json),来源与范围见 [维护说明](risk-guide.md)。\n\n| ID | 状态 / 证据等级 | 条目 | 后续任务 |\n| --- | --- | --- | --- |\n${register.items.map(item => `| ${item.id} | ${item.status} / ${labels[item.confirmation]} | ${escape(item.title)} | ${item.owners.join(', ')} |`).join('\n')}\n`
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
assert(process.argv.slice(2).every(arg => ['--check', '--write'].includes(arg)), 'Use --check or --write')
const register = read('risks.json')
const inventory = read('third-party.json')
validateRisks(register)
validateThirdParty(inventory, register)
const rendered = renderRisks(register)
if (process.argv.includes('--write')) fs.writeFileSync(path.join(refactorDir, 'risk-table.md'), rendered)
else assert.equal(fs.readFileSync(path.join(refactorDir, 'risk-table.md'), 'utf8').replaceAll('\r\n', '\n'), rendered, 'Regenerate risk-table.md')
console.log(`Risk register checked: ${register.items.length} items, ${inventory.vendored.length} vendored groups, ${inventory.integrations.length} integrations; no unresolved item is waived`)
}