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>
69 lines
3.2 KiB
JavaScript
69 lines
3.2 KiB
JavaScript
const WORK_CHIPS = {dirty: ["bi-pencil-square", "not committed", "warn"], unpushed: ["bi-cloud-upload", "unpushed", "info"], unmerged: ["bi-signpost-split", "not merged", "neutral"], task: ["bi-card-checklist", "task open", "violet"]};
|
||
const FLOW_SIDES = {pull: "pulls before the gate", push: "merges into when done"};
|
||
|
||
function workChips(reasons) {
|
||
if (!reasons || !reasons.length) {
|
||
return null;
|
||
}
|
||
return h("span", {class: "states work", role: "group", "aria-label": "Open work"}, reasons.map((reason) => {
|
||
const [name, label, tone] = Object.hasOwn(WORK_CHIPS, reason) ? WORK_CHIPS[reason] : ["bi-question-circle", reason, "neutral"];
|
||
return h("span", {class: "state", "data-tone": tone, title: `open work: ${label}`}, icon(name), label);
|
||
}));
|
||
}
|
||
|
||
function flowLine(flow, branch) {
|
||
if (!flow) {
|
||
return null;
|
||
}
|
||
if (flow.source === "invalid") {
|
||
return h("div", {class: "flow invalid", "data-flow": "invalid"}, icon("bi-exclamation-triangle"), "Merge flow invalid: pkgmgr cannot read the flow file");
|
||
}
|
||
const refs = (side) => (flow[side].length ? flow[side].map((ref) => h("code", {class: "flow-ref", title: `${FLOW_SIDES[side]}: ${ref}`}, ref)) : h("span", {class: "muted"}, "–"));
|
||
const arrow = () => h("span", {class: "flow-arrow", ...named("flows to")}, icon("bi-arrow-right"));
|
||
return h("div", {class: "flow", "data-flow": flow.source, role: "group", "aria-label": `Merge flow: ${flow.pull.join(", ") || "no source"} to ${branch || "–"} to ${flow.push.join(", ") || "no target"}`},
|
||
h("span", {class: "flow-label"}, icon("bi-sign-merge-right"), "Flow"),
|
||
h("span", {class: "flow-side"}, refs("pull")), arrow(),
|
||
h("code", {class: "flow-ref here", title: "this branch"}, branch || "–"), arrow(),
|
||
h("span", {class: "flow-side"}, refs("push")));
|
||
}
|
||
|
||
function flowDetails(flow, branch) {
|
||
if (!flow) {
|
||
return h("p", {class: "muted"}, "No merge flow set.");
|
||
}
|
||
if (flow.source === "invalid") {
|
||
return flowLine(flow, branch);
|
||
}
|
||
return [h("h3", {}, "Merge flow"), flowLine(flow, branch), meta([["Pulls before the gate", flow.pull.join(", ")], ["Merges into when done", flow.push.join(", ")], ["Source", flow.source]])];
|
||
}
|
||
|
||
function flowTarget(names, ref) {
|
||
if (names.has(ref)) {
|
||
return ref;
|
||
}
|
||
const bare = ref.includes("/") ? ref.slice(ref.indexOf("/") + 1) : "";
|
||
return names.has(bare) ? bare : null;
|
||
}
|
||
|
||
function flowEdges(repo) {
|
||
const names = new Set(repo.branches.map((entry) => entry.name));
|
||
const edges = [];
|
||
for (const branch of repo.branches) {
|
||
for (const flow of branch.worktrees.map((wt) => wt.flow).filter((found) => found && found.source !== "invalid")) {
|
||
for (const ref of flow.pull) {
|
||
const from = flowTarget(names, ref);
|
||
if (from && from !== branch.name) {
|
||
edges.push({from, into: branch.name, side: "pull", ref});
|
||
}
|
||
}
|
||
for (const ref of flow.push) {
|
||
const into = flowTarget(names, ref);
|
||
if (into && into !== branch.name) {
|
||
edges.push({from: branch.name, into, side: "push", ref});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return edges;
|
||
}
|