Files
skills/orchestrator/templates/ancestry.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 f7a59d43a5 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>
2026-11-12 16:15:14 +00:00

98 lines
4.3 KiB
JavaScript

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;
}