Requirement 001 phase 6, S0: - Speech: the security class shows as "Sécurité" and is spoken as in maritime radio, "say-cure-ee-tay" three times. A hex run of 7 or more characters with a letter and a digit (commit, session, message ids) is spelled in the NATO alphabet; every other digit is spoken on its own in ICAO pronunciation (42305: four two tree zero fife). The spelling runs inside clip() before the length cut, so a phrase still fits SPEECH_LIMIT; the displayed text keeps its digits and hashes. - English: every orchestrator label, tooltip, message, error, script output, document and test string is English (Regeln -> Firewall, Hierarchie -> Hierarchy, Steuerung -> Controls, Ausführen -> Run, ...); the page declares lang="en" and formats dates as en-GB. Task filter ids are English. German stays only in the skill trigger phrases that quote the operator, and as identifiers: the view keys (view=) and the leitstand id. - tests/test_english.py scans every tracked text file for umlauts and German words, with an allowlist of the quoted trigger phrases. BREAKING CHANGE: the Stream search tokens art: and seit: are now kind: and since:; an old art: or seit: token counts as a plain word. Evidence: make test 657 OK, ruff and markdownlint clean (/tmp/s0-make-test-1.log); e2e 17 OK, none skipped (/tmp/s0-e2e-1.log); MEDUSA 86 files, 24 findings on reworded strings accepted after an independent skeptic (21 path:line, 0 rejected, no behaviour change in signing, approving, rules, denying, offices, events), rescan 0 open. 🎯 Fix: 93 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 89 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
7.2 KiB
JavaScript
129 lines
7.2 KiB
JavaScript
const hier = {chart: null, sig: "", closed: new Set(), ids: 0};
|
||
const OFFICE_ICONS = {chair: "bi-award", line: "bi-person-badge", cluster: "bi-collection", work: "bi-briefcase", other: "bi-signpost-split"};
|
||
const OFFICE_GROUPS = [["cluster", "Organizers", "Organizer"], ["work", "Presidents", "President"], ["other", "Other offices", "Office"]];
|
||
|
||
function hierarchyScreen() {
|
||
hier.chart = h("section", {class: "card org-chart", "aria-labelledby": "org-title"});
|
||
orderDom.box = h("section", {class: "card agent-order", id: "agent-order", "aria-labelledby": "agent-order-title"});
|
||
hier.sig = "";
|
||
return h("div", {class: "hierarchy-body"}, hier.chart, orderDom.box);
|
||
}
|
||
|
||
function hierarchySig() {
|
||
const held = state.claims.map((claim) => [claim.office, claim.session, claim.agent, claim.work, claim.beat !== null && claim.beat >= state.now - ACTIVE_BEAT]);
|
||
return JSON.stringify([held, state.agent_order, state.people]);
|
||
}
|
||
|
||
function drawHierarchy(force) {
|
||
if (!hier.chart || !hier.chart.isConnected) {
|
||
hier.chart = null;
|
||
return;
|
||
}
|
||
const sig = hierarchySig();
|
||
if (!force && sig === hier.sig) {
|
||
return;
|
||
}
|
||
hier.sig = sig;
|
||
drawChart();
|
||
drawAgentOrder();
|
||
}
|
||
|
||
function focusKey(box) {
|
||
const at = document.activeElement;
|
||
return at && box.contains(at) ? at.dataset.focus || null : null;
|
||
}
|
||
|
||
function refocus(box, key) {
|
||
const found = key === null ? null : [...box.querySelectorAll("[data-focus]")].find((element) => element.dataset.focus === key);
|
||
if (found) {
|
||
found.focus();
|
||
}
|
||
}
|
||
|
||
function officeBtn(name, label, hint, run, focus) {
|
||
const button = iconBtn(name, label, "btn small ghost", run, hint);
|
||
button.dataset.focus = focus;
|
||
return button;
|
||
}
|
||
|
||
function holderOf(claim) {
|
||
return claim.agent || (claim.session === "-" ? "" : claim.session);
|
||
}
|
||
|
||
function officeRow(claim, label, title) {
|
||
const who = holderOf(claim);
|
||
const active = claim.beat !== null && claim.beat >= state.now - ACTIVE_BEAT;
|
||
return h("div", {class: "org-row", "data-office": claim.office},
|
||
h("span", {class: "org-office", title: `${title} ${claim.office}`}, icon(OFFICE_ICONS[claim.kind]), h("strong", {}, label)),
|
||
who ? h("button", {type: "button", class: "linkish org-holder", "data-focus": `agent:${claim.office}`, title: `Open ${who}`, on: {click: () => openAgent(who)}}, face(who, false), who) : h("span", {class: "muted"}, "–"),
|
||
active ? chip("bi-play-circle", "active", "ok") : chip("bi-pause-circle", "no heartbeat", "neutral"),
|
||
claim.work && claim.work !== "-" ? h("span", {class: "muted org-work", title: claim.work}, short(claim.work)) : null,
|
||
h("span", {class: "org-actions"},
|
||
officeBtn("bi-arrow-left-right", "Hand over", `Hand over: ${claim.office} to another session, with a passkey signature`, () => officeSheet("takeover", claim), `takeover:${claim.office}`),
|
||
officeBtn("bi-person-x", "Recall", `Recall: free ${claim.office}, with a passkey signature`, () => officeSheet("recall", claim), `recall:${claim.office}`)));
|
||
}
|
||
|
||
function vacantRow(office, label) {
|
||
return h("div", {class: "org-row vacant", "data-office": office},
|
||
h("span", {class: "org-office"}, icon(OFFICE_ICONS.chair), h("strong", {}, label)),
|
||
pill("vacant", "warn"),
|
||
h("span", {class: "org-actions"}, officeBtn("bi-person-plus", "Appoint", `Appoint: give ${office} to a session, with a passkey signature`, () => officeSheet("appoint", {office, session: "", work: ""}), `appoint:${office}`)));
|
||
}
|
||
|
||
function groupRow(glyph, label, count, note) {
|
||
return h("div", {class: "org-row group"}, h("span", {class: "org-office"}, icon(glyph), h("strong", {}, label)), counter(count), h("span", {class: "muted"}, note));
|
||
}
|
||
|
||
function orgNode(id, label, row, children) {
|
||
if (!children.length) {
|
||
return h("li", {}, h("div", {class: "org-line"}, h("span", {class: "org-gap", "aria-hidden": "true"}), row));
|
||
}
|
||
const open = !hier.closed.has(id);
|
||
hier.ids += 1;
|
||
const list = h("ul", {class: "org-tree", id: `org-${hier.ids}`, hidden: !open}, children);
|
||
const toggle = h("button", {type: "button", class: "btn small ghost icon-only org-toggle", "aria-expanded": String(open), "aria-controls": list.id, "data-focus": `toggle:${id}`, ...named(`${open ? "Collapse" : "Expand"}: ${label}`)}, icon(open ? "bi-chevron-down" : "bi-chevron-right"));
|
||
toggle.addEventListener("click", () => {
|
||
const now = list.hidden;
|
||
list.hidden = !now;
|
||
toggle.setAttribute("aria-expanded", String(now));
|
||
relabel(toggle, now ? "bi-chevron-down" : "bi-chevron-right", `${now ? "Collapse" : "Expand"}: ${label}`);
|
||
if (now) {
|
||
hier.closed.delete(id);
|
||
} else {
|
||
hier.closed.add(id);
|
||
}
|
||
});
|
||
return h("li", {"data-node": id}, h("div", {class: "org-line"}, toggle, row), list);
|
||
}
|
||
|
||
function chartTree() {
|
||
const claims = state.claims;
|
||
const dimensions = new Map();
|
||
for (const claim of claims.filter((entry) => entry.kind === "line")) {
|
||
dimensions.set(claim.dimension, [...(dimensions.get(claim.dimension) || []), claim]);
|
||
}
|
||
const lines = [...dimensions].sort(([a], [b]) => a.localeCompare(b)).map(([dimension, held]) => orgNode(`dimension:${dimension}`, dimension,
|
||
groupRow("bi-diagram-2", dimension, held.length, held.length === 1 ? "line" : "lines"),
|
||
held.map((claim) => orgNode(claim.office, claim.office, officeRow(claim, claim.line, "Representative"), []))));
|
||
const groups = OFFICE_GROUPS.map(([kind, label, title]) => [kind, label, title, claims.filter((entry) => entry.kind === kind)]).filter(([, , , held]) => held.length)
|
||
.map(([kind, label, title, held]) => orgNode(`group:${kind}`, label, groupRow(OFFICE_ICONS[kind], label, held.length, kind === "other" ? "" : `${kind}/…`),
|
||
held.map((claim) => orgNode(claim.office, claim.office, officeRow(claim, kind === "other" ? claim.office : claim.office.slice(kind.length + 1), title), []))));
|
||
const chair = claims.find((entry) => entry.kind === "chair");
|
||
const chairNode = orgNode("chair", "Chair", chair ? officeRow(chair, "Chair", "Office") : vacantRow("chair", "Chair"), [...lines, ...groups]);
|
||
const root = h("div", {class: "org-row org-root"}, h("span", {class: "org-office"}, icon("bi-person-circle"), h("strong", {}, HUMAN)), h("span", {class: "muted"}, "assigns every office with a passkey signature"));
|
||
return h("ul", {class: "org-tree root"}, orgNode("operator", HUMAN, root, [chairNode]));
|
||
}
|
||
|
||
function drawChart() {
|
||
const key = focusKey(hier.chart);
|
||
hier.ids = 0;
|
||
const count = state.claims.length;
|
||
hier.chart.replaceChildren(
|
||
h("div", {class: "inbox-head"}, h("h2", {class: "group-title", id: "org-title"}, "Hierarchy", counter(count)),
|
||
h("span", {class: "muted"}, "Formal offices from the council: Operator → Chair → dimensions → lines, plus organizers and presidents"),
|
||
h("span", {class: "spacer"}),
|
||
h("button", {type: "button", class: "btn small soft", "data-focus": "appoint:new", ...named("New office", "New office: give a free office to a session, with a passkey signature"), on: {click: () => officeSheet("appoint", null)}}, icon("bi-person-plus"), "New office")),
|
||
chartTree());
|
||
refocus(hier.chart, key);
|
||
}
|