Files
Web_FreeCAD_Bitbybit/scripts/check-freecad-execution-plan.mjs

81 lines
4.6 KiB
JavaScript

import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-execution-plan.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD execution plan: ${message}`) }
const requireText = (value, label) => { if (typeof value !== 'string' || !value.trim()) fail(`${label} must be a non-empty string.`) }
const requireTextList = (value, label) => {
if (!Array.isArray(value) || value.length === 0) fail(`${label} must be a non-empty array.`)
value.forEach((entry, index) => requireText(entry, `${label}[${index}]`))
}
const uniqueIds = (items, label) => {
if (!Array.isArray(items) || items.length === 0) fail(`${label} must be a non-empty array.`)
const ids = new Set()
for (const item of items) {
requireText(item?.id, `${label} id`)
if (ids.has(item.id)) fail(`duplicate ${label} id '${item.id}'.`)
ids.add(item.id)
}
return ids
}
if (plan.schemaVersion !== 1) fail(`unsupported schemaVersion '${plan.schemaVersion}'.`)
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.freecadCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline does not match the locked FreeCAD reference.')
const laneIds = uniqueIds(plan.lanes, 'lane')
const gateIds = uniqueIds(plan.gates, 'gate')
const programIds = uniqueIds(plan.programs, 'program')
const requiredPrograms = Array.from({ length: 10 }, (_, index) => `P${String(index + 1).padStart(2, '0')}`)
if (requiredPrograms.some((id) => !programIds.has(id)) || plan.programs.length !== requiredPrograms.length) fail('programs must contain exactly P01 through P10.')
const taskById = new Map()
const taskProgram = new Map()
const statuses = new Set(['pending', 'in_progress', 'completed', 'blocked'])
for (const program of plan.programs) {
requireText(program.title, `${program.id}.title`)
if (!laneIds.has(program.ownerLane)) fail(`${program.id} uses unknown owner lane '${program.ownerLane}'.`)
if (!gateIds.has(program.gate)) fail(`${program.id} uses unknown gate '${program.gate}'.`)
if (!Array.isArray(program.dependencies)) fail(`${program.id}.dependencies must be an array.`)
for (const dependency of program.dependencies) if (!programIds.has(dependency)) fail(`${program.id} depends on unknown program '${dependency}'.`)
if (!Array.isArray(program.tasks) || program.tasks.length === 0) fail(`${program.id} must contain tasks.`)
for (const task of program.tasks) {
requireText(task.id, `${program.id} task id`)
if (taskById.has(task.id)) fail(`duplicate task id '${task.id}'.`)
requireText(task.title, `${task.id}.title`)
if (!statuses.has(task.status)) fail(`${task.id} has invalid status '${task.status}'.`)
if (!Array.isArray(task.dependencies)) fail(`${task.id}.dependencies must be an array.`)
requireTextList(task.deliverables, `${task.id}.deliverables`)
requireTextList(task.acceptance, `${task.id}.acceptance`)
taskById.set(task.id, task)
taskProgram.set(task.id, program.id)
}
}
for (const task of taskById.values()) for (const dependency of task.dependencies) if (!taskById.has(dependency)) fail(`${task.id} depends on unknown task '${dependency}'.`)
const visitState = new Map()
const visit = (taskId, path = []) => {
const state = visitState.get(taskId)
if (state === 'done') return
if (state === 'visiting') fail(`task dependency cycle: ${[...path, taskId].join(' -> ')}.`)
visitState.set(taskId, 'visiting')
for (const dependency of taskById.get(taskId).dependencies) visit(dependency, [...path, taskId])
visitState.set(taskId, 'done')
}
for (const taskId of taskById.keys()) visit(taskId)
for (const gate of plan.gates) {
requireText(gate.title, `${gate.id}.title`)
requireTextList(gate.requiredPrograms, `${gate.id}.requiredPrograms`)
for (const programId of gate.requiredPrograms) if (!programIds.has(programId)) fail(`${gate.id} references unknown program '${programId}'.`)
}
for (const program of plan.programs) {
const gate = plan.gates.find((candidate) => candidate.id === program.gate)
if (!gate.requiredPrograms.includes(program.id)) fail(`${program.gate} does not require ${program.id}.`)
}
requireTextList(plan.rules, 'rules')
const statusCounts = Object.fromEntries([...statuses].map((status) => [status, [...taskById.values()].filter((task) => task.status === status).length]))
console.log(JSON.stringify({ status: 'execution-plan-pass', programs: plan.programs.length, lanes: plan.lanes.length, gates: plan.gates.length, tasks: taskById.size, statusCounts, taskPrograms: new Set(taskProgram.values()).size }, null, 2))