mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 11:56:15 -08:00
115 lines
6.8 KiB
JavaScript
115 lines
6.8 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
|
|
export const libraryGates = ['build', 'runtime', 'types', 'combinations', 'browser', 'devices', 'licenses', 'rollback', 'remote-ci', 'review-01', 'review-02', 'review-03']
|
|
export const siteGates = ['build', 'site-urls', 'site-assets', 'editor', 'devices', 'licenses', 'rollback', 'remote-ci', 'review-01', 'review-02', 'review-03']
|
|
export const sharedTasks = ['CI-01', 'CI-03', 'CI-04', 'REL-01', 'REL-09', 'REL-02', 'REL-03', 'REL-04', 'REVIEW-01', 'REVIEW-02', 'REVIEW-03']
|
|
|
|
export function dependencyClosure(name, names, edges, dynamicImports = []) {
|
|
if (dynamicImports.length)
|
|
return [...names].sort()
|
|
const dependencies = new Set([name])
|
|
let count = -1
|
|
while (count !== dependencies.size) {
|
|
count = dependencies.size
|
|
for (const edge of edges) {
|
|
if (dependencies.has(edge.consumer))
|
|
dependencies.add(edge.dependency)
|
|
}
|
|
}
|
|
return [...dependencies].sort()
|
|
}
|
|
|
|
export function findingPackages(item, tasks, names) {
|
|
const explicit = new Set(item.owners.flatMap(id => tasks.get(id)?.scope || []).filter(scope => names.includes(scope)))
|
|
// A generic review owner must not turn a package-specific finding into a global one.
|
|
return explicit.size ? [...explicit] : [...names]
|
|
}
|
|
|
|
export function validateLedger(ledger, inventory, tasks) {
|
|
assert.equal(ledger.schemaVersion, 1)
|
|
const expected = inventory.map(pkg => pkg.name).sort()
|
|
assert.deepEqual(ledger.packages.map(pkg => pkg.name).sort(), expected, 'Ledger must cover exactly every workspace package once')
|
|
assert.deepEqual(ledger.libraryGates, libraryGates, 'Missing mandatory library gate')
|
|
assert.deepEqual(ledger.siteGates, siteGates, 'Missing mandatory site gate')
|
|
assert.deepEqual(ledger.sharedTasks, sharedTasks, 'Missing mandatory shared release/review task')
|
|
for (const row of ledger.packages) {
|
|
const initial = inventory.find(pkg => pkg.name === row.name)
|
|
assert.equal(row.targetVersion, `${Number(initial.version.split('.')[0]) + 1}.0.0`, `Unexpected next major: ${row.name}`)
|
|
assert(['npm', 'renamed-npm', 'recovered-npm', 'site'].includes(row.distribution), `Unknown distribution: ${row.name}`)
|
|
assert.equal(row.distribution === 'site', initial.kind === 'docs', 'A site must not become a new npm publication implicitly')
|
|
assert(row.history?.file && row.history.rationale, `Missing distribution basis: ${row.name}`)
|
|
assert(row.rollback?.basis && row.rollback.strategy, `Missing rollback mapping: ${row.name}`)
|
|
assert(Array.isArray(row.extraRequirements) && row.extraRequirements.length, `Missing capability requirements: ${row.name}`)
|
|
assert(row.extraRequirements.every(item => typeof item === 'string' && item.trim()), 'Invalid capability requirement')
|
|
assert(new Set(row.extraRequirements).size === row.extraRequirements.length, 'Duplicate requirement')
|
|
assert(row.evidence && !Array.isArray(row.evidence) && typeof row.evidence === 'object')
|
|
assert(Object.keys(row.evidence).every(gate => (row.distribution === 'site' ? siteGates : libraryGates).includes(gate)), 'Unknown evidence gate')
|
|
assert(row.candidate === null || typeof row.candidate === 'object', 'Invalid candidate')
|
|
}
|
|
for (const id of ledger.sharedTasks)
|
|
assert(tasks.has(id), `Unknown release task: ${id}`)
|
|
}
|
|
|
|
export function evaluatePackage({ row, fingerprint, version, candidate, evidence, taskGaps, risks, assets, history }) {
|
|
const gates = row.distribution === 'site' ? siteGates : libraryGates
|
|
const blockers = []
|
|
const add = (kind, detail) => blockers.push({ kind, detail })
|
|
if (version !== row.targetVersion)
|
|
add('version', `${version} must be prepared as ${row.targetVersion} before final candidate validation`)
|
|
if (!history.verified)
|
|
add('distribution-history', history.reason)
|
|
if (!candidate) {
|
|
add('candidate', 'No candidate artifact is bound')
|
|
}
|
|
else {
|
|
for (const error of candidate.errors)
|
|
add('candidate', error)
|
|
if (candidate.inputFingerprint !== fingerprint)
|
|
add('stale-candidate', 'Source, dependencies, tooling or test inputs changed')
|
|
if (candidate.version !== version)
|
|
add('candidate-version', 'Artifact version differs from current manifest')
|
|
}
|
|
for (const id of taskGaps) add('task', id)
|
|
for (const risk of risks.filter(risk => risk.status === 'open')) add('risk', risk.id)
|
|
const checks = {}
|
|
for (const gate of gates) {
|
|
const report = evidence[gate]
|
|
const errors = []
|
|
if (!report) {
|
|
errors.push('Missing candidate-bound evidence')
|
|
}
|
|
else {
|
|
errors.push(...report.errors)
|
|
if (report.result !== 'pass')
|
|
errors.push(`Result is ${report.result}`)
|
|
if (report.package !== row.name || report.gate !== gate)
|
|
errors.push('Package/gate scope mismatch')
|
|
if (report.version !== version)
|
|
errors.push('Evidence version mismatch')
|
|
if (!candidate || report.candidateIntegrity !== candidate.integrity)
|
|
errors.push('Candidate integrity mismatch')
|
|
if (report.inputFingerprint !== fingerprint)
|
|
errors.push('Evidence input fingerprint is stale')
|
|
if (!report.checks?.length || report.checks.some(check => check.result !== 'pass'))
|
|
errors.push('Required checks contain missing, failed, skipped or unknown results')
|
|
if (!report.environment?.length || !report.command || !report.reviewedBy)
|
|
errors.push('Missing environment, command or review attribution')
|
|
if (gate === 'devices' && report.environment?.some(env => env.emulated !== false || !env.device || !env.os || !env.browser))
|
|
errors.push('Device evidence must explicitly identify real devices, OS and browser')
|
|
if (gate === 'devices' && report.checks?.some(check => check.mode !== 'native'))
|
|
errors.push('Device/capability checks cannot use mocks or emulation')
|
|
if (gate === 'remote-ci' && !report.environment?.some(env => env.remote === true && /^https:\/\/github\.com\/[^/]+\/[^/]+\/actions\/runs\/\d+$/.test(env.runUrl)))
|
|
errors.push('Remote CI evidence needs an actual GitHub Actions run reference')
|
|
const required = gate === 'devices' ? row.extraRequirements : gate === 'licenses' ? assets.map(asset => asset.id) : []
|
|
const covered = new Set(report.checks?.filter(check => check.result === 'pass').map(check => check.id))
|
|
for (const requirement of required) {
|
|
if (!covered.has(requirement))
|
|
errors.push(`Missing coverage: ${requirement}`)
|
|
}
|
|
}
|
|
checks[gate] = errors.length ? { status: 'blocked', errors } : { status: 'evidence-recorded' }
|
|
for (const error of errors) add(`evidence:${gate}`, error)
|
|
}
|
|
return { name: row.name, version, targetVersion: row.targetVersion, distribution: row.distribution, fingerprint, candidate, history, rollback: row.rollback, risks, assets, checks, blockers, status: blockers.length ? 'blocked' : 'evidence-complete', publicationAuthorized: false }
|
|
}
|