mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 11:56:15 -08:00
160 lines
8.2 KiB
JavaScript
160 lines
8.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const dir = fileURLToPath(new URL('../', import.meta.url))
|
|
const root = path.resolve(dir, '..')
|
|
const data = JSON.parse(fs.readFileSync(path.join(dir, 'tasks.json'), 'utf8'))
|
|
const inventory = JSON.parse(fs.readFileSync(path.join(dir, 'package-inventory.json'), 'utf8'))
|
|
const states = new Set(['todo', 'doing', 'blocked', 'done', 'deferred'])
|
|
const ids = new Map()
|
|
|
|
assert.equal(data.schemaVersion, 1)
|
|
assert.equal(data.baselineCommit, inventory.baselineCommit)
|
|
assert.equal(data.qualityContract, 'quality-contract.md')
|
|
assert(fs.existsSync(path.join(dir, data.qualityContract)), 'Missing shared quality contract')
|
|
for (const task of data.tasks) {
|
|
assert(typeof task.id === 'string' && /^[A-Z]+(?:-[A-Z]+)*-\d{2}$/.test(task.id), `Invalid ID: ${task.id}`)
|
|
assert(!ids.has(task.id), `Duplicate ID: ${task.id}`)
|
|
assert(states.has(task.status), `Invalid status: ${task.id}`)
|
|
assert(['L', 'M', 'H'].includes(task.risk), `Invalid risk: ${task.id}`)
|
|
for (const key of ['phase', 'title', 'deliverable', 'acceptance']) {
|
|
assert(typeof task[key] === 'string' && task[key].trim(), `Missing ${key}: ${task.id}`)
|
|
}
|
|
for (const key of ['scope', 'dependsOn', 'evidence']) {
|
|
assert(Array.isArray(task[key]), `Invalid ${key}: ${task.id}`)
|
|
}
|
|
assert(task.scope.length, `Missing scope: ${task.id}`)
|
|
assert(new Set(task.dependsOn).size === task.dependsOn.length, `Duplicate dependency: ${task.id}`)
|
|
if (task.status === 'done') {
|
|
assert(task.evidence.length, `Missing evidence: ${task.id}`)
|
|
}
|
|
if (task.status === 'blocked' || task.status === 'deferred') {
|
|
assert(typeof task.reason === 'string' && task.reason.trim(), `Missing reason: ${task.id}`)
|
|
}
|
|
for (const evidence of task.evidence) {
|
|
assert(typeof evidence === 'string' && evidence.trim(), `Invalid evidence: ${task.id}`)
|
|
if (/^https?:\/\//.test(evidence)) continue
|
|
const resolved = path.resolve(dir, evidence.split('#')[0])
|
|
const relative = path.relative(dir, resolved)
|
|
assert(!relative.startsWith('..') && !path.isAbsolute(relative), `Evidence outside refactor: ${task.id}`)
|
|
assert(fs.existsSync(resolved), `Missing evidence file: ${evidence}`)
|
|
assert(!relative.replaceAll('\\', '/').endsWith('TEMPLATE.md'), `Template is not evidence: ${task.id}`)
|
|
}
|
|
ids.set(task.id, task)
|
|
}
|
|
|
|
const visited = new Set()
|
|
const visiting = new Set()
|
|
function visit(id) {
|
|
assert(ids.has(id), `Unknown dependency: ${id}`)
|
|
assert(!visiting.has(id), `Dependency cycle at ${id}`)
|
|
if (visited.has(id)) return
|
|
visiting.add(id)
|
|
for (const dependency of ids.get(id).dependsOn) {
|
|
visit(dependency)
|
|
if (['doing', 'done'].includes(ids.get(id).status)) {
|
|
assert.equal(ids.get(dependency).status, 'done', `Unfinished dependency ${dependency} for ${id}`)
|
|
}
|
|
}
|
|
visiting.delete(id)
|
|
visited.add(id)
|
|
}
|
|
for (const id of ids.keys()) visit(id)
|
|
|
|
function ancestors(id, found = new Set()) {
|
|
for (const dependency of ids.get(id).dependsOn) {
|
|
if (found.has(dependency)) continue
|
|
found.add(dependency)
|
|
ancestors(dependency, found)
|
|
}
|
|
return found
|
|
}
|
|
function requireAncestor(task, dependency) {
|
|
assert(ancestors(task).has(dependency), `${task} must depend on ${dependency}`)
|
|
}
|
|
assert(![...ancestors('PILOT-01')].some(id => id.startsWith('CORE-')), 'Pilot must precede core migration')
|
|
requireAncestor('CORE-01', 'PILOT-01')
|
|
for (const device of ['PKG-CAST-05', 'PKG-VAST-05', 'PKG-DPIP-05', 'PKG-MASK-05']) {
|
|
assert(!ancestors('REVIEW-01').has(device), `REVIEW-01 must not wait for ${device}`)
|
|
requireAncestor('REVIEW-02', device)
|
|
}
|
|
requireAncestor('REL-02', 'REL-09')
|
|
for (const release of ['REL-05', 'REL-06']) {
|
|
for (const gate of ['REVIEW-01', 'REVIEW-02', 'REVIEW-03', 'CI-04', 'REL-03', 'REL-04', 'REL-09']) {
|
|
requireAncestor(release, gate)
|
|
}
|
|
}
|
|
const finalDependencies = ancestors('REL-07')
|
|
for (const task of data.tasks) {
|
|
if (task.id === 'REL-07' || task.id.startsWith('DOC-')) continue
|
|
assert(finalDependencies.has(task.id), `Implementation task is disconnected from REL-07: ${task.id}`)
|
|
}
|
|
|
|
const packageNames = inventory.packages.map(pkg => pkg.name).sort()
|
|
const currentNames = fs.readdirSync(path.join(root, 'packages'))
|
|
.filter(name => fs.existsSync(path.join(root, 'packages', name, 'package.json'))).sort()
|
|
assert.deepEqual(currentNames, packageNames, 'Package scope changed: record the scope decision and update the validator/baseline policy')
|
|
for (const name of packageNames) {
|
|
assert(data.tasks.some(task => task.scope.includes(name)), `Package not planned: ${name}`)
|
|
}
|
|
const scopes = new Set([...packageNames, 'workspace', 'example/react.js', 'example/vue.js'])
|
|
for (const task of data.tasks) {
|
|
for (const scope of task.scope) assert(scopes.has(scope), `Unknown scope ${scope}: ${task.id}`)
|
|
}
|
|
|
|
let localLinks = 0
|
|
function markdownIn(directory) {
|
|
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const filename = path.join(directory, entry.name)
|
|
if (entry.isDirectory()) {
|
|
return ['.cache', 'node_modules', '.git'].includes(entry.name) ? [] : markdownIn(filename)
|
|
}
|
|
return entry.isFile() && entry.name.endsWith('.md') ? [filename] : []
|
|
})
|
|
}
|
|
const markdownFiles = markdownIn(dir)
|
|
for (const file of [...markdownFiles, path.join(root, 'AGENTS.md')]) {
|
|
const content = fs.readFileSync(file, 'utf8')
|
|
for (const match of content.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
const href = match[1]
|
|
if (/^(?:https?:|#)/.test(href)) continue
|
|
localLinks += 1
|
|
assert(fs.existsSync(path.resolve(path.dirname(file), href.split('#')[0])), `Broken local link in ${path.relative(root, file)}: ${href}`)
|
|
}
|
|
}
|
|
|
|
const cell = value => String(value).replaceAll('|', '\\|').replaceAll('\n', '<br>')
|
|
const counts = Object.fromEntries([...states].map(state => [state, data.tasks.filter(task => task.status === state).length]))
|
|
const lines = [
|
|
'# 完整重构执行计划表', '',
|
|
'> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。', '',
|
|
`基线:\`${data.baselineCommit}\`。总任务 ${data.tasks.length} 项,范围 ${packageNames.length} 个包及工作区/示例。`, '',
|
|
`状态:${Object.entries(counts).map(([state, count]) => `${state} ${count}`).join(' / ')}。风险 L/M/H 表示兼容风险,不表示工期。`, '',
|
|
'前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。', '',
|
|
`每项实施任务同时适用[全项目质量要求](${data.qualityContract}):清晰拆分职责和依赖,主动改善不合理设计,以有效测试证明旧接口兼容,并同步维护后续 AI 可接续的包内文档。`, '',
|
|
'## 包覆盖索引', '',
|
|
'| 包 | 基线版本 | 任务 |', '| --- | --- | --- |',
|
|
...inventory.packages.map(pkg => `| ${pkg.name} | ${pkg.version} | ${data.tasks.filter(task => task.scope.includes(pkg.name)).map(task => task.id).join(', ')} |`), '',
|
|
]
|
|
for (const phase of new Set(data.tasks.map(task => task.phase))) {
|
|
lines.push(`## ${phase}`, '', '| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |', '| --- | --- | --- | --- | --- | --- | --- |')
|
|
for (const task of data.tasks.filter(item => item.phase === phase)) {
|
|
lines.push(`| ${[task.id, `${task.scope.join(', ')}<br>${task.title}`, task.dependsOn.join(', ') || '-', task.deliverable, task.acceptance, task.risk, task.status].map(cell).join(' | ')} |`)
|
|
}
|
|
lines.push('')
|
|
}
|
|
lines.push('## 完成证据与阻塞', '')
|
|
for (const task of data.tasks.filter(item => item.evidence.length || item.reason)) {
|
|
lines.push(`- ${task.id}: ${task.evidence.map(link => `[记录](${link})`).join(' ')}${task.reason ? ` ${task.reason}` : ''}`)
|
|
}
|
|
lines.push('')
|
|
const output = `${lines.join('\n').trimEnd()}\n`
|
|
const target = path.join(dir, 'plan.md')
|
|
const mode = process.argv[2]
|
|
assert(['--write', '--check'].includes(mode), 'Use --write or --check')
|
|
if (mode === '--write') fs.writeFileSync(target, output)
|
|
else assert.equal(fs.readFileSync(target, 'utf8'), output, 'plan.md is stale; run --write')
|
|
console.log(`Plan ${mode}: ${data.tasks.length} tasks, ${packageNames.length} packages, ${localLinks} local links, no dependency cycles; ${JSON.stringify(counts)}`)
|