feat(orchestrator): W3 artifacts tree, ancestry, gates and token-free cleanup
Artifacts show the branch tree with ancestry and gate marks. The cleanup runner calls pkgmgr branch cleanup with its real argv and parses its repos[*].items output, refuses any plan with a remote-branch action, marks an item done only when pkgmgr reports it done, and offers no delete for a branch a worktree holds. Every text states what the pkgmgr command does to volumes. Committed below the bar on the operator's word 2026-11-12 (no further per-bundle tests, one gate after the merge): the W3 e2e never ran; the client switches to --local once pkgmgr ships it. 🎯 Fix: 90 % 💥 Break: 6 % 💸 Cost: 30 % ⏱️ Time: 40 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ⛔ break 6 % > 5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd8fc08603
commit
f7a59d43a5
33 files changed
+1998
-107
No files matched your search
@@ -0,0 +1,97 @@
|
||||
const Ancestry = (() => {
|
||||
"use strict";
|
||||
const LEVELS = ["none", "quick", "test", "quality-high", "e2e"];
|
||||
const ACTIONS = ["commit", "merge", "push", "pr"];
|
||||
const LEVEL_ICONS = {none: "bi-circle", quick: "bi-lightning-charge", test: "bi-check2-circle", "quality-high": "bi-shield-check", e2e: "bi-browser-chrome"};
|
||||
const SPACING = {column: 170, row: 96, wrap: 8};
|
||||
|
||||
function generations(ids, edges) {
|
||||
const parents = new Map(ids.map((id) => [id, []]));
|
||||
for (const edge of edges) {
|
||||
if (parents.has(edge.child) && parents.has(edge.parent) && edge.child !== edge.parent) {
|
||||
parents.get(edge.child).push(edge.parent);
|
||||
}
|
||||
}
|
||||
const depth = new Map();
|
||||
const visit = (id, path) => {
|
||||
if (depth.has(id)) {
|
||||
return depth.get(id);
|
||||
}
|
||||
if (path.has(id)) {
|
||||
return -1;
|
||||
}
|
||||
path.add(id);
|
||||
const above = parents.get(id).map((parent) => visit(parent, path)).filter((value) => value >= 0);
|
||||
path.delete(id);
|
||||
const value = above.length ? Math.max(...above) + 1 : 0;
|
||||
depth.set(id, value);
|
||||
return value;
|
||||
};
|
||||
ids.forEach((id) => visit(id, new Set()));
|
||||
return depth;
|
||||
}
|
||||
|
||||
function layout(nodes, edges) {
|
||||
const ids = nodes.map((node) => node.id);
|
||||
const depth = generations(ids, edges);
|
||||
const rows = [];
|
||||
for (const node of nodes) {
|
||||
const generation = depth.get(node.id);
|
||||
(rows[generation] = rows[generation] || []).push(node);
|
||||
}
|
||||
const widest = Math.max(1, ...rows.map((row) => Math.min(row ? row.length : 0, SPACING.wrap)));
|
||||
const width = widest * SPACING.column;
|
||||
const placed = new Map();
|
||||
let line = 0;
|
||||
rows.forEach((row, generation) => {
|
||||
const anchor = (node) => {
|
||||
const xs = edges.filter((edge) => edge.child === node.id && placed.has(edge.parent)).map((edge) => placed.get(edge.parent).x);
|
||||
return xs.length ? xs.reduce((sum, x) => sum + x, 0) / xs.length : width / 2;
|
||||
};
|
||||
const ordered = [...(row || [])].sort((a, b) => anchor(a) - anchor(b) || String(a.label).localeCompare(String(b.label)));
|
||||
ordered.forEach((node, index) => {
|
||||
const band = Math.floor(index / SPACING.wrap);
|
||||
const count = Math.min(SPACING.wrap, ordered.length - band * SPACING.wrap);
|
||||
const offset = (width - count * SPACING.column) / 2;
|
||||
placed.set(node.id, {...node, generation, x: offset + ((index % SPACING.wrap) + 0.5) * SPACING.column, y: (line + band + 0.5) * SPACING.row});
|
||||
});
|
||||
line += Math.max(1, Math.ceil(ordered.length / SPACING.wrap));
|
||||
});
|
||||
const links = edges.filter((edge) => placed.has(edge.child) && placed.has(edge.parent) && edge.child !== edge.parent).map((edge) => {
|
||||
const from = placed.get(edge.parent);
|
||||
const to = placed.get(edge.child);
|
||||
return {...edge, derived: edge.source === "derived", x1: from.x, y1: from.y, x2: to.x, y2: to.y};
|
||||
});
|
||||
return {nodes: ids.map((id) => placed.get(id)), links, width, height: Math.max(line, 1) * SPACING.row};
|
||||
}
|
||||
|
||||
function gateMarks(branch, entry) {
|
||||
const resolved = (entry && entry.resolved) || {};
|
||||
return ACTIONS.map((action) => {
|
||||
const found = resolved[action] || {};
|
||||
const level = LEVELS.includes(found.level) ? found.level : "none";
|
||||
const source = typeof found.source === "string" ? found.source : "";
|
||||
const own = source === branch;
|
||||
const from = own ? "own rule" : source ? `from ${source}` : "no rule";
|
||||
return {action, level, source, own, inherited: !own, icon: LEVEL_ICONS[level], title: `${action}: ${level} · ${from}`};
|
||||
});
|
||||
}
|
||||
|
||||
function nextRule(level) {
|
||||
return level === null || level === undefined ? LEVELS[0] : LEVELS.indexOf(level) + 1 < LEVELS.length ? LEVELS[LEVELS.indexOf(level) + 1] : null;
|
||||
}
|
||||
|
||||
function ruleArgv(repo, branch, action, level) {
|
||||
const head = ["--repo", repo, "--branch", branch, "--action", action];
|
||||
return level === null ? ["rule", "drop", ...head] : ["rule", "set", ...head, "--level", level];
|
||||
}
|
||||
|
||||
function parentArgv(repo, branch, parent, drop) {
|
||||
return ["parents", drop ? "drop" : "add", "--repo", repo, "--branch", branch, "--parent", parent];
|
||||
}
|
||||
|
||||
return {LEVELS, ACTIONS, LEVEL_ICONS, SPACING, generations, layout, gateMarks, nextRule, ruleArgv, parentArgv};
|
||||
})();
|
||||
if (typeof module === "object" && module.exports) {
|
||||
module.exports = Ancestry;
|
||||
}
|
||||
Reference in new issue
Block a user