@azure-id/orc 1.9.2 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README-id.md +21 -36
  3. package/README.md +25 -36
  4. package/bin/build-agents.js +117 -20
  5. package/bin/cli.js +725 -29
  6. package/bin/gotcha-import.js +1081 -0
  7. package/bin/gotcha.js +1286 -0
  8. package/bin/graph-query.js +1 -1
  9. package/bin/graph.js +717 -717
  10. package/bin/habit.js +1453 -0
  11. package/bin/mockrun-catalog.js +281 -276
  12. package/bin/run-undo.js +398 -0
  13. package/bin/trace-write.js +657 -0
  14. package/bin/verify-contracts.js +525 -79
  15. package/bin/verify-package.js +51 -4
  16. package/bin/webui/api.js +26 -0
  17. package/bin/webui/app.html +239 -232
  18. package/bin/webui/css/00-tokens.css +110 -92
  19. package/bin/webui/css/04-motion.css +87 -0
  20. package/bin/webui/css/06-responsive.css +203 -178
  21. package/bin/webui/css/panels/behaviour.css +205 -0
  22. package/bin/webui/fixtures/behaviour.js +532 -0
  23. package/bin/webui/fixtures/index.js +34 -0
  24. package/bin/webui/fixtures/knowledge.js +7 -1
  25. package/bin/webui/fixtures/stats.js +16 -0
  26. package/bin/webui/i18n/en/behaviour.json +143 -0
  27. package/bin/webui/i18n/en/nav.json +25 -24
  28. package/bin/webui/i18n/en/tour.json +37 -35
  29. package/bin/webui/i18n/id/behaviour.json +143 -0
  30. package/bin/webui/i18n/id/nav.json +25 -24
  31. package/bin/webui/i18n/id/tour.json +37 -35
  32. package/bin/webui/js/01-i18n.js +155 -154
  33. package/bin/webui/js/90-tour.js +498 -494
  34. package/bin/webui/js/91-shortcuts.js +126 -126
  35. package/bin/webui/js/99-boot.js +121 -118
  36. package/bin/webui/js/panels/behaviour.js +1022 -0
  37. package/mock-run/INDEX.md +109 -107
  38. package/mock-run/gotcha-import.md +118 -0
  39. package/mock-run/habits.md +129 -0
  40. package/mock-run/orc-quick.md +6 -1
  41. package/package.json +1 -1
  42. package/templates/agents/MODEL-MAPPING.md +7 -7
  43. package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
  44. package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
  45. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +1 -4
  46. package/templates/agents/orc-claude-writer-opus-4-8-high.md +48 -53
  47. package/templates/agents/orc-claude-writer-opus-5-med.md +1 -8
  48. package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
  49. package/templates/agents/orc-executor-haiku-4-5.md +14 -6
  50. package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
  51. package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
  52. package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
  53. package/templates/agents/orc-executor-opus-5-high.md +14 -6
  54. package/templates/agents/orc-executor-opus-5-low.md +14 -6
  55. package/templates/agents/orc-executor-opus-5-med.md +14 -6
  56. package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
  57. package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
  58. package/templates/agents/orc-executor-sonnet-5-high.md +14 -6
  59. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
  60. package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
  61. package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
  62. package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
  63. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
  64. package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
  65. package/templates/agents/orc-planner-mini-sonnet-5-high.md +1 -2
  66. package/templates/agents/orc-planner-opus-5-med.md +1 -4
  67. package/templates/agents/orc-recon-opus-5-low.md +1 -8
  68. package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
  69. package/templates/agents/orc-retro-opus-5-med.md +6 -8
  70. package/templates/agents/orc-retro-sonnet-5-high.md +6 -7
  71. package/templates/agents/orc-reviewer-opus-5-med.md +52 -16
  72. package/templates/agents/orc-scout-opus-5-low.md +1 -6
  73. package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
  74. package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
  75. package/templates/agents/orc-test-author-opus-5-med.md +4 -5
  76. package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
  77. package/templates/agents/orc-verifier-opus-5-med.md +16 -8
  78. package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +74 -79
  79. package/templates/agents/orc-wiki-scanner-opus-5-med.md +1 -8
  80. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
  81. package/templates/commands/orc-analyze.md +13 -21
  82. package/templates/commands/orc-fast.md +10 -15
  83. package/templates/commands/orc-poly.md +12 -21
  84. package/templates/commands/orc-pr-driver.md +11 -30
  85. package/templates/commands/orc-pr-setup.md +10 -31
  86. package/templates/commands/orc-route.md +11 -41
  87. package/templates/commands/orc-test.md +5 -60
  88. package/templates/hooks/README.md +34 -0
  89. package/templates/hooks/orc-session-hook.js +264 -0
  90. package/templates/hooks/orc-statusline.js +3 -1
  91. package/templates/skills/_shared/README.md +9 -0
  92. package/templates/skills/_shared/code-graph.md +47 -55
  93. package/templates/skills/_shared/config-precedence.md +3 -1
  94. package/templates/skills/_shared/extra-dispatch.md +73 -88
  95. package/templates/skills/_shared/gotchas.md +228 -177
  96. package/templates/skills/_shared/habits.md +101 -0
  97. package/templates/skills/_shared/lane-contract.md +84 -0
  98. package/templates/skills/_shared/phases/README.md +142 -83
  99. package/templates/skills/_shared/phases/analyst-gates.md +10 -21
  100. package/templates/skills/_shared/phases/execution.md +8 -14
  101. package/templates/skills/_shared/phases/house-rules.md +27 -32
  102. package/templates/skills/_shared/phases/intake.md +127 -133
  103. package/templates/skills/_shared/phases/mock-example.md +46 -56
  104. package/templates/skills/_shared/phases/plan-handoff.md +91 -97
  105. package/templates/skills/_shared/phases/planning.md +7 -17
  106. package/templates/skills/_shared/phases/preflight.md +19 -42
  107. package/templates/skills/_shared/phases/review.md +23 -27
  108. package/templates/skills/_shared/phases/rules.md +18 -42
  109. package/templates/skills/_shared/phases/scoring.md +55 -65
  110. package/templates/skills/_shared/phases/security-checklist.md +46 -50
  111. package/templates/skills/_shared/phases/security.md +45 -55
  112. package/templates/skills/_shared/phases/ship.md +6 -15
  113. package/templates/skills/_shared/phases/stop-resume.md +2 -5
  114. package/templates/skills/_shared/phases/summary.md +73 -48
  115. package/templates/skills/_shared/phases/testgen.md +41 -51
  116. package/templates/skills/_shared/phases/trace-verbs.md +433 -0
  117. package/templates/skills/_shared/phases/trace.md +136 -367
  118. package/templates/skills/_shared/phases/verify.md +62 -70
  119. package/templates/skills/_shared/phases/wave-grouping.md +128 -133
  120. package/templates/skills/_shared/phases/wiki-consult.md +10 -6
  121. package/templates/skills/_shared/read-ladder.md +2 -55
  122. package/templates/skills/_shared/return-validation.md +17 -70
  123. package/templates/skills/_shared/review-slice.md +79 -0
  124. package/templates/skills/_shared/smoke-gate.md +46 -28
  125. package/templates/skills/context-combiner/SKILL.md +15 -44
  126. package/templates/skills/orc/SKILL.md +34 -62
  127. package/templates/skills/orc/references/pattern-gate.md +89 -89
  128. package/templates/skills/orc/references/phases/intake.md +41 -47
  129. package/templates/skills/orc/references/phases/integration.md +13 -19
  130. package/templates/skills/orc/references/preflight-report.md +8 -9
  131. package/templates/skills/orc/references/ultra-mode.md +8 -6
  132. package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
  133. package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
  134. package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
  135. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +11 -52
  136. package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
  137. package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
  138. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
  139. package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
  140. package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
  141. package/templates/skills/orc-advisor/SKILL.md +56 -60
  142. package/templates/skills/orc-analyze/SKILL.md +30 -66
  143. package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
  144. package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
  145. package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
  146. package/templates/skills/orc-diy/README.md +31 -0
  147. package/templates/skills/orc-diy/SKILL.md +23 -74
  148. package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
  149. package/templates/skills/orc-fast/SKILL.md +44 -72
  150. package/templates/skills/orc-judge/SKILL.md +77 -82
  151. package/templates/skills/orc-mini/SKILL.md +61 -109
  152. package/templates/skills/orc-pattern/SKILL.md +27 -48
  153. package/templates/skills/orc-poly/SKILL.md +32 -61
  154. package/templates/skills/orc-pr-driver/SKILL.md +25 -51
  155. package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
  156. package/templates/skills/orc-pr-setup/SKILL.md +20 -45
  157. package/templates/skills/orc-quick/README.md +43 -2
  158. package/templates/skills/orc-quick/SKILL.md +76 -107
  159. package/templates/skills/orc-quick/references/dispatch-gate.md +16 -5
  160. package/templates/skills/orc-quick/references/gh-mode.md +48 -1
  161. package/templates/skills/orc-quick/references/look.md +3 -1
  162. package/templates/skills/orc-retro/SKILL.md +19 -18
  163. package/templates/skills/orc-retro/examples/retro-mock.md +1 -1
  164. package/templates/skills/orc-route/SKILL.md +25 -45
  165. package/templates/skills/orc-test/SKILL.md +16 -37
  166. package/templates/skills/orc-verify/SKILL.md +14 -32
  167. package/templates/skills/orc-wait/SKILL.md +156 -163
  168. package/templates/skills/orc-wiki/references/phases/phase-0.md +1 -6
  169. package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
  170. package/templates/skills/orc-wiki/references/phases/phase-2.md +1 -6
  171. package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
  172. package/templates/skills/orc-wiki/references/phases/phase-3c.md +1 -6
@@ -1,154 +1,155 @@
1
- "use strict";
2
- /* 01-i18n.js — orc ui client
3
- LANGS, loadLang, setLang, t, applyStaticText, cycleLang.
4
-
5
- Keys are written out IN FULL, never assembled from a fragment. The v0.48.1
6
- file split is by key PREFIX only; `docs.ship.confirm` is still spelled
7
- `docs.ship.confirm` at every call site.
8
-
9
- Loaded by app.html in the order its numeric prefix names. Classic
10
- script, no import/export: an ES module import carries no query string,
11
- and every static request here needs the per-launch session token. */
12
-
13
- /* ------------------------------------------------------------------- i18n -- */
14
- /*
15
- THE SCOPE RULE, and it is deliberately narrow: only the panel's OWN prose is
16
- translated. Everything that arrives from `bin/cli.js --json` stays exactly as
17
- the CLI wrote it — config keys and their descriptions, values, agent names,
18
- model ids, file paths, commands, `orc doctor` messages, command output. Those
19
- are identifiers, not sentences: a translated config key is a key that does not
20
- exist, and a translated command is a command you cannot type. So `t()` is
21
- never applied to a value read out of a payload, and neither string table
22
- contains a single ORC key name.
23
-
24
- The tables are plain JSON served as static assets. Adding a language is a
25
- FOLDER in i18n/ plus one row in LANGS — no build step, no library, no
26
- dependency, and no server change (serve.js walks i18n/ at boot).
27
- */
28
- const LANGS = [
29
- { code: "en", label: "English" },
30
- { code: "id", label: "Bahasa Indonesia" },
31
- ];
32
- const LANG_KEY = "orc-ui-lang";
33
-
34
- // v0.48.1 — one file per namespace instead of one 53 KB table, so changing the
35
- // Docs panel's wording means opening i18n/en/docs.json rather than paging
36
- // through every string in the panel.
37
- //
38
- // A plain array, and deliberately NOT an index file to fetch: an index would be
39
- // one more round trip and one more thing to forget to update. A namespace owns
40
- // one or more key PREFIXES (`stats` owns `stats.*` and `cost.*`), which the
41
- // test suite asserts — a key whose prefix belongs to no namespace would load
42
- // nowhere and render as a raw dotted string.
43
- const NAMESPACES = [
44
- "common",
45
- "nav",
46
- "banner",
47
- "overview",
48
- "settings",
49
- "lanes",
50
- "rules",
51
- "runs",
52
- "knowledge",
53
- "stats",
54
- "flow",
55
- "crosslink",
56
- "learn",
57
- "mockrun",
58
- "maintenance",
59
- "pact",
60
- "boundary",
61
- "wait",
62
- "hookui",
63
- "handoff",
64
- "challenge",
65
- "docs",
66
- "test",
67
- "extra",
68
- "experiment",
69
- "tour",
70
- ];
71
-
72
- let lang = "en";
73
- // English is loaded first and kept as the FALLBACK table: a key the other
74
- // language has not translated yet renders in English rather than as a raw
75
- // dotted key. A half-finished translation degrades to mixed prose, never to
76
- // debug output on somebody's screen.
77
- let DICT = {};
78
- let DICT_EN = {};
79
-
80
- // The namespaces are fetched in parallel and merged into ONE flat table, so
81
- // `t()` is unchanged and no call site knows the split happened.
82
- //
83
- // A namespace that fails to load falls back to ENGLISH for its keys rather than
84
- // rejecting the whole language: a network hiccup on one file should cost that
85
- // file's prose, not the entire panel. English itself has no fallback below it,
86
- // so a failure there rejects and boot() lands on raw keys — ugly, never blank.
87
- function loadLang(code) {
88
- return Promise.all(
89
- NAMESPACES.map((ns) =>
90
- fetch(`/i18n/${code}/${ns}.json`, { headers: { "x-orc-token": TOKEN }, cache: "no-store" })
91
- .then((r) => {
92
- if (!r.ok) throw new Error(`i18n ${code}/${ns} ${r.status}`);
93
- return r.json();
94
- })
95
- .catch((e) => {
96
- if (code === "en") throw e;
97
- return null;
98
- })
99
- )
100
- ).then((parts) => Object.assign({}, ...parts.filter(Boolean)));
101
- }
102
-
103
- // A missing or broken table must never blank the UI. Every failure path here
104
- // lands on English, so the worst case is untranslated text — never empty text.
105
- async function setLang(code, opts) {
106
- const rerender = !opts || opts.rerender !== false;
107
- const want = LANGS.some((l) => l.code === code) ? code : "en";
108
- try {
109
- DICT = want === "en" ? DICT_EN : await loadLang(want);
110
- lang = want;
111
- } catch (_) {
112
- DICT = DICT_EN;
113
- lang = "en";
114
- }
115
- try {
116
- localStorage.setItem(LANG_KEY, lang);
117
- } catch (_) {}
118
- document.documentElement.setAttribute("lang", lang);
119
- applyStaticText();
120
- if (rerender) route();
121
- }
122
-
123
- function t(key, vars) {
124
- let s = DICT[key];
125
- if (s === undefined) s = DICT_EN[key];
126
- if (s === undefined) s = key;
127
- if (vars) for (const k of Object.keys(vars)) s = s.split("{" + k + "}").join(String(vars[k]));
128
- return s;
129
- }
130
-
131
- // English has one plural form and Indonesian has none, so count-aware lookup is
132
- // a second key rather than a plural-rules engine: `n === 1` picks the singular.
133
- const tn = (n, key, vars) => t(n === 1 ? key : key + "Plural", Object.assign({ n }, vars || {}));
134
-
135
- // The parts of the shell that are markup rather than a render function. Called
136
- // on every language change; panels re-render themselves through `route()`.
137
- function applyStaticText() {
138
- for (const node of document.querySelectorAll("[data-i18n]")) node.textContent = t(node.dataset.i18n);
139
- const tt = $("#theme-toggle");
140
- if (tt) tt.textContent = document.documentElement.getAttribute("data-theme") === "light" ? t("rail.dark") : t("rail.light");
141
- const hint = $("#shortcut-hint");
142
- if (hint) hint.title = t("rail.shortcuts");
143
- const label = $("#lang-label");
144
- if (label) label.textContent = (LANGS.find((l) => l.code === lang) || LANGS[0]).label;
145
- const btn = $("#lang-toggle");
146
- if (btn) btn.title = t("lang.title");
147
- }
148
-
149
- // Two languages, so the control is a TOGGLE rather than a menu: one click, no
150
- // dropdown to open. A third language would make this a <select>; two do not.
151
- function cycleLang() {
152
- const i = LANGS.findIndex((l) => l.code === lang);
153
- setLang(LANGS[(i + 1) % LANGS.length].code);
154
- }
1
+ "use strict";
2
+ /* 01-i18n.js — orc ui client
3
+ LANGS, loadLang, setLang, t, applyStaticText, cycleLang.
4
+
5
+ Keys are written out IN FULL, never assembled from a fragment. The v0.48.1
6
+ file split is by key PREFIX only; `docs.ship.confirm` is still spelled
7
+ `docs.ship.confirm` at every call site.
8
+
9
+ Loaded by app.html in the order its numeric prefix names. Classic
10
+ script, no import/export: an ES module import carries no query string,
11
+ and every static request here needs the per-launch session token. */
12
+
13
+ /* ------------------------------------------------------------------- i18n -- */
14
+ /*
15
+ THE SCOPE RULE, and it is deliberately narrow: only the panel's OWN prose is
16
+ translated. Everything that arrives from `bin/cli.js --json` stays exactly as
17
+ the CLI wrote it — config keys and their descriptions, values, agent names,
18
+ model ids, file paths, commands, `orc doctor` messages, command output. Those
19
+ are identifiers, not sentences: a translated config key is a key that does not
20
+ exist, and a translated command is a command you cannot type. So `t()` is
21
+ never applied to a value read out of a payload, and neither string table
22
+ contains a single ORC key name.
23
+
24
+ The tables are plain JSON served as static assets. Adding a language is a
25
+ FOLDER in i18n/ plus one row in LANGS — no build step, no library, no
26
+ dependency, and no server change (serve.js walks i18n/ at boot).
27
+ */
28
+ const LANGS = [
29
+ { code: "en", label: "English" },
30
+ { code: "id", label: "Bahasa Indonesia" },
31
+ ];
32
+ const LANG_KEY = "orc-ui-lang";
33
+
34
+ // v0.48.1 — one file per namespace instead of one 53 KB table, so changing the
35
+ // Docs panel's wording means opening i18n/en/docs.json rather than paging
36
+ // through every string in the panel.
37
+ //
38
+ // A plain array, and deliberately NOT an index file to fetch: an index would be
39
+ // one more round trip and one more thing to forget to update. A namespace owns
40
+ // one or more key PREFIXES (`stats` owns `stats.*` and `cost.*`), which the
41
+ // test suite asserts — a key whose prefix belongs to no namespace would load
42
+ // nowhere and render as a raw dotted string.
43
+ const NAMESPACES = [
44
+ "common",
45
+ "nav",
46
+ "banner",
47
+ "overview",
48
+ "settings",
49
+ "lanes",
50
+ "rules",
51
+ "runs",
52
+ "knowledge",
53
+ "stats",
54
+ "behaviour",
55
+ "flow",
56
+ "crosslink",
57
+ "learn",
58
+ "mockrun",
59
+ "maintenance",
60
+ "pact",
61
+ "boundary",
62
+ "wait",
63
+ "hookui",
64
+ "handoff",
65
+ "challenge",
66
+ "docs",
67
+ "test",
68
+ "extra",
69
+ "experiment",
70
+ "tour",
71
+ ];
72
+
73
+ let lang = "en";
74
+ // English is loaded first and kept as the FALLBACK table: a key the other
75
+ // language has not translated yet renders in English rather than as a raw
76
+ // dotted key. A half-finished translation degrades to mixed prose, never to
77
+ // debug output on somebody's screen.
78
+ let DICT = {};
79
+ let DICT_EN = {};
80
+
81
+ // The namespaces are fetched in parallel and merged into ONE flat table, so
82
+ // `t()` is unchanged and no call site knows the split happened.
83
+ //
84
+ // A namespace that fails to load falls back to ENGLISH for its keys rather than
85
+ // rejecting the whole language: a network hiccup on one file should cost that
86
+ // file's prose, not the entire panel. English itself has no fallback below it,
87
+ // so a failure there rejects and boot() lands on raw keys — ugly, never blank.
88
+ function loadLang(code) {
89
+ return Promise.all(
90
+ NAMESPACES.map((ns) =>
91
+ fetch(`/i18n/${code}/${ns}.json`, { headers: { "x-orc-token": TOKEN }, cache: "no-store" })
92
+ .then((r) => {
93
+ if (!r.ok) throw new Error(`i18n ${code}/${ns} ${r.status}`);
94
+ return r.json();
95
+ })
96
+ .catch((e) => {
97
+ if (code === "en") throw e;
98
+ return null;
99
+ })
100
+ )
101
+ ).then((parts) => Object.assign({}, ...parts.filter(Boolean)));
102
+ }
103
+
104
+ // A missing or broken table must never blank the UI. Every failure path here
105
+ // lands on English, so the worst case is untranslated text — never empty text.
106
+ async function setLang(code, opts) {
107
+ const rerender = !opts || opts.rerender !== false;
108
+ const want = LANGS.some((l) => l.code === code) ? code : "en";
109
+ try {
110
+ DICT = want === "en" ? DICT_EN : await loadLang(want);
111
+ lang = want;
112
+ } catch (_) {
113
+ DICT = DICT_EN;
114
+ lang = "en";
115
+ }
116
+ try {
117
+ localStorage.setItem(LANG_KEY, lang);
118
+ } catch (_) {}
119
+ document.documentElement.setAttribute("lang", lang);
120
+ applyStaticText();
121
+ if (rerender) route();
122
+ }
123
+
124
+ function t(key, vars) {
125
+ let s = DICT[key];
126
+ if (s === undefined) s = DICT_EN[key];
127
+ if (s === undefined) s = key;
128
+ if (vars) for (const k of Object.keys(vars)) s = s.split("{" + k + "}").join(String(vars[k]));
129
+ return s;
130
+ }
131
+
132
+ // English has one plural form and Indonesian has none, so count-aware lookup is
133
+ // a second key rather than a plural-rules engine: `n === 1` picks the singular.
134
+ const tn = (n, key, vars) => t(n === 1 ? key : key + "Plural", Object.assign({ n }, vars || {}));
135
+
136
+ // The parts of the shell that are markup rather than a render function. Called
137
+ // on every language change; panels re-render themselves through `route()`.
138
+ function applyStaticText() {
139
+ for (const node of document.querySelectorAll("[data-i18n]")) node.textContent = t(node.dataset.i18n);
140
+ const tt = $("#theme-toggle");
141
+ if (tt) tt.textContent = document.documentElement.getAttribute("data-theme") === "light" ? t("rail.dark") : t("rail.light");
142
+ const hint = $("#shortcut-hint");
143
+ if (hint) hint.title = t("rail.shortcuts");
144
+ const label = $("#lang-label");
145
+ if (label) label.textContent = (LANGS.find((l) => l.code === lang) || LANGS[0]).label;
146
+ const btn = $("#lang-toggle");
147
+ if (btn) btn.title = t("lang.title");
148
+ }
149
+
150
+ // Two languages, so the control is a TOGGLE rather than a menu: one click, no
151
+ // dropdown to open. A third language would make this a <select>; two do not.
152
+ function cycleLang() {
153
+ const i = LANGS.findIndex((l) => l.code === lang);
154
+ setLang(LANGS[(i + 1) % LANGS.length].code);
155
+ }