Govern task context and advance execution pointer
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-20 06:02:43 -04:00
parent 380cbed4ff
commit 10640aeb3c
984 changed files with 543475 additions and 327 deletions

View File

@@ -0,0 +1,379 @@
import fs from "node:fs";
import crypto from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
export const CONTEXT_LIMITS = Object.freeze({
queueBytes: 4096,
taskBytes: 8192,
parentManifestBytes: 24576,
parentStatusBytes: 6144,
evidenceFiles: 12,
evidenceBytes: 8192,
contextTokens: 3500,
});
const read = (file) => fs.readFileSync(file, "utf8");
const json = (file) => JSON.parse(read(file));
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const tokenEstimate = (value) => Math.ceil(Buffer.byteLength(value, "utf8") / 4);
const queuePath = path.join(root, "docs/EXECUTION_QUEUE.md");
const planPath = path.join(root, "tests/golden/M15-03A/next-task-plan.json");
const taskIndexPath = path.join(root, "tests/golden/M15-03A/task-index.json");
const sha256File = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
let cachedTaskIndex;
let taskIndexLoaded = false;
const safeName = (value) => value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96) || "gap";
const expandTask = (task) => ({
...task,
fixture: { path: `tests/files/web/generated/${task.id}-${safeName(task.gapId)}.blend`, state: "REQUIRED" },
desktopCommand: `build_blender_5.2.0/bin/blender -b --factory-startup --python tools/web/generated/${task.id}.py -- tests/files/web/generated/${task.id}-${safeName(task.gapId)}.blend`,
webCommand: `npm --prefix web run test:generated-gap -- --task ${task.id}`,
comparator: `node tools/web/check-generated-gap.mjs --task ${task.id}`,
exitCriteria: ["desktop fixture evidence exists", "WASM uses the same fixture", "comparator passes", "save/reopen preserves Main", "manifest hashes all artifacts"],
});
export function parseQueue() {
const source = read(queuePath);
const current = source.match(/当前任务\s*[:]\s*`([^`]+)`/u)?.[1]
?? source.match(/\|\s*当前任务\s*\|\s*`([^`]+)`/u)?.[1];
const parentManifest = source.match(/parent manifest\s*[:]\s*`?([^`\n]+)`?/iu)?.[1]?.trim()
?? source.match(/\|\s*parent manifest\s*\|\s*`([^`]+)`/u)?.[1];
if (!current || !parentManifest) throw new Error("EXECUTION_QUEUE.md must declare current task and parent manifest");
return { path: queuePath, source, currentTask: current, parentManifest };
}
function loadTaskIndex() {
if (taskIndexLoaded) return cachedTaskIndex;
taskIndexLoaded = true;
if (!fs.existsSync(taskIndexPath)) {
if (fs.existsSync(planPath)) throw new Error(`missing task index; run node tools/web/generate-task-index.mjs (${relative(taskIndexPath)})`);
return null;
}
const index = json(taskIndexPath);
if (index.schemaVersion !== 1 || !index.source?.path || !index.catalog?.path || !index.entries) {
throw new Error(`invalid task index: ${relative(taskIndexPath)}`);
}
cachedTaskIndex = index;
return cachedTaskIndex;
}
export function verifyTaskIndex() {
const index = loadTaskIndex();
if (!index) return null;
const sourcePath = path.join(root, index.source.path);
const catalogPath = path.join(root, index.catalog.path);
if (!fs.existsSync(sourcePath) || sha256File(sourcePath) !== index.source.sha256) {
throw new Error(`task index is stale; run node tools/web/generate-task-index.mjs (${relative(taskIndexPath)})`);
}
if (!fs.existsSync(catalogPath) || sha256File(catalogPath) !== index.catalog.sha256) {
throw new Error(`task catalog hash mismatch; run node tools/web/generate-task-index.mjs (${relative(taskIndexPath)})`);
}
return index;
}
function indexedEntry(task, index = loadTaskIndex()) {
const metadata = index?.entries?.[task];
if (!metadata) return null;
const catalogPath = path.join(root, index.catalog.path);
if (!fs.existsSync(catalogPath)) throw new Error(`missing task catalog: ${relative(catalogPath)}`);
const handle = fs.openSync(catalogPath, "r");
try {
const buffer = Buffer.alloc(metadata.length);
fs.readSync(handle, buffer, 0, metadata.length, metadata.offset);
const entry = { ...expandTask(JSON.parse(buffer.toString("utf8"))), previous: metadata.previous, next: metadata.next };
if (entry.id !== task) throw new Error(`task catalog id mismatch for ${task}`);
return entry;
} finally {
fs.closeSync(handle);
}
}
export function readIndexedTask(task) {
return indexedEntry(task);
}
function planEntry(task) {
const indexed = indexedEntry(task);
if (indexed) return indexed;
return null;
}
function planParent(task) {
const index = loadTaskIndex();
if (index?.entries?.[task]) return index.entries[task].previous;
return null;
}
function cardFields(source) {
const get = (name) => source.match(new RegExp(`^[-*]?\\s*\\x60?${name}\\x60?\\s*[:]\\s*\\x60?([^\\n\\x60]+)`, "imu"))?.[1]?.trim();
const task = get("task");
const parent = get("parent");
const status = get("status");
const gap = get("gap");
const ownerFamily = get("ownerFamily");
const targetImplementationClass = get("targetImplementationClass");
return { task, parent, status, gap, ownerFamily, targetImplementationClass };
}
function commandLines(source) {
const candidates = [];
for (const line of source.split("\n")) {
const clean = line.replaceAll("`", "").replace(/^\s*[-*]\s*/, "").trim();
if (clean === "bash") continue;
if (/^(npm|node|cmake|ctest|bash|build_[A-Za-z0-9_./-]+\/bin\/blender)\b/u.test(clean)) candidates.push(clean);
}
return [...new Set(candidates)].slice(0, 8);
}
function inputPaths(source) {
const paths = [];
for (const match of source.matchAll(/`([^`]+)`/gu)) {
const value = match[1].trim();
if (!value || /\s/u.test(value)) continue;
if (/^(docs|tests|tools|web|blender-5\.2\.0|build_blender_5\.2\.0)\//u.test(value)) paths.push(value);
}
return [...new Set(paths)];
}
function pathBytes(file, limit) {
let stat;
try {
stat = fs.lstatSync(file);
} catch (error) {
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return { bytes: null, reason: "MISSING_PATH" };
return { bytes: null, reason: "PATH_STAT_FAILED" };
}
if (stat.isSymbolicLink()) return { bytes: null, reason: "SYMLINK_PATH" };
if (stat.isFile()) return { bytes: stat.size };
if (!stat.isDirectory()) return { bytes: null, reason: "UNREADABLE_PATH_TYPE" };
// Directory evidence is measured recursively, but stop as soon as it cannot fit.
let bytes = 0;
const stack = [file];
while (stack.length > 0) {
const current = stack.pop();
let entries;
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
return { bytes: null, reason: "PATH_STAT_FAILED" };
}
for (const entry of entries) {
const child = path.join(current, entry.name);
if (entry.isSymbolicLink()) return { bytes: null, reason: "SYMLINK_PATH" };
if (entry.isDirectory()) {
stack.push(child);
continue;
}
if (entry.isFile()) {
try {
bytes += fs.statSync(child).size;
} catch {
return { bytes: null, reason: "PATH_STAT_FAILED" };
}
if (bytes > limit) return { bytes };
}
}
}
return { bytes };
}
const pathWithinRoot = (candidate) => {
if (typeof candidate !== "string" || candidate.length === 0 || path.isAbsolute(candidate)) return false;
const absolute = path.resolve(root, candidate);
return absolute === root || absolute.startsWith(`${root}${path.sep}`);
};
const normalizedPathKey = (candidate) => path.normalize(candidate).replace(/[\\/]+$/u, "");
export function selectInputPaths(candidates, {
sourceBytes = 0,
sourceTokens = 0,
reservedPaths = [],
generatedPaths = [],
limits = {},
} = {}) {
const effectiveLimits = { ...CONTEXT_LIMITS, ...limits };
const unique = [...new Set((candidates ?? []).filter((candidate) => typeof candidate === "string" && candidate.length > 0))];
const reserved = new Set(reservedPaths.map(normalizedPathKey));
const generated = new Set(generatedPaths.map(normalizedPathKey));
const contextRemainingTokens = Math.max(0, effectiveLimits.contextTokens - sourceTokens);
const contextRemainingBytes = Math.max(0, effectiveLimits.contextTokens * 4 - sourceBytes);
const excluded = [];
const selected = [];
let selectedBytes = 0;
let selectedTokens = 0;
for (const candidate of unique) {
const normalized = normalizedPathKey(candidate);
if (generated.has(normalized)) {
excluded.push({ path: candidate, bytes: null, reason: "GENERATED_EVIDENCE_OUTPUT" });
continue;
}
if (reserved.has(normalized)) {
excluded.push({ path: candidate, bytes: 0, reason: "ALREADY_IN_CONTEXT" });
continue;
}
if (!pathWithinRoot(candidate)) {
excluded.push({ path: candidate, bytes: null, reason: "PATH_OUTSIDE_REPOSITORY" });
continue;
}
const absolute = path.join(root, candidate);
const measured = pathBytes(absolute, effectiveLimits.evidenceBytes);
if (measured.reason) {
excluded.push({ path: candidate, bytes: measured.bytes, reason: measured.reason });
continue;
}
const bytes = measured.bytes;
const tokens = Math.ceil(bytes / 4);
if (selected.length >= effectiveLimits.evidenceFiles) {
excluded.push({ path: candidate, bytes, reason: "EVIDENCE_FILE_COUNT" });
continue;
}
if (bytes > effectiveLimits.evidenceBytes || selectedBytes + bytes > effectiveLimits.evidenceBytes) {
excluded.push({ path: candidate, bytes, reason: "EVIDENCE_BYTE_BUDGET" });
continue;
}
if (bytes > contextRemainingBytes || selectedBytes + bytes > contextRemainingBytes
|| tokens > contextRemainingTokens || selectedTokens + tokens > contextRemainingTokens) {
excluded.push({ path: candidate, bytes, reason: "CONTEXT_REMAINING_SPACE" });
continue;
}
selected.push({ path: candidate, bytes, tokens });
selectedBytes += bytes;
selectedTokens += tokens;
}
return {
inputPaths: selected.map(({ path: candidate }) => candidate),
inputSelection: {
schemaVersion: 1,
limits: {
maxFiles: effectiveLimits.evidenceFiles,
evidenceBytes: effectiveLimits.evidenceBytes,
contextRemainingBytes,
contextRemainingTokens,
},
source: { bytes: sourceBytes, tokens: sourceTokens },
selected,
excluded,
totals: { files: selected.length, bytes: selectedBytes, tokens: selectedTokens },
},
};
}
function nextFromPlan(task) {
const index = loadTaskIndex();
if (index?.entries?.[task]) return index.entries[task].next;
return null;
}
export function buildTaskContext(requestedTask) {
verifyTaskIndex();
const queue = parseQueue();
const task = requestedTask ?? queue.currentTask;
const taskPath = path.join(root, "docs/tasks", `${task}.md`);
const entry = planEntry(task);
const taskExists = fs.existsSync(taskPath);
const taskSource = taskExists ? read(taskPath) : "";
const fields = taskExists ? cardFields(taskSource) : {};
if (taskExists && fields.task !== task) throw new Error(`task card id mismatch: ${relative(taskPath)}`);
const parent = fields.parent ?? (task === queue.currentTask ? path.basename(path.dirname(queue.parentManifest)) : planParent(task));
if (!parent) throw new Error(`task ${task} has no parent; add a compact task card or plan parent`);
const parentManifestPath = path.join(root, "tests/golden", parent, "manifest.json");
if (!fs.existsSync(parentManifestPath)) throw new Error(`missing parent manifest: ${relative(parentManifestPath)}`);
const parentManifestSource = read(parentManifestPath);
const parentManifest = JSON.parse(parentManifestSource);
if (parentManifest.nextTask !== task) throw new Error(`parent manifest nextTask=${parentManifest.nextTask} does not point to ${task}`);
if (parentManifest.status && parentManifest.status !== "done" && !parentManifest.enablingTask) {
throw new Error(`parent manifest status=${parentManifest.status} is not complete`);
}
const parentStatusPath = path.join(root, "docs/status", `${parent}.md`);
const parentStatusSource = fs.existsSync(parentStatusPath) ? read(parentStatusPath) : "";
const ownManifestPath = path.join(root, "tests/golden", task, "manifest.json");
const ownManifest = fs.existsSync(ownManifestPath) ? json(ownManifestPath) : null;
if (!requestedTask && task === queue.currentTask && ownManifest?.status === "done") {
throw new Error(`queue current task ${task} is already done; advance EXECUTION_QUEUE.md to ${ownManifest.nextTask}`);
}
if (!requestedTask && task === queue.currentTask && path.basename(path.dirname(queue.parentManifest)) !== parent) {
throw new Error(`queue parent manifest does not match task card parent ${parent}`);
}
const goal = taskExists
? (taskSource.match(/^##\s+目标\s*\n([\s\S]*?)(?=^##\s|$)/mu)?.[1].trim()
?? taskSource.match(/^#\s+[^\n:]+[:]\s*(.+)$/mu)?.[1].trim()
?? taskSource.match(/^#\s+[^\n]+\n\n([^\n]+)/u)?.[1].trim()
?? `完成 ${entry?.gapId ?? task} 的最小可观察切片`)
: `完成 ${entry?.gapId ?? task} 的最小可观察切片`;
const commands = [...commandLines(taskSource), ...(entry ? [entry.desktopCommand, entry.webCommand, entry.comparator] : [])].filter(Boolean).filter((value, index, values) => values.indexOf(value) === index).slice(0, 8);
const taskBytes = Buffer.byteLength(taskSource, "utf8");
const taskInputPaths = taskExists ? inputPaths(taskSource) : [];
const candidatePaths = taskExists && taskInputPaths.length > 0
? taskInputPaths
: [entry?.fixture?.path, entry?.comparator && "tools/web/check-generated-gap.mjs"].filter(Boolean);
const contextSources = [queue.source, taskSource, parentManifestSource, parentStatusSource];
const sourceBytes = contextSources.reduce((sum, source) => sum + Buffer.byteLength(source, "utf8"), 0);
const sourceTokens = contextSources.reduce((sum, source) => sum + tokenEstimate(source), 0);
const reservedPaths = [relative(queuePath), relative(taskPath), relative(parentManifestPath), relative(parentStatusPath)];
const ownStatusPath = path.join(root, "docs/status", `${task}.md`);
const ownOutputDir = path.join(root, "tests/golden", task);
const generatedPaths = [relative(ownOutputDir), relative(ownManifestPath), relative(ownStatusPath)];
const selectedInputs = selectInputPaths(candidatePaths, { sourceBytes, sourceTokens, reservedPaths, generatedPaths });
const nextTask = ownManifest?.nextTask ?? nextFromPlan(task) ?? null;
const context = {
schemaVersion: 1,
task,
parentTask: parent,
status: fields.status ?? entry?.state ?? "pending",
goal,
scope: {
gap: fields.gap ?? entry?.gapId ?? "NOT_APPLICABLE",
ownerFamily: fields.ownerFamily ?? entry?.ownerFamily ?? "NOT_APPLICABLE",
implementationClass: fields.targetImplementationClass ?? entry?.targetImplementationClass ?? "NOT_APPLICABLE",
},
commands,
inputPaths: selectedInputs.inputPaths,
inputSelection: selectedInputs.inputSelection,
exitCriteria: entry?.exitCriteria ?? ["focused command exits 0", "report and manifest are hash-bound", "failure path does not publish a late result"],
nextTask,
sourceDocuments: {
queue: relative(queuePath),
taskCard: taskExists ? relative(taskPath) : "TASK_INDEX_ENTRY",
taskIndex: relative(taskIndexPath),
parentManifest: relative(parentManifestPath),
parentStatus: fs.existsSync(parentStatusPath) ? relative(parentStatusPath) : "NOT_APPLICABLE",
},
readPolicy: {
required: [relative(queuePath), taskExists ? relative(taskPath) : "TASK_INDEX_ENTRY", relative(parentManifestPath), fs.existsSync(parentStatusPath) ? relative(parentStatusPath) : "NOT_APPLICABLE"],
machineOnly: [relative(taskIndexPath), "tests/golden/M15-03A/task-catalog.jsonl"],
optionalByNeed: ["docs/WEB_BLENDER_MODELER_V1_SCOPE.md", "任务卡列出的精确协议/生产路径", "任务卡明确命名的长期计划小节"],
forbiddenByDefault: ["完整 CURRENT_EXECUTION_PLAN.md", "完整 PROJECT_STATUS_AND_NEXT_WORK.md", "全部 docs/status/*.md", "完整 parity/WBS 计划", "README 历史和 test-results/"],
},
parent: {
manifest: {
schemaVersion: parentManifest.schemaVersion,
task: parentManifest.task,
parentTask: parentManifest.parentTask,
status: parentManifest.status ?? "done",
nextTask: parentManifest.nextTask,
artifactCount: Object.keys(parentManifest.artifacts ?? {}).length,
},
statusSummary: parentStatusSource.split("\n").filter(Boolean).slice(0, 8),
},
budgets: { ...CONTEXT_LIMITS, taskBytes, taskLines: taskSource ? taskSource.split("\n").length : 0, taskTokens: tokenEstimate(taskSource) },
};
return { context, files: { queuePath, taskPath, parentManifestPath, parentStatusPath }, sources: { queue, taskSource, parentManifest, parentManifestSource, parentStatusSource } };
}
export function contextSizeReport(bundle) {
const { context, files, sources } = bundle;
const measure = (file, source) => ({ path: relative(file), bytes: Buffer.byteLength(source, "utf8"), lines: source ? source.split("\n").length : 0, tokens: tokenEstimate(source) });
const docs = [measure(files.queuePath, sources.queue.source), measure(files.taskPath, sources.taskSource), measure(files.parentManifestPath, sources.parentManifestSource), measure(files.parentStatusPath, sources.parentStatusSource)];
const sourceTokens = docs.reduce((sum, item) => sum + item.tokens, 0);
const evidenceBytes = context?.inputSelection?.totals?.bytes ?? 0;
const evidenceFiles = context?.inputSelection?.totals?.files ?? 0;
const evidenceTokens = context?.inputSelection?.totals?.tokens ?? 0;
const totalTokens = sourceTokens + evidenceTokens;
return { documents: docs, sourceTokens, evidenceFiles, evidenceBytes, evidenceTokens, totalTokens, withinBudget: docs[0].bytes <= CONTEXT_LIMITS.queueBytes && docs[1].bytes <= CONTEXT_LIMITS.taskBytes && docs[2].bytes <= CONTEXT_LIMITS.parentManifestBytes && docs[3].bytes <= CONTEXT_LIMITS.parentStatusBytes && evidenceFiles <= CONTEXT_LIMITS.evidenceFiles && evidenceBytes <= CONTEXT_LIMITS.evidenceBytes && totalTokens <= CONTEXT_LIMITS.contextTokens };
}