@azure-id/orc 1.9.2 → 2.1.0

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 (228) hide show
  1. package/CHANGELOG.md +722 -0
  2. package/README-id.md +56 -42
  3. package/README.md +58 -41
  4. package/bin/build-agents.js +125 -29
  5. package/bin/clear-logs.js +620 -0
  6. package/bin/cli.js +1873 -180
  7. package/bin/fix.js +459 -0
  8. package/bin/gotcha-import.js +1081 -0
  9. package/bin/gotcha.js +1566 -0
  10. package/bin/graph-query.js +1 -1
  11. package/bin/graph.js +717 -717
  12. package/bin/habit.js +1538 -0
  13. package/bin/mockrun-catalog.js +284 -276
  14. package/bin/onboarding-content.js +2 -1
  15. package/bin/pricing.json +28 -21
  16. package/bin/run-undo.js +398 -0
  17. package/bin/trace-write.js +869 -0
  18. package/bin/verify-contracts.js +627 -100
  19. package/bin/verify-package.js +68 -10
  20. package/bin/webui/api.js +151 -6
  21. package/bin/webui/app.html +239 -232
  22. package/bin/webui/css/00-tokens.css +110 -92
  23. package/bin/webui/css/04-motion.css +87 -0
  24. package/bin/webui/css/06-responsive.css +203 -178
  25. package/bin/webui/css/panels/behaviour.css +205 -0
  26. package/bin/webui/css/panels/hookui.css +79 -0
  27. package/bin/webui/fixtures/behaviour.js +562 -0
  28. package/bin/webui/fixtures/extra.js +9 -9
  29. package/bin/webui/fixtures/flow.js +81 -81
  30. package/bin/webui/fixtures/hookui.js +506 -431
  31. package/bin/webui/fixtures/index.js +66 -3
  32. package/bin/webui/fixtures/knowledge.js +9 -3
  33. package/bin/webui/fixtures/maintenance.js +123 -42
  34. package/bin/webui/fixtures/rules.js +1007 -1002
  35. package/bin/webui/fixtures/settings.js +6 -7
  36. package/bin/webui/fixtures/stats.js +25 -6
  37. package/bin/webui/fixtures/wait.js +98 -97
  38. package/bin/webui/i18n/en/behaviour.json +156 -0
  39. package/bin/webui/i18n/en/hookui.json +290 -194
  40. package/bin/webui/i18n/en/maintenance.json +71 -52
  41. package/bin/webui/i18n/en/nav.json +25 -24
  42. package/bin/webui/i18n/en/settings.json +60 -60
  43. package/bin/webui/i18n/en/tour.json +37 -35
  44. package/bin/webui/i18n/id/behaviour.json +156 -0
  45. package/bin/webui/i18n/id/hookui.json +290 -194
  46. package/bin/webui/i18n/id/maintenance.json +71 -52
  47. package/bin/webui/i18n/id/nav.json +25 -24
  48. package/bin/webui/i18n/id/settings.json +60 -60
  49. package/bin/webui/i18n/id/tour.json +37 -35
  50. package/bin/webui/js/01-i18n.js +155 -154
  51. package/bin/webui/js/05-banners.js +181 -172
  52. package/bin/webui/js/06-edit.js +193 -183
  53. package/bin/webui/js/90-tour.js +498 -494
  54. package/bin/webui/js/91-shortcuts.js +126 -126
  55. package/bin/webui/js/99-boot.js +121 -118
  56. package/bin/webui/js/panels/behaviour.js +1087 -0
  57. package/bin/webui/js/panels/hookui.js +2196 -1691
  58. package/bin/webui/js/panels/maintenance.js +336 -235
  59. package/bin/webui/js/panels/settings.js +726 -719
  60. package/mock-run/INDEX.md +110 -107
  61. package/mock-run/extra-slots.md +1 -1
  62. package/mock-run/gotcha-import.md +118 -0
  63. package/mock-run/habits.md +129 -0
  64. package/mock-run/orc-aftermath.md +392 -392
  65. package/mock-run/orc-budget.md +29 -33
  66. package/mock-run/orc-cli.md +200 -200
  67. package/mock-run/orc-explain.md +86 -86
  68. package/mock-run/orc-extra.md +389 -392
  69. package/mock-run/orc-fix.md +87 -0
  70. package/mock-run/orc-pattern.md +112 -112
  71. package/mock-run/orc-quick.md +6 -1
  72. package/mock-run/orc.md +157 -157
  73. package/package.json +1 -1
  74. package/templates/agents/MODEL-MAPPING.md +37 -34
  75. package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
  76. package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
  77. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -5
  78. package/templates/agents/{orc-claude-writer-opus-4-8-high.md → orc-claude-writer-opus-5-low.md} +48 -53
  79. package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
  80. package/templates/agents/orc-executor-haiku-4-5.md +14 -6
  81. package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
  82. package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
  83. package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
  84. package/templates/agents/orc-executor-opus-5-high.md +14 -6
  85. package/templates/agents/orc-executor-opus-5-low.md +14 -6
  86. package/templates/agents/orc-executor-opus-5-med.md +14 -6
  87. package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
  88. package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
  89. package/templates/agents/orc-executor-sonnet-5-high.md +15 -7
  90. package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
  91. package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
  92. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
  93. package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
  94. package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
  95. package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
  96. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
  97. package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
  98. package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
  99. package/templates/agents/orc-planner-opus-5-med.md +1 -4
  100. package/templates/agents/orc-recon-opus-5-low.md +1 -8
  101. package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
  102. package/templates/agents/orc-retro-opus-5-med.md +6 -8
  103. package/templates/agents/orc-retro-sonnet-5-high.md +7 -8
  104. package/templates/agents/orc-reviewer-opus-5-low.md +96 -0
  105. package/templates/agents/orc-scout-opus-5-low.md +1 -6
  106. package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
  107. package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
  108. package/templates/agents/orc-test-author-opus-5-med.md +4 -5
  109. package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
  110. package/templates/agents/orc-verifier-opus-5-med.md +16 -8
  111. package/templates/agents/{orc-wiki-scanner-opus-4-8-high.md → orc-wiki-scanner-opus-5-low.md} +74 -79
  112. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
  113. package/templates/commands/orc-analyze.md +13 -21
  114. package/templates/commands/orc-fast.md +10 -15
  115. package/templates/commands/orc-fix.md +18 -0
  116. package/templates/commands/orc-poly.md +12 -21
  117. package/templates/commands/orc-pr-driver.md +11 -30
  118. package/templates/commands/orc-pr-setup.md +10 -31
  119. package/templates/commands/orc-route.md +11 -41
  120. package/templates/commands/orc-test.md +5 -60
  121. package/templates/commands/orc-wiki.md +64 -64
  122. package/templates/hooks/README.md +109 -7
  123. package/templates/hooks/orc-session-hook.js +339 -0
  124. package/templates/hooks/orc-statusline-render.js +317 -23
  125. package/templates/hooks/orc-statusline.js +165 -50
  126. package/templates/hooks/orc-subagent-line.js +23 -5
  127. package/templates/hooks/orc-weather-fetch.js +123 -0
  128. package/templates/skills/_shared/README.md +9 -0
  129. package/templates/skills/_shared/code-graph.md +47 -55
  130. package/templates/skills/_shared/config-precedence.md +3 -1
  131. package/templates/skills/_shared/extra-dispatch.md +78 -93
  132. package/templates/skills/_shared/gotchas.md +248 -177
  133. package/templates/skills/_shared/habits.md +101 -0
  134. package/templates/skills/_shared/lane-contract.md +84 -0
  135. package/templates/skills/_shared/opus5-only.md +14 -14
  136. package/templates/skills/_shared/phases/README.md +142 -83
  137. package/templates/skills/_shared/phases/analyst-gates.md +10 -21
  138. package/templates/skills/_shared/phases/execution.md +8 -14
  139. package/templates/skills/_shared/phases/house-rules.md +27 -32
  140. package/templates/skills/_shared/phases/intake.md +127 -133
  141. package/templates/skills/_shared/phases/mock-example.md +46 -56
  142. package/templates/skills/_shared/phases/plan-handoff.md +91 -97
  143. package/templates/skills/_shared/phases/planning.md +7 -17
  144. package/templates/skills/_shared/phases/preflight.md +19 -42
  145. package/templates/skills/_shared/phases/review.md +24 -28
  146. package/templates/skills/_shared/phases/rules.md +18 -42
  147. package/templates/skills/_shared/phases/scoring.md +55 -65
  148. package/templates/skills/_shared/phases/security-checklist.md +46 -50
  149. package/templates/skills/_shared/phases/security.md +45 -55
  150. package/templates/skills/_shared/phases/ship.md +6 -15
  151. package/templates/skills/_shared/phases/stop-resume.md +2 -5
  152. package/templates/skills/_shared/phases/summary.md +73 -48
  153. package/templates/skills/_shared/phases/testgen.md +41 -51
  154. package/templates/skills/_shared/phases/trace-verbs.md +447 -0
  155. package/templates/skills/_shared/phases/trace.md +66 -297
  156. package/templates/skills/_shared/phases/verify.md +62 -70
  157. package/templates/skills/_shared/phases/wave-grouping.md +128 -133
  158. package/templates/skills/_shared/phases/wiki-consult.md +10 -6
  159. package/templates/skills/_shared/read-ladder.md +2 -55
  160. package/templates/skills/_shared/return-validation.md +17 -70
  161. package/templates/skills/_shared/review-slice.md +106 -0
  162. package/templates/skills/_shared/smoke-gate.md +46 -28
  163. package/templates/skills/_shared/wait.md +1 -0
  164. package/templates/skills/context-combiner/SKILL.md +15 -44
  165. package/templates/skills/orc/README.md +2 -2
  166. package/templates/skills/orc/SKILL.md +35 -63
  167. package/templates/skills/orc/config.md +25 -27
  168. package/templates/skills/orc/examples/full-run-mock.md +2 -1
  169. package/templates/skills/orc/references/effort-and-mode.md +17 -17
  170. package/templates/skills/orc/references/pattern-gate.md +89 -89
  171. package/templates/skills/orc/references/phases/intake.md +41 -47
  172. package/templates/skills/orc/references/phases/integration.md +13 -19
  173. package/templates/skills/orc/references/preflight-report.md +11 -12
  174. package/templates/skills/orc/references/ultra-mode.md +9 -7
  175. package/templates/skills/orc/schemas/checkpoint.md +122 -122
  176. package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
  177. package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
  178. package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
  179. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +12 -53
  180. package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
  181. package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
  182. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
  183. package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
  184. package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
  185. package/templates/skills/orc-advisor/SKILL.md +56 -60
  186. package/templates/skills/orc-analyze/SKILL.md +33 -67
  187. package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
  188. package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
  189. package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
  190. package/templates/skills/orc-claude/SKILL.md +14 -14
  191. package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
  192. package/templates/skills/orc-diy/README.md +31 -0
  193. package/templates/skills/orc-diy/SKILL.md +27 -78
  194. package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
  195. package/templates/skills/orc-fast/SKILL.md +49 -75
  196. package/templates/skills/orc-fix/SKILL.md +110 -0
  197. package/templates/skills/orc-judge/SKILL.md +77 -82
  198. package/templates/skills/orc-mini/SKILL.md +64 -112
  199. package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
  200. package/templates/skills/orc-pattern/SKILL.md +27 -48
  201. package/templates/skills/orc-poly/SKILL.md +32 -61
  202. package/templates/skills/orc-pr-driver/SKILL.md +25 -51
  203. package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
  204. package/templates/skills/orc-pr-setup/SKILL.md +20 -45
  205. package/templates/skills/orc-quick/README.md +44 -3
  206. package/templates/skills/orc-quick/SKILL.md +77 -108
  207. package/templates/skills/orc-quick/references/dispatch-gate.md +18 -6
  208. package/templates/skills/orc-quick/references/gh-mode.md +50 -1
  209. package/templates/skills/orc-quick/references/look.md +3 -1
  210. package/templates/skills/orc-retro/SKILL.md +20 -19
  211. package/templates/skills/orc-retro/examples/retro-mock.md +17 -17
  212. package/templates/skills/orc-route/SKILL.md +25 -45
  213. package/templates/skills/orc-test/SKILL.md +16 -37
  214. package/templates/skills/orc-verify/SKILL.md +14 -32
  215. package/templates/skills/orc-wait/SKILL.md +156 -163
  216. package/templates/skills/orc-wiki/README.md +325 -325
  217. package/templates/skills/orc-wiki/SKILL.md +195 -195
  218. package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
  219. package/templates/skills/orc-wiki/references/extra.md +2 -2
  220. package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
  221. package/templates/skills/orc-wiki/references/phases/phase-0.md +2 -7
  222. package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
  223. package/templates/skills/orc-wiki/references/phases/phase-2.md +4 -9
  224. package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
  225. package/templates/skills/orc-wiki/references/phases/phase-3c.md +4 -8
  226. package/templates/agents/orc-claude-writer-opus-5-med.md +0 -55
  227. package/templates/agents/orc-reviewer-opus-5-med.md +0 -60
  228. package/templates/agents/orc-wiki-scanner-opus-5-med.md +0 -81
@@ -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
+ }