@azure-id/orc 1.9.0 → 1.9.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 (134) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README-id.md +57 -59
  3. package/README.md +53 -54
  4. package/bin/build-agents.js +109 -109
  5. package/bin/cli.js +262 -58
  6. package/bin/graph-audit.js +235 -0
  7. package/bin/graph-extract.js +308 -3
  8. package/bin/graph-gain.js +23 -3
  9. package/bin/graph-map.js +9 -1
  10. package/bin/graph-query.js +152 -10
  11. package/bin/graph-shard.js +6 -1
  12. package/bin/graph-signals.js +287 -4
  13. package/bin/graph.js +717 -658
  14. package/bin/onboarding-content.js +4 -4
  15. package/bin/pricing.json +207 -200
  16. package/bin/verify-contracts.js +117 -2
  17. package/bin/verify-package.js +11 -7
  18. package/bin/webui/css/04-motion.css +162 -152
  19. package/bin/webui/css/panels/knowledge.css +563 -116
  20. package/bin/webui/fixtures/extra.js +2036 -2036
  21. package/bin/webui/fixtures/hookui.js +5 -5
  22. package/bin/webui/fixtures/knowledge.js +21 -4
  23. package/bin/webui/fixtures/settings.js +305 -305
  24. package/bin/webui/i18n/en/knowledge.json +122 -4
  25. package/bin/webui/i18n/id/knowledge.json +122 -4
  26. package/bin/webui/js/panels/knowledge.js +573 -9
  27. package/bin/webui/js/panels/overview.js +13 -4
  28. package/mock-run/context-combiner.md +100 -100
  29. package/mock-run/orc-budget.md +534 -534
  30. package/mock-run/orc-challenge-council.md +262 -262
  31. package/mock-run/orc-ultra.md +103 -103
  32. package/package.json +1 -1
  33. package/templates/agents/MODEL-MAPPING.md +43 -43
  34. package/templates/agents/orc-advisor-opus-5-xhigh.md +56 -56
  35. package/templates/agents/orc-analyze-mini-opus-5-med.md +60 -60
  36. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +58 -58
  37. package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -75
  38. package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -110
  39. package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -114
  40. package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -112
  41. package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -132
  42. package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -109
  43. package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -109
  44. package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -90
  45. package/templates/agents/orc-claude-writer-opus-5-med.md +55 -55
  46. package/templates/agents/orc-context-combiner-opus-5-high.md +88 -88
  47. package/templates/agents/orc-doc-checker-opus-5-low.md +108 -108
  48. package/templates/agents/orc-doc-writer-opus-5-med.md +134 -134
  49. package/templates/agents/orc-executor-haiku-4-5.md +2 -1
  50. package/templates/agents/orc-executor-opus-4-7-high.md +2 -1
  51. package/templates/agents/orc-executor-opus-4-7-med.md +2 -1
  52. package/templates/agents/orc-executor-opus-4-8-high.md +2 -1
  53. package/templates/agents/orc-executor-opus-5-high.md +4 -3
  54. package/templates/agents/orc-executor-opus-5-low.md +4 -3
  55. package/templates/agents/orc-executor-opus-5-med.md +4 -3
  56. package/templates/agents/orc-executor-sonnet-4-6-high.md +2 -1
  57. package/templates/agents/orc-executor-sonnet-4-6-med.md +2 -1
  58. package/templates/agents/orc-executor-sonnet-5-high.md +2 -1
  59. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -1
  60. package/templates/agents/orc-judge-opus-5-xhigh.md +85 -85
  61. package/templates/agents/orc-learn-writer-opus-5-low.md +73 -73
  62. package/templates/agents/orc-pattern-codifier-opus-5-med.md +65 -65
  63. package/templates/agents/orc-planner-mini-opus-5-med.md +4 -4
  64. package/templates/agents/orc-planner-mini-sonnet-5-high.md +1 -1
  65. package/templates/agents/orc-planner-opus-5-med.md +160 -160
  66. package/templates/agents/orc-recon-opus-5-low.md +18 -8
  67. package/templates/agents/orc-recon-sonnet-4-6-med.md +15 -5
  68. package/templates/agents/orc-retro-opus-5-med.md +3 -3
  69. package/templates/agents/orc-reviewer-opus-5-med.md +60 -60
  70. package/templates/agents/orc-scout-opus-5-low.md +40 -40
  71. package/templates/agents/orc-system-analyst-opus-5-high.md +120 -120
  72. package/templates/agents/orc-test-author-opus-5-med.md +71 -71
  73. package/templates/agents/orc-test-designer-opus-5-high.md +158 -158
  74. package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -130
  75. package/templates/agents/orc-verifier-opus-5-med.md +69 -69
  76. package/templates/agents/orc-wiki-scanner-opus-5-med.md +81 -81
  77. package/templates/commands/orc-ultra.md +17 -17
  78. package/templates/commands/orc-verify.md +11 -11
  79. package/templates/hooks/README.md +13 -3
  80. package/templates/hooks/orc-effort-guard.js +178 -178
  81. package/templates/hooks/orc-graph-hook.js +53 -7
  82. package/templates/hooks/orc-statusline.js +6 -6
  83. package/templates/skills/_shared/code-graph.md +57 -18
  84. package/templates/skills/_shared/config-precedence.md +198 -198
  85. package/templates/skills/_shared/extra-dispatch.md +1346 -1346
  86. package/templates/skills/_shared/opus5-only.md +8 -8
  87. package/templates/skills/_shared/phases/analyst-gates.md +136 -136
  88. package/templates/skills/_shared/phases/execution.md +4 -4
  89. package/templates/skills/_shared/phases/planning.md +3 -3
  90. package/templates/skills/_shared/phases/preflight.md +1 -1
  91. package/templates/skills/_shared/phases/review.md +4 -2
  92. package/templates/skills/_shared/phases/ship.md +1 -1
  93. package/templates/skills/_shared/phases/trace.md +3 -2
  94. package/templates/skills/_shared/read-ladder.md +14 -1
  95. package/templates/skills/_shared/return-validation.md +1 -1
  96. package/templates/skills/context-combiner/SKILL.md +1 -1
  97. package/templates/skills/context-combiner/schemas/combined-report.md +78 -78
  98. package/templates/skills/orc/README.md +2 -2
  99. package/templates/skills/orc/SKILL.md +8 -8
  100. package/templates/skills/orc/config.md +9 -9
  101. package/templates/skills/orc/examples/full-run-mock.md +73 -73
  102. package/templates/skills/orc/references/effort-and-mode.md +222 -222
  103. package/templates/skills/orc/references/preflight-report.md +2 -2
  104. package/templates/skills/orc/references/ultra-mode.md +3 -3
  105. package/templates/skills/orc/subskills/orc-planner/SKILL.md +2 -2
  106. package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -121
  107. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +76 -76
  108. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +45 -45
  109. package/templates/skills/orc-analyze/SKILL.md +2 -2
  110. package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -42
  111. package/templates/skills/orc-analyze/schemas/report-audit.md +83 -83
  112. package/templates/skills/orc-analyze/schemas/report-prose.md +63 -63
  113. package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -78
  114. package/templates/skills/orc-challenge/README.md +142 -142
  115. package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -273
  116. package/templates/skills/orc-challenge/references/council.md +315 -315
  117. package/templates/skills/orc-challenge/references/intake.md +171 -171
  118. package/templates/skills/orc-claude/SKILL.md +14 -14
  119. package/templates/skills/orc-diy/README.md +143 -143
  120. package/templates/skills/orc-diy/references/flow-schema.md +1 -1
  121. package/templates/skills/orc-doc/README.md +229 -229
  122. package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -325
  123. package/templates/skills/orc-doc/references/chunking.md +527 -527
  124. package/templates/skills/orc-fast/SKILL.md +4 -4
  125. package/templates/skills/orc-learn/SKILL.md +14 -14
  126. package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -61
  127. package/templates/skills/orc-mini/SKILL.md +16 -20
  128. package/templates/skills/orc-mini/references/complexity.md +25 -11
  129. package/templates/skills/orc-quick/SKILL.md +12 -13
  130. package/templates/skills/orc-quick/references/gh-mode.md +1 -1
  131. package/templates/skills/orc-quick/references/look.md +5 -5
  132. package/templates/skills/orc-verify/SKILL.md +15 -15
  133. package/templates/skills/orc-verify/examples/verify-mock.md +33 -33
  134. package/templates/skills/orc-wiki/references/extra.md +1 -1
@@ -3,7 +3,8 @@
3
3
  What ORC knows about this repo: the wiki, its coverage, the code patterns,
4
4
  the repair memory, and what it knows from next door.
5
5
 
6
- FIVE TABS since v0.49.1, the Crosslink two-tab precedent. This was one
6
+ SIX TABS since v1.9.2 (the code graph got its own), FIVE since v0.49.1 —
7
+ the Crosslink two-tab precedent. This was one
7
8
  scrolling column of six cards, and the release that made the CLI stop
8
9
  discarding what it computes roughly tripled the content — which is not
9
10
  survivable as one scroll.
@@ -89,6 +90,7 @@ async function renderKnowledge(body) {
89
90
  patterns: () => knPatternsTab(d),
90
91
  memory: () => knMemoryTab(d, body),
91
92
  peers: () => knPeersTab(d),
93
+ graph: () => knGraphTab(d, body),
92
94
  };
93
95
  const select = (which) => {
94
96
  KN_TAB = which;
@@ -103,6 +105,8 @@ async function renderKnowledge(body) {
103
105
  ["patterns", t("knowledge.tab.patterns")],
104
106
  ["memory", t("knowledge.tab.memory")],
105
107
  ["peers", t("knowledge.tab.peers")],
108
+ // v1.9.2 — the code graph moved off the Wiki tab onto its own.
109
+ ["graph", t("knowledge.tab.graph")],
106
110
  ]) {
107
111
  const b = el("button", null, label);
108
112
  b.type = "button";
@@ -149,8 +153,10 @@ function knowledgeHeaderStrip(d) {
149
153
  the state word is never replaced by a friendlier synonym. An update is FREE
150
154
  (parser only, no model), so it is a button; an OFF graph shows the one config
151
155
  command that turns it on, because turning a feature on is the user's call. */
152
- function graphCard(g, body, gain) {
153
- const c = card(t("knowledge.graph.title"));
156
+ // `exact` (v1.9.2): on the Code graph tab the state ladder above already carries
157
+ // the chip and the update button, so this card is the exact numbers only.
158
+ function graphCard(g, body, gain, exact) {
159
+ const c = card(exact ? t("knowledge.cg.exact.title") : t("knowledge.graph.title"));
154
160
  if (!g || !g.state) {
155
161
  c.append(empty(t("knowledge.graph.unknown"), t("knowledge.graph.unknownHint")));
156
162
  return c;
@@ -160,6 +166,7 @@ function graphCard(g, body, gain) {
160
166
  c.append(el("pre", "cmd", "orc config set code_graph on"));
161
167
  return c;
162
168
  }
169
+ if (exact) c.append(el("div", "note", t("knowledge.cg.exact.lead")));
163
170
  const head = el("div", "row-actions");
164
171
  head.append(chip(String(g.state).toUpperCase(), g.state === "fresh" ? "ok" : g.state === "drifted" ? "warn" : "idle", g.state === "drifted"));
165
172
  const upd = el("button", "btn btn-sm", "orc graph update");
@@ -170,7 +177,7 @@ function graphCard(g, body, gain) {
170
177
  renderKnowledge(body);
171
178
  });
172
179
  head.append(upd);
173
- c.append(head);
180
+ if (!exact) c.append(head);
174
181
  if (g.state === "none") {
175
182
  c.append(el("div", "note", t("knowledge.graph.none")));
176
183
  return c;
@@ -187,14 +194,28 @@ function graphCard(g, body, gain) {
187
194
  [t("knowledge.graph.behind"), g.behind ? `+${b.added || 0} · ~${b.changed || 0} · -${b.deleted || 0}` : "—"],
188
195
  // `code_graph_notes` is a config VALUE — shown, never translated.
189
196
  [t("knowledge.graph.notes"), g.notes || "—"],
197
+ // A1 (v1.9.1) — how many named symbols the parser found per file, and how
198
+ // many files it read as EMPTY. A graph nobody can get an answer out of
199
+ // looks exactly like a healthy one until this row is on the card.
200
+ ...(g.density
201
+ ? [
202
+ [
203
+ t("knowledge.graph.density"),
204
+ `${g.density.symbols_per_file} symbols/file · ${Math.round(g.density.zero_share * 100)} % files empty${g.thin ? " · THIN" : ""}`,
205
+ ],
206
+ ]
207
+ : []),
190
208
  ])
191
209
  );
210
+ if (g.thin) c.append(el("div", "note warn", t("knowledge.graph.thin")));
192
211
  if (g.state === "drifted") c.append(el("div", "note warn", t("knowledge.graph.drifted")));
193
212
  // EW3/EW4 — two things keep the map fresh without a lane step. A panel that
194
213
  // shows a DRIFTED map without saying that is a panel that invites a needless
195
214
  // click.
196
- c.append(el("div", "note", t("knowledge.graph.selfheal")));
197
- c.append(el("div", "note", t("knowledge.graph.locator")));
215
+ if (!exact) {
216
+ c.append(el("div", "note", t("knowledge.graph.selfheal")));
217
+ c.append(el("div", "note", t("knowledge.graph.locator")));
218
+ }
198
219
  // K5 (v1.8.2 W4b) — the gain strip. It renders exactly the CLI's JSON and
199
220
  // invents nothing: the word "estimate" is part of the string, never a
200
221
  // tooltip, and `avoided` is a RANGE because one number for a counterfactual
@@ -221,11 +242,18 @@ function gainStrip(c, g, body) {
221
242
  );
222
243
  c.append(
223
244
  kvList([
224
- [t("knowledge.gain.paid"), `${kTokUi(g.paid.total)} (${kTokUi(g.paid.card)} · ${kTokUi(g.paid.source)} · ${kTokUi(g.paid.hints)})`],
245
+ // M3 (v1.9.1) — the ENVELOPE beside the cards. Until this release `paid`
246
+ // counted the card and called itself exact, and the card is between a
247
+ // quarter and an eighth of the answer a lane actually receives.
248
+ [t("knowledge.gain.paid"), `${kTokUi(g.paid.total)} (${kTokUi(g.paid.card)} · ${kTokUi(g.paid.source)} · ${kTokUi(g.paid.hints)} · ${kTokUi(g.paid.envelope || 0)})`],
225
249
  [t("knowledge.gain.avoided"), `~${kTokUi(g.avoided.low)} – ${kTokUi(g.avoided.high)}`],
226
250
  [t("knowledge.gain.net"), `~${kTokUi(g.net.low)} – ${kTokUi(g.net.high)} · ~${g.calls.low} – ${g.calls.high}`],
227
251
  [t("knowledge.gain.byCommand"), Object.entries(g.by_command).map(([k, v]) => `${k} ${v}`).join(" · ") || "—"],
228
- [t("knowledge.gain.hints"), `${g.hints.injected} · ${g.hints.read_notes} · ${g.hints.updates}`],
252
+ // R2 (v1.9.1) — the wide whole-file reads no hint named. A ledger written
253
+ // before 1.9.1 has no such count and reads as 0.
254
+ [t("knowledge.gain.hints"), `${g.hints.injected} · ${g.hints.read_notes} · ${g.hints.updates} · ${g.hints.wide_unhinted || 0}`],
255
+ // M2 — the reads this project never made. A fact, with no advice after it.
256
+ ...(g.never_called && g.never_called.length ? [[t("knowledge.gain.never"), g.never_called.join(" · ")]] : []),
229
257
  ])
230
258
  );
231
259
  const btn = el("button", "btn btn-sm", t("knowledge.gain.measure"));
@@ -264,7 +292,7 @@ function knWikiTab(d, body) {
264
292
  const out = frag();
265
293
  out.append(wikiPlanCard(d.plan, d.debt, body));
266
294
  out.append(wikiOneDocCard(d.refs));
267
- out.append(graphCard(d.graph, body, d.graphGain));
295
+ // v1.9.2 — the code graph card moved to its own tab (knGraphTab).
268
296
  if (d.usage && d.usage.rows) out.append(wikiUsageCard(d.usage, body));
269
297
 
270
298
  const w = d.wiki;
@@ -1128,3 +1156,539 @@ function wikiActions(body, w) {
1128
1156
  wrap.append(sync);
1129
1157
  return wrap;
1130
1158
  }
1159
+
1160
+ /* ── TAB 6 — CODE GRAPH (v1.9.2) ──────────────────────────────────────────────
1161
+ The graph used to be one card on the Wiki tab: a list of exact numbers and no
1162
+ picture of what any of them meant. It has its own tab now, in reading order:
1163
+ WHERE THIS GRAPH STANDS (the state ladder and the one action it needs), WHAT
1164
+ IT IS (the pipeline), WHAT A GRAPH LOOKS LIKE (an example, labelled as one),
1165
+ THE NUMBERS (drawn to scale), WHAT IT COST AND SAVED (two bars on one scale),
1166
+ every exact number, and what each word means.
1167
+
1168
+ THE PICTURES DERIVE NOTHING. Every number is `orc graph status --json` or
1169
+ `orc graph gain --json`, drawn to scale and never re-computed into a new
1170
+ claim. The example graph is static prose, and it says so on its face. */
1171
+ function knGraphTab(d, body) {
1172
+ const out = frag();
1173
+ const g = d.graph;
1174
+ const live = !!(g && (g.state === "fresh" || g.state === "drifted"));
1175
+ out.append(cgStateCard(g, body));
1176
+ out.append(cgFlowCard(g, live));
1177
+ out.append(cgAnatomyCard());
1178
+ if (live) out.append(cgNumbersCard(g));
1179
+ if (g && g.state && g.state !== "off") out.append(cgGainCard(d.graphGain));
1180
+ if (live) out.append(graphCard(g, body, d.graphGain, true));
1181
+ out.append(cgGlossaryCard());
1182
+ return out;
1183
+ }
1184
+
1185
+ // Reduced motion means the final number at once — never a count that moves.
1186
+ const cgStill = () => !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
1187
+
1188
+ // Counts a tile up to the CLI's number, then writes the CLI's own string, so the
1189
+ // resting value is exactly what `--json` sent.
1190
+ function cgCount(node, value, delay) {
1191
+ const final = value === undefined || value === null ? "—" : String(value);
1192
+ const n = Number(value);
1193
+ if (value === undefined || value === null || !Number.isFinite(n) || cgStill()) {
1194
+ node.textContent = final;
1195
+ return;
1196
+ }
1197
+ const dec = Number.isInteger(n) ? 0 : 1;
1198
+ node.textContent = (0).toFixed(dec);
1199
+ const dur = 1100;
1200
+ let start = null;
1201
+ const step = (ts) => {
1202
+ if (start === null) start = ts;
1203
+ const p = Math.min(1, (ts - start) / dur);
1204
+ const eased = 1 - Math.pow(1 - p, 3);
1205
+ node.textContent = p < 1 ? (n * eased).toFixed(dec) : final;
1206
+ if (p < 1) requestAnimationFrame(step);
1207
+ };
1208
+ setTimeout(() => requestAnimationFrame(step), delay || 0);
1209
+ }
1210
+
1211
+ // A bar segment that grows to its share. The width is set through CSSOM, never a
1212
+ // `style` attribute (the panel serves under `style-src 'self'`).
1213
+ function cgSeg(cls, share, label) {
1214
+ const s = el("div", "cg-seg " + cls);
1215
+ s.style.setProperty("--w", Math.max(0, Math.min(100, share * 100)).toFixed(2) + "%");
1216
+ if (label) s.title = label;
1217
+ return s;
1218
+ }
1219
+
1220
+ /* ── 1. the state ladder ─────────────────────────────────────────────────────
1221
+ Four states, in the order a graph lives through them. The word is the CLI's;
1222
+ the sentence under it says what it means, and the connector says what moves
1223
+ the graph to the next one. */
1224
+ function cgStateCard(g, body) {
1225
+ const c = card(t("knowledge.cg.state.title"));
1226
+ if (!g || !g.state) {
1227
+ c.append(empty(t("knowledge.graph.unknown"), t("knowledge.graph.unknownHint")));
1228
+ return c;
1229
+ }
1230
+ c.append(el("div", "note", t("knowledge.cg.state.lead")));
1231
+ const ladder = el("div", "cg-ladder");
1232
+ const steps = [
1233
+ ["OFF", t("knowledge.cg.state.off"), t("knowledge.cg.state.toNone")],
1234
+ ["NONE", t("knowledge.cg.state.none"), t("knowledge.cg.state.toFresh")],
1235
+ ["FRESH", t("knowledge.cg.state.fresh"), t("knowledge.cg.state.toDrifted")],
1236
+ ["DRIFTED", t("knowledge.cg.state.drifted"), null],
1237
+ ];
1238
+ // The CLI's state word, upper-cased the way the header strip shows it.
1239
+ const nowWord = String(g.state).toUpperCase();
1240
+ steps.forEach(([word, what, next], i) => {
1241
+ const here = nowWord === word;
1242
+ const s = el("div", "cg-step cg-step-" + word.toLowerCase() + (here ? " cg-step-now" : ""));
1243
+ s.style.setProperty("--i", String(i));
1244
+ const dot = el("span", "cg-step-dot");
1245
+ if (here) dot.append(el("span", "cg-beacon"));
1246
+ s.append(dot);
1247
+ s.append(el("span", "cg-step-word", word));
1248
+ s.append(el("span", "cg-step-what", what));
1249
+ if (here) s.append(el("span", "cg-step-here", t("knowledge.cg.state.here")));
1250
+ ladder.append(s);
1251
+ if (next) {
1252
+ const link = el("div", "cg-step-link");
1253
+ link.style.setProperty("--i", String(i));
1254
+ link.append(el("span", "cg-step-line"));
1255
+ link.append(el("span", "cg-step-next", next));
1256
+ ladder.append(link);
1257
+ }
1258
+ });
1259
+ // The loop back: a DRIFTED graph becomes FRESH again with one free update.
1260
+ c.append(ladder, el("div", "cg-loop", t("knowledge.cg.state.loop")));
1261
+
1262
+ // What to do now — the ONE action this state needs, or none.
1263
+ const now = el("div", "cg-now cg-now-" + g.state);
1264
+ if (g.state === "off") {
1265
+ now.append(el("div", null, t("knowledge.cg.now.off")));
1266
+ now.append(el("pre", "cmd", "orc config set code_graph on"));
1267
+ } else if (g.state === "none" || g.state === "drifted") {
1268
+ now.append(el("div", null, g.state === "none" ? t("knowledge.cg.now.none") : t("knowledge.cg.now.drifted")));
1269
+ const upd = el("button", "btn btn-sm", "orc graph update");
1270
+ upd.type = "button";
1271
+ upd.addEventListener("click", async () => {
1272
+ upd.disabled = true;
1273
+ const r = await post("/api/graph/update", {});
1274
+ toast(r.command, r.ok ? "ok" : "bad", r.output);
1275
+ renderKnowledge(body);
1276
+ });
1277
+ now.append(upd);
1278
+ } else {
1279
+ now.append(el("div", null, t("knowledge.cg.now.fresh")));
1280
+ }
1281
+ c.append(now);
1282
+ // The line a lane prints into the chat, verbatim. It is the CLI's sentence.
1283
+ if (g.line) {
1284
+ c.append(el("div", "note", t("knowledge.cg.state.lineNote")));
1285
+ c.append(el("pre", "cmd cg-line", g.line));
1286
+ }
1287
+ return c;
1288
+ }
1289
+
1290
+ /* ── 2. the pipeline ─────────────────────────────────────────────────────────
1291
+ Five stations, left to right, with dots running between them. The first
1292
+ three are free (a parser, no model); the card at the end is what a lane
1293
+ pays for in context tokens. */
1294
+ function cgFlowCard(g, live) {
1295
+ const c = card(t("knowledge.cg.flow.title"));
1296
+ c.append(el("div", "note", t("knowledge.cg.flow.lead")));
1297
+ const rail = el("div", "cg-flow");
1298
+ const stations = [
1299
+ ["</>", t("knowledge.cg.flow.code"), t("knowledge.cg.flow.codeWhat"), live ? t("knowledge.cg.flow.files", { n: g.files }) : null, "free"],
1300
+ ["f(x)", t("knowledge.cg.flow.parse"), t("knowledge.cg.flow.parseWhat"), t("knowledge.cg.flow.parseCost"), "free"],
1301
+ ["◉─◉", t("knowledge.cg.flow.store"), t("knowledge.cg.flow.storeWhat"), live ? t("knowledge.cg.flow.symbols", { n: g.symbols }) : null, "free"],
1302
+ ["?", t("knowledge.cg.flow.ask"), t("knowledge.cg.flow.askWhat"), "ctx · impact · map", null],
1303
+ ["▤", t("knowledge.cg.flow.card"), t("knowledge.cg.flow.cardWhat"), t("knowledge.cg.flow.cardCost"), "paid"],
1304
+ ];
1305
+ stations.forEach(([glyph, name, what, value, cost], i) => {
1306
+ if (i) {
1307
+ const link = el("div", "cg-link");
1308
+ link.style.setProperty("--i", String(i));
1309
+ for (let k = 0; k < 3; k++) {
1310
+ const dot = el("span", "cg-link-dot");
1311
+ dot.style.setProperty("--k", String(k));
1312
+ link.append(dot);
1313
+ }
1314
+ rail.append(link);
1315
+ }
1316
+ const s = el("div", "cg-station" + (cost ? " cg-cost-" + cost : ""));
1317
+ s.style.setProperty("--i", String(i));
1318
+ s.append(el("span", "cg-station-num", String(i + 1)));
1319
+ s.append(el("span", "cg-station-glyph", glyph));
1320
+ s.append(el("span", "cg-station-name", name));
1321
+ s.append(el("span", "cg-station-what", what));
1322
+ if (value) s.append(el("span", "cg-station-value", value));
1323
+ rail.append(s);
1324
+ });
1325
+ c.append(rail);
1326
+ // The second layer — the only one that can cost a model.
1327
+ const notes = el("div", "cg-notes-layer");
1328
+ notes.append(el("span", "cg-notes-tag", t("knowledge.cg.flow.notesTag")));
1329
+ notes.append(el("span", null, t("knowledge.cg.flow.notes", { mode: g && g.notes ? g.notes : "—" })));
1330
+ c.append(notes);
1331
+ c.append(el("div", "note", t("knowledge.graph.locator")));
1332
+ return c;
1333
+ }
1334
+
1335
+ /* ── 3. the anatomy — an EXAMPLE graph ───────────────────────────────────────
1336
+ Four small files from a made-up shop, drawn the way the graph sees them.
1337
+ It is never this repo's code, and the card says so in its title. Point at a
1338
+ dot and the caption says what `orc graph ctx` would answer for it. */
1339
+ const CG_SAMPLE = {
1340
+ files: [
1341
+ { name: "routes/orders.js", x: 16, y: 34, w: 212, h: 236 },
1342
+ { name: "middleware/auth.js", x: 262, y: 20, w: 184, h: 110 },
1343
+ { name: "services/orders.js", x: 262, y: 162, w: 184, h: 122 },
1344
+ { name: "db/index.js", x: 480, y: 96, w: 184, h: 116 },
1345
+ ],
1346
+ syms: [
1347
+ { id: "route", label: "GET /orders/:id", x: 122, y: 110 },
1348
+ { id: "list", label: "listOrders", x: 122, y: 206 },
1349
+ { id: "auth", label: "requireAuth", x: 354, y: 78 },
1350
+ { id: "find", label: "findOrder", x: 354, y: 226 },
1351
+ { id: "query", label: "query", x: 572, y: 158 },
1352
+ ],
1353
+ // kind: call (solid, a dot runs along it) · ref (dashed) · import (dotted, file to file)
1354
+ edges: [
1355
+ { from: "route", to: "auth", kind: "ref" },
1356
+ { from: "route", to: "find", kind: "call" },
1357
+ { from: "list", to: "find", kind: "call" },
1358
+ { from: "find", to: "query", kind: "call" },
1359
+ { from: "f1", to: "f2", kind: "import", d: "M228 60 L262 60" },
1360
+ { from: "f1", to: "f3", kind: "import", d: "M228 256 L262 256" },
1361
+ { from: "f3", to: "f4", kind: "import", d: "M446 196 L480 184" },
1362
+ ],
1363
+ };
1364
+
1365
+ // The captions, one per example dot. Keys are written out in full.
1366
+ const CG_CAPTION = {
1367
+ route: () => t("knowledge.cg.anatomy.route"),
1368
+ list: () => t("knowledge.cg.anatomy.list"),
1369
+ auth: () => t("knowledge.cg.anatomy.auth"),
1370
+ find: () => t("knowledge.cg.anatomy.find"),
1371
+ query: () => t("knowledge.cg.anatomy.query"),
1372
+ };
1373
+
1374
+ function cgSvg(tag, attrs) {
1375
+ const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
1376
+ for (const [k, v] of Object.entries(attrs || {})) node.setAttribute(k, String(v));
1377
+ return node;
1378
+ }
1379
+
1380
+ function cgAnatomyCard() {
1381
+ const c = card(t("knowledge.cg.anatomy.title"));
1382
+ c.append(el("div", "note", t("knowledge.cg.anatomy.lead")));
1383
+ const wrap = el("div", "cg-anatomy");
1384
+ const scroll = el("div", "scroll-x cg-anatomy-scroll");
1385
+ const svg = cgSvg("svg", { class: "cg-svg", viewBox: "0 0 680 300", role: "img", "aria-label": t("knowledge.cg.anatomy.title") });
1386
+ const defs = cgSvg("defs");
1387
+ for (const [id, cls] of [["cg-arrow-call", "cg-head-call"], ["cg-arrow-ref", "cg-head-ref"]]) {
1388
+ const m = cgSvg("marker", { id, viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse" });
1389
+ m.append(cgSvg("path", { d: "M0 0 L10 5 L0 10 z", class: cls }));
1390
+ defs.append(m);
1391
+ }
1392
+ svg.append(defs);
1393
+
1394
+ CG_SAMPLE.files.forEach((f, i) => {
1395
+ const gF = cgSvg("g", { class: "cg-file" });
1396
+ gF.style.setProperty("--d", i * 90 + "ms");
1397
+ gF.append(cgSvg("rect", { x: f.x, y: f.y, width: f.w, height: f.h, rx: 10 }));
1398
+ const label = cgSvg("text", { x: f.x + 12, y: f.y + 20, class: "cg-file-name" });
1399
+ label.textContent = f.name;
1400
+ gF.append(label);
1401
+ svg.append(gF);
1402
+ });
1403
+ const pos = {};
1404
+ for (const s of CG_SAMPLE.syms) pos[s.id] = s;
1405
+
1406
+ // Edges first, so the dots sit on top of them.
1407
+ const edgeEls = [];
1408
+ CG_SAMPLE.edges.forEach((e, i) => {
1409
+ let d = e.d;
1410
+ if (!d) {
1411
+ const a = pos[e.from];
1412
+ const b = pos[e.to];
1413
+ const mx = (a.x + b.x) / 2;
1414
+ d = `M${a.x + 10} ${a.y} C${mx} ${a.y} ${mx} ${b.y} ${b.x - 11} ${b.y}`;
1415
+ }
1416
+ const gE = cgSvg("g", { class: "cg-edge cg-edge-" + e.kind });
1417
+ gE.dataset.from = e.from;
1418
+ gE.dataset.to = e.to;
1419
+ gE.style.setProperty("--d", 380 + i * 110 + "ms");
1420
+ const attrs = { d, pathLength: 1, class: "cg-edge-line" };
1421
+ if (e.kind === "call") attrs["marker-end"] = "url(#cg-arrow-call)";
1422
+ if (e.kind === "ref") attrs["marker-end"] = "url(#cg-arrow-ref)";
1423
+ gE.append(cgSvg("path", attrs));
1424
+ if (e.kind === "call") {
1425
+ const p = cgSvg("path", { d, pathLength: 1, class: "cg-pulse" });
1426
+ p.style.setProperty("--d", 1200 + i * 450 + "ms");
1427
+ gE.append(p);
1428
+ }
1429
+ svg.append(gE);
1430
+ edgeEls.push(gE);
1431
+ });
1432
+
1433
+ const caption = el("div", "cg-caption");
1434
+ const setCaption = (id) => {
1435
+ caption.replaceChildren();
1436
+ if (!id) {
1437
+ caption.append(el("span", "cg-caption-hint", t("knowledge.cg.anatomy.hint")));
1438
+ return;
1439
+ }
1440
+ const s = pos[id];
1441
+ caption.append(el("span", "cg-caption-sym mono", s.label));
1442
+ caption.append(el("span", "cg-caption-text", CG_CAPTION[id]()));
1443
+ caption.append(el("span", "cg-caption-cmd mono", "orc graph ctx " + (s.label.includes(" ") ? `"${s.label}"` : s.label)));
1444
+ };
1445
+ const focus = (id) => {
1446
+ wrap.classList.toggle("cg-focus", !!id);
1447
+ for (const gE of edgeEls) gE.classList.toggle("cg-edge-hi", !!id && (gE.dataset.from === id || gE.dataset.to === id));
1448
+ for (const n of svg.querySelectorAll(".cg-sym")) n.classList.toggle("cg-sym-hi", n.dataset.id === id);
1449
+ setCaption(id);
1450
+ };
1451
+
1452
+ CG_SAMPLE.syms.forEach((s, i) => {
1453
+ const gS = cgSvg("g", { class: "cg-sym", tabindex: 0, role: "button", "aria-label": s.label });
1454
+ gS.dataset.id = s.id;
1455
+ gS.style.setProperty("--d", 200 + i * 80 + "ms");
1456
+ gS.append(cgSvg("circle", { cx: s.x, cy: s.y, r: 16, class: "cg-sym-halo" }));
1457
+ gS.append(cgSvg("circle", { cx: s.x, cy: s.y, r: 8, class: "cg-sym-dot" }));
1458
+ const tx = cgSvg("text", { x: s.x, y: s.y - 20, class: "cg-sym-name", "text-anchor": "middle" });
1459
+ tx.textContent = s.label;
1460
+ gS.append(tx);
1461
+ gS.addEventListener("mouseenter", () => focus(s.id));
1462
+ gS.addEventListener("focus", () => focus(s.id));
1463
+ gS.addEventListener("mouseleave", () => focus(null));
1464
+ gS.addEventListener("blur", () => focus(null));
1465
+ svg.append(gS);
1466
+ });
1467
+
1468
+ scroll.append(svg);
1469
+ wrap.append(scroll);
1470
+ setCaption(null);
1471
+ wrap.append(caption);
1472
+
1473
+ // The legend: every mark on the picture, and what it means.
1474
+ const legend = el("div", "cg-legend");
1475
+ for (const [cls, name, what] of [
1476
+ ["cg-lg-file", t("knowledge.cg.legend.file"), t("knowledge.cg.legend.fileWhat")],
1477
+ ["cg-lg-sym", t("knowledge.cg.legend.symbol"), t("knowledge.cg.legend.symbolWhat")],
1478
+ ["cg-lg-call", t("knowledge.cg.legend.call"), t("knowledge.cg.legend.callWhat")],
1479
+ ["cg-lg-ref", t("knowledge.cg.legend.ref"), t("knowledge.cg.legend.refWhat")],
1480
+ ["cg-lg-import", t("knowledge.cg.legend.import"), t("knowledge.cg.legend.importWhat")],
1481
+ ]) {
1482
+ const row = el("div", "cg-legend-row");
1483
+ row.append(el("span", "cg-lg " + cls));
1484
+ const txt = el("div", "cg-legend-text");
1485
+ txt.append(el("strong", null, name));
1486
+ txt.append(el("span", "note", what));
1487
+ row.append(txt);
1488
+ legend.append(row);
1489
+ }
1490
+ wrap.append(legend);
1491
+ c.append(wrap);
1492
+ return c;
1493
+ }
1494
+
1495
+ /* ── 4. the numbers, drawn ───────────────────────────────────────────────────
1496
+ Tiles that count up to the CLI's own number, the three change counts, and
1497
+ the density as a bar — per language when the CLI sent the split. */
1498
+ function cgNumbersCard(g) {
1499
+ const c = card(t("knowledge.cg.num.title"));
1500
+ const tiles = el("div", "cg-tiles");
1501
+ const tile = (label, value, what, i) => {
1502
+ const box = el("div", "cg-tile");
1503
+ box.style.setProperty("--i", String(i));
1504
+ const v = el("span", "cg-tile-value");
1505
+ cgCount(v, value, i * 120);
1506
+ box.append(v, el("span", "cg-tile-label", label), el("span", "cg-tile-what", what));
1507
+ tiles.append(box);
1508
+ };
1509
+ tile(t("knowledge.graph.files"), g.files, t("knowledge.cg.num.filesWhat"), 0);
1510
+ tile(t("knowledge.graph.symbols"), g.symbols, t("knowledge.cg.num.symbolsWhat"), 1);
1511
+ tile(t("knowledge.cg.num.perFile"), g.density ? g.density.symbols_per_file : undefined, t("knowledge.cg.num.perFileWhat"), 2);
1512
+ tile(t("knowledge.graph.generation"), g.generation, t("knowledge.cg.num.generationWhat"), 3);
1513
+ c.append(tiles);
1514
+
1515
+ // What changed since the map was last built. Three counts, the CLI's.
1516
+ if (g.behind) {
1517
+ const b = g.behind;
1518
+ const row = el("div", "cg-behind");
1519
+ row.append(el("span", "cg-behind-label", t("knowledge.cg.num.behind")));
1520
+ for (const [cls, sign, n, label] of [
1521
+ ["cg-b-add", "+", b.added || 0, t("knowledge.cg.num.added")],
1522
+ ["cg-b-chg", "~", b.changed || 0, t("knowledge.cg.num.changed")],
1523
+ ["cg-b-del", "-", b.deleted || 0, t("knowledge.cg.num.deleted")],
1524
+ ]) {
1525
+ const chipEl = el("span", "cg-bchip " + cls + (n ? "" : " cg-b-zero"));
1526
+ chipEl.append(el("strong", null, sign + n), el("span", null, label));
1527
+ row.append(chipEl);
1528
+ }
1529
+ c.append(row);
1530
+ }
1531
+
1532
+ // The density. `zero_share` is the CLI's; the bar only draws it.
1533
+ if (g.density) {
1534
+ const den = g.density;
1535
+ const pctEmpty = Math.round(den.zero_share * 100);
1536
+ c.append(el("div", "cg-sub", t("knowledge.cg.num.densityTitle")));
1537
+ const bar = el("div", "cg-bar");
1538
+ bar.append(cgSeg("cg-seg-ok", 1 - den.zero_share), cgSeg("cg-seg-empty", den.zero_share));
1539
+ c.append(bar);
1540
+ const key = el("div", "cg-bar-key");
1541
+ key.append(el("span", "cg-key cg-key-ok", t("knowledge.cg.num.hasSymbols", { pct: 100 - pctEmpty })));
1542
+ key.append(el("span", "cg-key cg-key-empty", t("knowledge.cg.num.empty", { pct: pctEmpty, n: den.zero_files })));
1543
+ c.append(key);
1544
+ if (den.by_lang && Object.keys(den.by_lang).length) {
1545
+ const langs = el("div", "cg-langs");
1546
+ Object.entries(den.by_lang).forEach(([lang, v], i) => {
1547
+ const r = el("div", "cg-lang");
1548
+ r.style.setProperty("--i", String(i));
1549
+ r.append(el("span", "cg-lang-name mono", lang));
1550
+ const lb = el("div", "cg-bar cg-bar-sm");
1551
+ const share = v.files ? v.zero / v.files : 0;
1552
+ lb.append(cgSeg("cg-seg-ok", 1 - share), cgSeg("cg-seg-empty", share));
1553
+ r.append(lb);
1554
+ r.append(el("span", "cg-lang-nums", t("knowledge.cg.num.lang", { files: v.files, symbols: v.symbols, zero: v.zero })));
1555
+ langs.append(r);
1556
+ });
1557
+ c.append(langs);
1558
+ }
1559
+ c.append(el("div", "note", t("knowledge.cg.num.densityNote")));
1560
+ if (g.thin) c.append(el("div", "note warn", t("knowledge.graph.thin")));
1561
+ }
1562
+ return c;
1563
+ }
1564
+
1565
+ /* ── 5. what it cost, and what it probably saved ─────────────────────────────
1566
+ Two bars on ONE scale. `paid` is exact and solid; `avoided` is an ESTIMATE
1567
+ and a RANGE, so it is drawn solid to its low end and striped to its high
1568
+ end — a single-number bar for a counterfactual is the claim this meter
1569
+ refuses to make. */
1570
+ const CG_READS = {
1571
+ ctx: () => t("knowledge.cg.read.ctx"),
1572
+ "for-slice": () => t("knowledge.cg.read.forSlice"),
1573
+ impact: () => t("knowledge.cg.read.impact"),
1574
+ map: () => t("knowledge.cg.read.map"),
1575
+ changes: () => t("knowledge.cg.read.changes"),
1576
+ cochange: () => t("knowledge.cg.read.cochange"),
1577
+ coverage: () => t("knowledge.cg.read.coverage"),
1578
+ path: () => t("knowledge.cg.read.path"),
1579
+ };
1580
+
1581
+ function cgGainCard(gain) {
1582
+ const c = card(t("knowledge.cg.gain.title"));
1583
+ if (!gain || gain.state !== "rows") {
1584
+ c.append(empty(t("knowledge.gain.none"), t("knowledge.cg.gain.noneHint")));
1585
+ return c;
1586
+ }
1587
+ c.append(el("div", "note", t("knowledge.cg.gain.lead")));
1588
+ const p = gain.paid || {};
1589
+ const a = gain.avoided || {};
1590
+ const scale = Math.max(1, p.total || 0, a.high || 0);
1591
+
1592
+ const rows = el("div", "cg-gain");
1593
+ const paidRow = el("div", "cg-gain-row");
1594
+ paidRow.append(el("span", "cg-gain-name", t("knowledge.cg.gain.paid")));
1595
+ const paidBar = el("div", "cg-bar cg-bar-lg");
1596
+ for (const [cls, n, label] of [
1597
+ ["cg-seg-card", p.card || 0, t("knowledge.cg.gain.card")],
1598
+ ["cg-seg-source", p.source || 0, t("knowledge.cg.gain.source")],
1599
+ ["cg-seg-hints", p.hints || 0, t("knowledge.cg.gain.hintsPart")],
1600
+ ["cg-seg-env", p.envelope || 0, t("knowledge.cg.gain.envelope")],
1601
+ ])
1602
+ paidBar.append(cgSeg(cls, n / scale, `${label} ${kTokUi(n)}`));
1603
+ paidRow.append(paidBar, el("span", "cg-gain-val", kTokUi(p.total || 0)));
1604
+ rows.append(paidRow);
1605
+ const avRow = el("div", "cg-gain-row");
1606
+ avRow.append(el("span", "cg-gain-name", t("knowledge.cg.gain.avoided")));
1607
+ const avBar = el("div", "cg-bar cg-bar-lg");
1608
+ avBar.append(cgSeg("cg-seg-low", (a.low || 0) / scale), cgSeg("cg-seg-range", ((a.high || 0) - (a.low || 0)) / scale));
1609
+ avRow.append(avBar, el("span", "cg-gain-val", `~${kTokUi(a.low || 0)} – ${kTokUi(a.high || 0)}`));
1610
+ rows.append(avRow);
1611
+ c.append(rows);
1612
+
1613
+ const key = el("div", "cg-bar-key");
1614
+ for (const [cls, label, n] of [
1615
+ ["cg-key-card", t("knowledge.cg.gain.card"), p.card],
1616
+ ["cg-key-source", t("knowledge.cg.gain.source"), p.source],
1617
+ ["cg-key-hints", t("knowledge.cg.gain.hintsPart"), p.hints],
1618
+ ["cg-key-env", t("knowledge.cg.gain.envelope"), p.envelope],
1619
+ ["cg-key-range", t("knowledge.cg.gain.range"), null],
1620
+ ])
1621
+ key.append(el("span", "cg-key " + cls, n === null ? label : `${label} ${kTokUi(n || 0)}`));
1622
+ c.append(key);
1623
+ c.append(el("div", "note", t("knowledge.gain.estimate")));
1624
+
1625
+ // Which reads were asked, in the CLI's own order. A read never asked keeps its
1626
+ // slot, muted — "never asked" and "not a read" are different facts.
1627
+ const names = gain.read_set && gain.read_set.length ? gain.read_set : Object.keys(gain.by_command || {});
1628
+ if (names.length) {
1629
+ c.append(el("div", "cg-sub", t("knowledge.cg.gain.readsTitle", { n: gain.calls_recorded, runs: gain.runs === undefined ? "—" : gain.runs })));
1630
+ const counts = names.map((k) => (gain.by_command && gain.by_command[k]) || 0);
1631
+ const most = Math.max(1, ...counts);
1632
+ const list = el("div", "cg-reads");
1633
+ names.forEach((k, i) => {
1634
+ const n = counts[i];
1635
+ const r = el("div", "cg-read" + (n ? "" : " cg-read-never"));
1636
+ r.style.setProperty("--i", String(i));
1637
+ r.append(el("span", "cg-read-cmd mono", k));
1638
+ const lb = el("div", "cg-bar cg-bar-sm");
1639
+ lb.append(cgSeg("cg-seg-read", n / most));
1640
+ r.append(lb);
1641
+ r.append(el("span", "cg-read-n", n ? String(n) : t("knowledge.cg.gain.never")));
1642
+ r.append(el("span", "cg-read-what", CG_READS[k] ? CG_READS[k]() : ""));
1643
+ list.append(r);
1644
+ });
1645
+ c.append(list);
1646
+ }
1647
+ return c;
1648
+ }
1649
+
1650
+ /* ── 6. what each word means ─────────────────────────────────────────────────
1651
+ Every word this tab and the exact card use, in four groups. */
1652
+ function cgGlossaryCard() {
1653
+ const c = card(t("knowledge.cg.gloss.title"));
1654
+ c.append(el("div", "note", t("knowledge.cg.gloss.lead")));
1655
+ const groups = [
1656
+ [t("knowledge.cg.gloss.gStructure"), [
1657
+ [t("knowledge.cg.legend.file"), t("knowledge.cg.gloss.file")],
1658
+ [t("knowledge.cg.legend.symbol"), t("knowledge.cg.gloss.symbol")],
1659
+ [t("knowledge.cg.legend.call"), t("knowledge.cg.gloss.call")],
1660
+ [t("knowledge.graph.generation"), t("knowledge.cg.gloss.generation")],
1661
+ ]],
1662
+ [t("knowledge.cg.gloss.gFresh"), [
1663
+ ["FRESH · DRIFTED", t("knowledge.cg.gloss.freshDrifted")],
1664
+ ["NONE · OFF", t("knowledge.cg.gloss.noneOff")],
1665
+ [t("knowledge.cg.num.behind"), t("knowledge.cg.gloss.behind")],
1666
+ [t("knowledge.cg.gloss.selfHealTerm"), t("knowledge.graph.selfheal")],
1667
+ ]],
1668
+ [t("knowledge.cg.gloss.gQuality"), [
1669
+ [t("knowledge.cg.num.perFile"), t("knowledge.cg.gloss.density")],
1670
+ ["THIN", t("knowledge.cg.gloss.thin")],
1671
+ [t("knowledge.graph.notes"), t("knowledge.cg.gloss.notes")],
1672
+ ]],
1673
+ [t("knowledge.cg.gloss.gCost"), [
1674
+ [t("knowledge.cg.gain.paid"), t("knowledge.cg.gloss.paid")],
1675
+ [t("knowledge.cg.gain.envelope"), t("knowledge.cg.gloss.envelope")],
1676
+ [t("knowledge.cg.gain.avoided"), t("knowledge.cg.gloss.avoided")],
1677
+ [t("knowledge.cg.gloss.netTerm"), t("knowledge.cg.gloss.net")],
1678
+ [t("knowledge.cg.gloss.hintsTerm"), t("knowledge.cg.gloss.hints")],
1679
+ [t("knowledge.gain.measure"), t("knowledge.cg.gloss.measured")],
1680
+ ]],
1681
+ ];
1682
+ const grid = el("div", "cg-gloss");
1683
+ groups.forEach(([title, terms], gi) => {
1684
+ const box = el("div", "cg-gloss-group");
1685
+ box.style.setProperty("--i", String(gi));
1686
+ box.append(el("div", "cg-gloss-head", title));
1687
+ const dl = el("dl", "cg-gloss-list");
1688
+ for (const [term, meaning] of terms) dl.append(el("dt", null, term), el("dd", null, meaning));
1689
+ box.append(dl);
1690
+ grid.append(box);
1691
+ });
1692
+ c.append(grid);
1693
+ return c;
1694
+ }