@azure-id/orc 1.7.1 → 1.8.1

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 (58) hide show
  1. package/CHANGELOG.md +3649 -3381
  2. package/README-id.md +923 -844
  3. package/README.md +836 -788
  4. package/bin/build-agents.js +43 -27
  5. package/bin/cli.js +701 -3
  6. package/bin/graph-extract.js +927 -0
  7. package/bin/graph-notes.js +188 -0
  8. package/bin/graph-query.js +808 -0
  9. package/bin/graph-resolve.js +178 -0
  10. package/bin/graph-signals.js +277 -0
  11. package/bin/graph.js +605 -0
  12. package/bin/verify-contracts.js +4669 -4553
  13. package/bin/verify-package.js +626 -616
  14. package/bin/webui/api.js +1419 -1414
  15. package/bin/webui/fixtures/index.js +579 -576
  16. package/bin/webui/fixtures/knowledge.js +316 -291
  17. package/bin/webui/i18n/en/knowledge.json +167 -151
  18. package/bin/webui/i18n/en/overview.json +101 -100
  19. package/bin/webui/i18n/id/knowledge.json +167 -151
  20. package/bin/webui/i18n/id/overview.json +101 -100
  21. package/bin/webui/js/panels/knowledge.js +1065 -1006
  22. package/bin/webui/js/panels/overview.js +492 -488
  23. package/package.json +39 -39
  24. package/templates/agents/MODEL-MAPPING.md +163 -158
  25. package/templates/agents/orc-executor-haiku-4-5.md +133 -121
  26. package/templates/agents/orc-executor-opus-4-7-high.md +134 -122
  27. package/templates/agents/orc-executor-opus-4-7-med.md +134 -122
  28. package/templates/agents/orc-executor-opus-4-8-high.md +134 -122
  29. package/templates/agents/orc-executor-opus-5-high.md +134 -122
  30. package/templates/agents/orc-executor-opus-5-low.md +134 -122
  31. package/templates/agents/orc-executor-opus-5-med.md +134 -122
  32. package/templates/agents/orc-executor-sonnet-4-6-high.md +134 -122
  33. package/templates/agents/orc-executor-sonnet-4-6-med.md +134 -122
  34. package/templates/agents/orc-executor-sonnet-5-high.md +134 -122
  35. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +86 -0
  36. package/templates/hooks/README.md +444 -396
  37. package/templates/hooks/orc-graph-hook.js +336 -0
  38. package/templates/hooks/orc-statusline-render.js +922 -921
  39. package/templates/hooks/orc-statusline.js +1596 -1545
  40. package/templates/skills/_shared/README.md +4 -0
  41. package/templates/skills/_shared/code-graph.md +220 -0
  42. package/templates/skills/_shared/opus5-only.md +4 -0
  43. package/templates/skills/_shared/phases/execution.md +166 -147
  44. package/templates/skills/_shared/phases/planning.md +142 -135
  45. package/templates/skills/_shared/phases/preflight.md +132 -118
  46. package/templates/skills/_shared/phases/review.md +63 -53
  47. package/templates/skills/_shared/phases/ship.md +96 -88
  48. package/templates/skills/_shared/phases/trace.md +6 -0
  49. package/templates/skills/_shared/phases/wiki-consult.md +194 -189
  50. package/templates/skills/_shared/read-ladder.md +124 -102
  51. package/templates/skills/_shared/return-validation.md +259 -250
  52. package/templates/skills/orc/SKILL.md +255 -254
  53. package/templates/skills/orc-diy/references/flow-schema.md +101 -100
  54. package/templates/skills/orc-fast/SKILL.md +236 -229
  55. package/templates/skills/orc-mini/SKILL.md +267 -259
  56. package/templates/skills/orc-quick/SKILL.md +378 -361
  57. package/templates/skills/orc-quick/references/dispatch-gate.md +6 -0
  58. package/templates/skills/orc-wiki/references/staleness.md +294 -288
@@ -1,1006 +1,1065 @@
1
- "use strict";
2
- /* panels/knowledge.js — orc ui client
3
- What ORC knows about this repo: the wiki, its coverage, the code patterns,
4
- the repair memory, and what it knows from next door.
5
-
6
- FIVE TABS since v0.49.1, the Crosslink two-tab precedent. This was one
7
- scrolling column of six cards, and the release that made the CLI stop
8
- discarding what it computes roughly tripled the content — which is not
9
- survivable as one scroll.
10
-
11
- THE PANEL DERIVES NOTHING. It never computes a tier, a distance, a coverage
12
- percentage, an order, an estimate or a price: `computeWikiFreshness` is the
13
- one engine, `orc wiki plan` decides the order, and a value the CLI could not
14
- compute renders as an em dash, never as a guess.
15
-
16
- A FREE ACTION GETS A BUTTON; A PAID ACTION GETS A COPY-ABLE COMMAND — and
17
- that line is visible on the panel rather than implied.
18
-
19
- Loaded by app.html in the order its numeric prefix names. Classic
20
- script, no import/export: an ES module import carries no query string,
21
- and every static request here needs the per-launch session token. */
22
-
23
- /* =============================================================== KNOWLEDGE == */
24
-
25
- PANELS.knowledge = function (host) {
26
- head(host, t("knowledge.title"), t("knowledge.sub"));
27
- const body = el("div", "stack");
28
- host.append(body);
29
- renderKnowledge(body);
30
- };
31
-
32
- // Which tab was open, so a write that re-renders the panel does not throw the
33
- // user back to the first one.
34
- let KN_TAB = "wiki";
35
-
36
- async function renderKnowledge(body) {
37
- body.replaceChildren(skeleton(6));
38
- const [wikiRes, impactRes, patRes, gotRes, planRes, debtRes, usageRes, docsRes, covRes, refsRes] = await Promise.all([
39
- read("/api/wiki").catch((e) => ({ data: null, error: e })),
40
- read("/api/wiki/impact").catch((e) => ({ data: null, error: e })),
41
- read("/api/patterns").catch((e) => ({ data: null, error: e })),
42
- read("/api/gotchas").catch((e) => ({ data: null, error: e })),
43
- read("/api/wiki/plan").catch((e) => ({ data: null, error: e })),
44
- read("/api/wiki/debt").catch((e) => ({ data: null, error: e })),
45
- read("/api/wiki/usage").catch((e) => ({ data: null, error: e })),
46
- read("/api/wiki/docs").catch((e) => ({ data: null, error: e })),
47
- read("/api/wiki/coverage").catch((e) => ({ data: null, error: e })),
48
- // v1.7.0 — the derived-reference sweep. `--check`, always: opening a panel
49
- // must never write to a repo.
50
- read("/api/wiki/refs").catch((e) => ({ data: null, error: e })),
51
- ]);
52
- const d = {
53
- wiki: wikiRes.data || {},
54
- impact: impactRes.data,
55
- patterns: patRes.data || {},
56
- gotchas: gotRes.data || {},
57
- plan: planRes.data,
58
- debt: debtRes.data,
59
- usage: usageRes.data,
60
- docs: docsRes.data,
61
- coverage: covRes.data,
62
- refs: refsRes.data,
63
- // A read that FAILED is not the same as a read that came back empty, and
64
- // rendering them identically is what turned a truncated 30 KB payload into
65
- // "this repo has no wiki and no git" (v0.49.4). The server already puts the
66
- // CLI's own words in the 500 body; the panel just has to stop discarding
67
- // them.
68
- errors: { coverage: covRes.error || null, docs: docsRes.error || null },
69
- };
70
-
71
- const out = frag();
72
- // THE HEADER STRIP, above the tabs and on every one of them: the numbers that
73
- // answer "what does ORC know about this repo?" in one line. Every value is
74
- // CLI-computed, and one it could not compute renders as an em dash.
75
- out.append(knowledgeHeaderStrip(d));
76
-
77
- const tabs = el("div", "tabs");
78
- const pane = el("div", "tab-pane stack");
79
- const views = {
80
- wiki: () => knWikiTab(d, body),
81
- coverage: () => knCoverageTab(d),
82
- patterns: () => knPatternsTab(d),
83
- memory: () => knMemoryTab(d, body),
84
- peers: () => knPeersTab(d),
85
- };
86
- const select = (which) => {
87
- KN_TAB = which;
88
- for (const b of tabs.children) b.setAttribute("aria-selected", String(b.dataset.tab === which));
89
- pane.replaceChildren(views[which]());
90
- };
91
- // Keys are written out in full, never assembled from the tab id — a key built
92
- // from a fragment is invisible to every check that looks for one.
93
- for (const [which, label] of [
94
- ["wiki", t("knowledge.tab.wiki")],
95
- ["coverage", t("knowledge.tab.coverage")],
96
- ["patterns", t("knowledge.tab.patterns")],
97
- ["memory", t("knowledge.tab.memory")],
98
- ["peers", t("knowledge.tab.peers")],
99
- ]) {
100
- const b = el("button", null, label);
101
- b.type = "button";
102
- b.dataset.tab = which;
103
- b.addEventListener("click", () => select(which));
104
- tabs.append(b);
105
- }
106
- out.append(tabs, pane);
107
- body.replaceChildren(out);
108
- select(views[KN_TAB] ? KN_TAB : "wiki");
109
- }
110
-
111
- /* THE HEADER STRIP. Six numbers, all of them the CLI's. A `—` means the CLI
112
- could not compute it, and that is an ANSWER — never a zero, never a guess. */
113
- function knowledgeHeaderStrip(d) {
114
- const w = d.wiki || {};
115
- const strip = el("div", "kn-strip");
116
- const item = (label, value, kind) => {
117
- const box = el("div", "kn-strip-item" + (kind ? " kn-" + kind : ""));
118
- box.append(el("span", "kn-strip-value", value));
119
- box.append(el("span", "kn-strip-label", label));
120
- strip.append(box);
121
- };
122
- // The tier word is the CLI's own. Never a friendlier synonym.
123
- item(t("knowledge.strip.wiki"), w.tier || (w.state ? String(w.state).toUpperCase() : "—"));
124
- item(t("knowledge.strip.docs"), w.docs === undefined ? "—" : String(w.docs));
125
- item(
126
- t("knowledge.strip.covered"),
127
- d.coverage && d.coverage.ok ? d.coverage.coverage_pct + "%" : "—"
128
- );
129
- item(t("knowledge.strip.blind"), w.blind === undefined ? "—" : String(w.blind));
130
- item(
131
- t("knowledge.strip.pending"),
132
- d.debt && d.debt.ok && d.debt.pending !== undefined ? String(d.debt.pending) : "—"
133
- );
134
- item(t("knowledge.strip.patterns"), String((d.patterns.patterns || []).length));
135
- item(t("knowledge.strip.gotchas"), d.gotchas.count === undefined ? "—" : String(d.gotchas.count));
136
- return strip;
137
- }
138
-
139
- /* ── TAB 1 — WIKI ────────────────────────────────────────────────────────── */
140
- function knWikiTab(d, body) {
141
- const out = frag();
142
- out.append(wikiPlanCard(d.plan, d.debt, body));
143
- out.append(wikiOneDocCard(d.refs));
144
- if (d.usage && d.usage.rows) out.append(wikiUsageCard(d.usage, body));
145
-
146
- const w = d.wiki;
147
- const wc = card(t("knowledge.wiki"), wikiActions(body, w));
148
- if (!w.state || w.state === "none") {
149
- wc.append(empty(t("knowledge.wiki.none"), t("knowledge.wiki.noneHint")));
150
- } else if (w.state !== "registered") {
151
- wc.append(el("div", "note", t("knowledge.wiki.unregistered", { state: String(w.state).toUpperCase() })));
152
- wc.append(el("div", "note", t("knowledge.wiki.syncHint")));
153
- } else {
154
- const tierChip = chip(w.tier || t("overview.tile.wikiUnknown"), w.tier === "FRESH" ? "ok" : w.tier === "AGING" ? "warn" : "bad", w.tier === "STALE");
155
- const headRow = el("div", "row-actions");
156
- headRow.append(tierChip);
157
- // The per-doc split as a small stacked bar. The counts are the CLI's.
158
- if (w.counts) {
159
- const total = Math.max(1, w.counts.FRESH + w.counts.AGING + w.counts.STALE + (w.counts.unknown || 0));
160
- const bar = el("div", "kn-tierbar");
161
- for (const [tier, cls] of [["FRESH", "ok"], ["AGING", "warn"], ["STALE", "bad"], ["unknown", "idle"]]) {
162
- const n = w.counts[tier] || 0;
163
- if (!n) continue;
164
- const seg = el("div", "kn-tierbar-seg kn-tier-" + cls);
165
- seg.style.setProperty("--w", ((n / total) * 100).toFixed(2) + "%");
166
- seg.title = `${tier} ${n}`;
167
- bar.append(seg);
168
- }
169
- headRow.append(bar);
170
- }
171
- wc.append(headRow);
172
- const rows = [
173
- [t("knowledge.field.docs"), String(w.docs)],
174
- [t("knowledge.field.lastScan"), w.last_scan],
175
- ];
176
- // THE DOC PINNING THE TIER, BY NAME. A hash is not a thing anybody can go
177
- // and refresh — and until v0.49.1 the hash is all `--json` carried.
178
- if (w.worst)
179
- rows.push([t("knowledge.field.worst"), `${w.worst.file} (${w.worst.distance}c)`]);
180
- rows.push(
181
- [
182
- t("knowledge.field.distance"),
183
- w.distance === null ? t("knowledge.field.unmeasurable") : t("knowledge.field.distanceValue", { n: w.distance }),
184
- ],
185
- [t("knowledge.field.anchor"), w.anchor ? String(w.anchor).slice(0, 8) : ""],
186
- // `wiki_fresh_max` / `wiki_aging_max` are config keys — the numbers are
187
- // shown, the key names are not paraphrased.
188
- [t("knowledge.field.edges"), w.edges ? `fresh < ${w.edges.freshMax}c · aging <= ${w.edges.agingMax}c` : ""],
189
- [t("knowledge.field.tags"), w.crosslink_tags === undefined ? "" : String(w.crosslink_tags)],
190
- [t("knowledge.field.blind"), w.blind ? String(w.blind) : "0"]
191
- );
192
- wc.append(kvList(rows));
193
- // The reason text is the CLI's own sentence about a real doc — verbatim.
194
- for (const r of w.reasons || []) wc.append(el("div", "note", t("knowledge.wiki.why", { reason: r })));
195
- wc.append(el("div", "note", t("knowledge.wiki.freshNote")));
196
- // The orientation doc is read FIRST by every consumer, and the panel used
197
- // not to mention it exists.
198
- if (w.orientation) {
199
- if (w.orientation.present)
200
- wc.append(el("div", "note ok", t("knowledge.orientation.present", { file: w.orientation.file })));
201
- else {
202
- wc.append(el("div", "note warn", t("knowledge.orientation.missing")));
203
- wc.append(el("pre", "cmd", w.orientation.regenerate));
204
- }
205
- }
206
- }
207
- out.append(wc);
208
-
209
- // THE DOC TABLE — the headline addition. A row EXPANDS IN PLACE (one at a
210
- // time, detail fetched on first open); there is no detail box below the
211
- // table, which is the Runs-row rule.
212
- out.append(d.errors && d.errors.docs ? failBox(d.errors.docs) : wikiDocsCard(d.docs));
213
-
214
- // impact, unchanged, moved onto this tab
215
- const imp = d.impact;
216
- if (imp && imp.ok) {
217
- const c = card(t("knowledge.impact.title"));
218
- const rec = el("div", "row-actions");
219
- rec.append(chip(imp.recommendation, imp.recommendation === "CLEAN" ? "ok" : imp.recommendation === "DELTA" ? "info" : "warn"));
220
- c.append(rec);
221
- for (const r of imp.reasons || []) c.append(el("div", "note", r));
222
- c.append(
223
- el(
224
- "div",
225
- "note",
226
- t("knowledge.impact.counts", {
227
- registered: imp.registered,
228
- touched: imp.touched,
229
- structural: imp.structural,
230
- pct: imp.affected_pct,
231
- threshold: imp.threshold,
232
- })
233
- )
234
- );
235
- const scroll = el("div", "scroll-x");
236
- const table = el("table");
237
- const thead = el("thead");
238
- const hr = el("tr");
239
- for (const h of [t("knowledge.impact.col.doc"), t("knowledge.impact.col.state"), t("knowledge.impact.col.detail")])
240
- hr.append(el("th", null, h));
241
- thead.append(hr);
242
- const tb = el("tbody");
243
- for (const doc of imp.docs) {
244
- const tr = el("tr");
245
- tr.append(el("td", "mono", doc.file));
246
- const st = el("td");
247
- st.append(chip(doc.state, doc.state === "CLEAN" ? "ok" : doc.state === "TOUCHED" ? "info" : "warn"));
248
- tr.append(st);
249
- tr.append(el("td", "note", doc.gone.length ? "gone: " + doc.gone.join(", ") : doc.hits.slice(0, 4).join(", ")));
250
- tb.append(tr);
251
- }
252
- table.append(thead, tb);
253
- scroll.append(table);
254
- c.append(scroll);
255
- out.append(c);
256
- } else if (imp && !imp.ok) {
257
- const c = card(t("knowledge.impact.title"));
258
- c.append(el("div", "note", imp.hint || `unavailable (${imp.reason})`));
259
- out.append(c);
260
- }
261
- return out;
262
- }
263
-
264
- /* THE DOC TABLE. Ordered exactly as the CLI ordered it — THE PANEL NEVER
265
- INVENTS A RANK (the `wiki plan` rule). A row expands IN PLACE. */
266
- function wikiDocsCard(docs) {
267
- const c = card(t("knowledge.docs.title"));
268
- if (!docs || !docs.ok || !(docs.docs || []).length) {
269
- c.append(empty(t("knowledge.docs.none"), (docs && docs.hint) || t("knowledge.docs.noneHint")));
270
- return c;
271
- }
272
- c.append(el("div", "note", t("knowledge.docs.note")));
273
- const scroll = el("div", "scroll-x");
274
- const table = el("table");
275
- const thead = el("thead");
276
- const hr = el("tr");
277
- for (const h of [
278
- t("knowledge.docs.col.doc"),
279
- t("knowledge.docs.col.tier"),
280
- t("knowledge.docs.col.distance"),
281
- t("knowledge.docs.col.covers"),
282
- t("knowledge.docs.col.used"),
283
- t("knowledge.docs.col.tags"),
284
- ])
285
- hr.append(el("th", null, h));
286
- thead.append(hr);
287
- const tb = el("tbody");
288
- const open = { row: null };
289
- for (const r of docs.docs) {
290
- const tr = el("tr", "kn-doc-row" + (r.retire_hint ? " row-muted" : ""));
291
- tr.tabIndex = 0;
292
- const detail = el("tr", "kn-doc-detail");
293
- const dtd = el("td");
294
- dtd.setAttribute("colspan", "6");
295
- detail.append(dtd);
296
- detail.hidden = true;
297
- let loaded = false;
298
- const toggle = () => {
299
- const willOpen = detail.hidden;
300
- // One row open at a time — the Runs-row rule.
301
- if (open.row && open.row !== detail) {
302
- open.row.hidden = true;
303
- open.row.previousSibling.classList.remove("open");
304
- }
305
- detail.hidden = !willOpen;
306
- tr.classList.toggle("open", willOpen);
307
- open.row = willOpen ? detail : null;
308
- if (willOpen && !loaded) {
309
- loaded = true;
310
- loadWikiDoc(dtd, r.file);
311
- }
312
- };
313
- tr.addEventListener("click", toggle);
314
- tr.addEventListener("keydown", (e) => {
315
- if (e.key === "Enter" || e.key === " ") {
316
- e.preventDefault();
317
- toggle();
318
- }
319
- });
320
- tr.append(el("td", "mono", r.file.replace(/^wiki\//, "")));
321
- const tc = el("td");
322
- tc.append(chip(r.tier, r.tier === "FRESH" ? "ok" : r.tier === "AGING" ? "warn" : r.tier === "STALE" ? "bad" : ""));
323
- tr.append(tc);
324
- tr.append(el("td", "num", r.distance === null ? "?" : r.distance + "c"));
325
- tr.append(el("td", "note", (r.covers || []).join(", ")));
326
- // `used: null` is NOT zero-use — unknown must never be reported as dead.
327
- tr.append(el("td", "num", r.used === null ? "?" : `${r.used}/${r.used_of}`));
328
- tr.append(el("td", "num", String(r.crosslink_tags || 0)));
329
- tb.append(tr, detail);
330
- }
331
- table.append(thead, tb);
332
- scroll.append(table);
333
- c.append(scroll);
334
- return c;
335
- }
336
-
337
- async function loadWikiDoc(host, file) {
338
- host.replaceChildren(skeleton(2));
339
- const r = await read("/api/wiki/show?doc=" + encodeURIComponent(file)).catch(() => ({ data: null }));
340
- const d = r.data;
341
- if (!d || !d.ok) {
342
- host.replaceChildren(el("div", "note", (d && d.hint) || t("common.loadFail")));
343
- return;
344
- }
345
- const out = frag();
346
- out.append(
347
- kvList([
348
- [t("knowledge.docs.field.title"), d.title || ""],
349
- [t("knowledge.docs.field.type"), d.doc_type || ""],
350
- [t("knowledge.docs.field.scanned"), d.scanned_commit ? String(d.scanned_commit).slice(0, 8) : ""],
351
- [t("knowledge.docs.field.covers"), (d.covers || []).join(", ")],
352
- [t("knowledge.docs.field.coveredFiles"), String(d.covered_files)],
353
- [t("knowledge.docs.field.lastUsed"), d.last_used || t("knowledge.usage.neverUsed")],
354
- ])
355
- );
356
- if ((d.tags || []).length) {
357
- const tl = el("div", "row-actions");
358
- for (const tag of d.tags) tl.append(chip(tag.tag, "info"));
359
- out.append(tl);
360
- }
361
- // Free repairs FIRST, as everywhere else on this panel.
362
- for (const rep of d.free_repairs || []) {
363
- const row = el("div", "free-row");
364
- row.append(chip(t("knowledge.plan.free"), "ok"));
365
- row.append(el("span", null, rep.what));
366
- row.append(el("code", "mono", rep.cmd));
367
- out.append(row);
368
- }
369
- // REVEAL. The body is fetched only on an explicit request, one artifact at a
370
- // time, and it is rendered as DOM — never as HTML.
371
- const reveal = el("button", "btn btn-ghost btn-sm", t("knowledge.reveal"));
372
- reveal.type = "button";
373
- reveal.addEventListener("click", async () => {
374
- reveal.disabled = true;
375
- const b = await read("/api/wiki/show?doc=" + encodeURIComponent(file) + "&body=1").catch(() => ({ data: null }));
376
- const box = el("div", "kn-body");
377
- box.append(renderMd((b.data && b.data.body) || ""));
378
- reveal.replaceWith(box);
379
- });
380
- out.append(reveal);
381
- host.replaceChildren(out);
382
- }
383
-
384
- /* ── TAB 2 — COVERAGE ────────────────────────────────────────────────────── */
385
- function knCoverageTab(d) {
386
- const out = frag();
387
- const cov = d.coverage;
388
- const c = card(t("knowledge.coverage.title"));
389
- // The read itself failed — a crash, a timeout, output the server could not
390
- // parse. That is a BROKEN PANEL, not a repo without a wiki, and it must never
391
- // again be reported as the latter.
392
- if (d.errors && d.errors.coverage) {
393
- c.append(failBox(d.errors.coverage));
394
- out.append(c);
395
- return out;
396
- }
397
- if (!cov || !cov.ok) {
398
- c.append(empty(t("knowledge.coverage.na"), (cov && cov.hint) || t("knowledge.coverage.naHint")));
399
- out.append(c);
400
- return out;
401
- }
402
- const big = el("div", "kn-coverage");
403
- big.append(el("span", "kn-coverage-num", cov.coverage_pct + "%"));
404
- big.append(el("span", "note", t("knowledge.coverage.sub", { covered: cov.covered, tracked: cov.tracked })));
405
- c.append(big);
406
- const bar = el("div", "kn-tierbar");
407
- const seg = el("div", "kn-tierbar-seg kn-tier-ok");
408
- seg.style.setProperty("--w", cov.coverage_pct + "%");
409
- bar.append(seg);
410
- const rest = el("div", "kn-tierbar-seg kn-tier-idle");
411
- rest.style.setProperty("--w", 100 - cov.coverage_pct + "%");
412
- bar.append(rest);
413
- c.append(bar);
414
- // NOT OPTIONAL CHROME. There is no threshold, no config key, and nothing
415
- // branches on this number — a coverage percentage that starts nagging becomes
416
- // a number people game.
417
- c.append(el("div", "note", t("knowledge.coverage.notATarget")));
418
- out.append(c);
419
-
420
- if ((cov.uncovered_dirs || []).length) {
421
- const uc = card(t("knowledge.coverage.uncovered"));
422
- uc.append(el("div", "note", t("knowledge.coverage.uncoveredNote")));
423
- const scroll = el("div", "scroll-x");
424
- const table = el("table");
425
- const thead = el("thead");
426
- const hr = el("tr");
427
- for (const h of [t("knowledge.coverage.col.dir"), t("knowledge.coverage.col.files"), t("knowledge.coverage.col.last")])
428
- hr.append(el("th", null, h));
429
- thead.append(hr);
430
- const tb = el("tbody");
431
- for (const row of cov.uncovered_dirs) {
432
- const tr = el("tr");
433
- tr.append(el("td", "mono", row.dir));
434
- tr.append(el("td", "num", String(row.files)));
435
- tr.append(el("td", "note", row.newest_commit || ""));
436
- tb.append(tr);
437
- }
438
- table.append(thead, tb);
439
- scroll.append(table);
440
- uc.append(scroll);
441
- out.append(uc);
442
- }
443
-
444
- // THE STRUCTURAL BLIND SPOT AS THE FILE LIST IT ALWAYS WAS — no longer as the
445
- // number `2`, which tells a user nothing they can act on.
446
- const blind = (d.wiki && d.wiki.blind_spot) || (d.impact && d.impact.blind_spot) || [];
447
- const bc = card(t("knowledge.blind.title"));
448
- if (!blind.length) {
449
- bc.append(el("div", "note ok", t("knowledge.blind.none")));
450
- } else {
451
- bc.append(el("div", "note", t("knowledge.blind.note")));
452
- const fl = el("div", "file-list");
453
- for (const f of blind) fl.append(el("div", null, f));
454
- bc.append(fl);
455
- }
456
- out.append(bc);
457
- return out;
458
- }
459
-
460
- /* ── TAB 3 — PATTERNS ────────────────────────────────────────────────────── */
461
- function knPatternsTab(d) {
462
- const out = frag();
463
- const p = d.patterns || {};
464
- const pc = card(t("knowledge.patterns.title"));
465
- pc.append(el("div", "note", t("knowledge.patterns.note")));
466
- if (!(p.patterns || []).length) {
467
- pc.append(empty(t("knowledge.patterns.none"), t("knowledge.patterns.noneHint")));
468
- } else {
469
- for (const row of p.patterns) {
470
- const box = el("div", "kn-pattern");
471
- const hdr = el("div", "row-actions");
472
- // The language KEY is the CLI's framework id (`react`, `nestjs`, …) — a
473
- // translated one would resolve to no playbook.
474
- hdr.append(el("span", "mono", row.lang));
475
- hdr.append(el("span", "note", relAge(row.mtime_ms)));
476
- box.append(hdr);
477
- box.append(el("div", "note mono", row.path));
478
- const more = el("button", "btn btn-ghost btn-sm", t("knowledge.patterns.inspect"));
479
- more.type = "button";
480
- const detail = el("div", "stack stack-sm");
481
- more.addEventListener("click", async () => {
482
- more.disabled = true;
483
- await loadPattern(detail, row.lang);
484
- more.remove();
485
- });
486
- box.append(more, detail);
487
- pc.append(box);
488
- }
489
- }
490
- // Known-but-uncached languages: PAID, so a command and never a button.
491
- const cached = new Set((p.patterns || []).map((x) => x.lang));
492
- const uncached = (p.known_languages || []).filter((l) => !cached.has(l));
493
- if (uncached.length) {
494
- pc.append(el("div", "note", t("knowledge.patterns.known", { list: uncached.join(", ") })));
495
- for (const l of uncached.slice(0, 6)) pc.append(el("pre", "cmd", `/orc-pattern ${l}`));
496
- pc.append(el("div", "note", t("knowledge.patterns.paidWhy")));
497
- }
498
- out.append(pc);
499
- return out;
500
- }
501
-
502
- async function loadPattern(host, lang) {
503
- host.replaceChildren(skeleton(2));
504
- const r = await read("/api/pattern/show?lang=" + encodeURIComponent(lang)).catch(() => ({ data: null }));
505
- const d = r.data;
506
- if (!d || !d.ok) {
507
- host.replaceChildren(el("div", "note", (d && d.hint) || t("common.loadFail")));
508
- return;
509
- }
510
- const out = frag();
511
- out.append(
512
- kvList([
513
- [t("knowledge.patterns.field.codified"), d.codified_at || "—"],
514
- [t("knowledge.patterns.field.source"), d.source_commit ? String(d.source_commit).slice(0, 8) : "—"],
515
- [t("knowledge.patterns.field.playbook"), d.playbook || "—"],
516
- [t("knowledge.patterns.field.size"), `${d.lines} · ${d.bytes}`],
517
- [t("knowledge.patterns.field.counts"), `${d.conventions} · ${d.invariants}`],
518
- ])
519
- );
520
- // An unheadered file SAYS SO in one line. No date is ever derived from an
521
- // mtime — that is the `/orc-pact` UNCHECKABLE rule.
522
- if (!d.headered) out.append(el("div", "note warn", t("knowledge.patterns.unheadered")));
523
- if ((d.headings || []).length) {
524
- const hl = el("div", "row-actions");
525
- for (const h of d.headings.slice(0, 20)) hl.append(chip(h, ""));
526
- out.append(hl);
527
- }
528
- // CONFLICTS get their own block: they are the most decision-shaped thing in
529
- // the file (the project does X, the invariant says Y) and until now they were
530
- // invisible outside it.
531
- if ((d.conflicts || []).length) {
532
- const cb = el("div", "kn-conflicts");
533
- cb.append(el("div", "kn-conflicts-head", t("knowledge.patterns.conflicts")));
534
- for (const c of d.conflicts) cb.append(el("div", "note", c));
535
- out.append(cb);
536
- }
537
- const reveal = el("button", "btn btn-ghost btn-sm", t("knowledge.reveal"));
538
- reveal.type = "button";
539
- reveal.addEventListener("click", async () => {
540
- reveal.disabled = true;
541
- const b = await read("/api/pattern/show?lang=" + encodeURIComponent(lang) + "&body=1").catch(() => ({ data: null }));
542
- const box = el("div", "kn-body");
543
- box.append(renderMd((b.data && b.data.body) || ""));
544
- reveal.replaceWith(box);
545
- });
546
- out.append(reveal);
547
- out.append(el("div", "note", t("knowledge.patterns.literalNote")));
548
- host.replaceChildren(out);
549
- }
550
-
551
- /* ── TAB 4 — MEMORY (gotchas) ────────────────────────────────────────────── */
552
- function knMemoryTab(d, body) {
553
- const out = frag();
554
- const g = d.gotchas || {};
555
- const gc = card(t("knowledge.gotchas.title"));
556
- gc.append(el("div", "note", t("knowledge.gotchas.note")));
557
- if (!g.count) {
558
- gc.append(empty(t("knowledge.gotchas.none"), t("knowledge.gotchas.noneHint")));
559
- out.append(gc);
560
- return out;
561
- }
562
- const scroll = el("div", "scroll-x");
563
- const table = el("table");
564
- const thead = el("thead");
565
- const hr = el("tr");
566
- // These are the gotcha record's own field names, printed by `orc gotcha
567
- // list` — column headers stay in the file's vocabulary.
568
- for (const h of ["Id", "Area", "Kind", "Hits", "Last seen", "Trigger"]) hr.append(el("th", null, h));
569
- thead.append(hr);
570
- const tb = el("tbody");
571
- const open = { row: null };
572
- for (const e of g.gotchas) {
573
- const tr = el("tr", "kn-doc-row");
574
- tr.tabIndex = 0;
575
- const detail = el("tr", "kn-doc-detail");
576
- const dtd = el("td");
577
- dtd.setAttribute("colspan", "6");
578
- detail.append(dtd);
579
- detail.hidden = true;
580
- // EVERY FIELD the CLI already emits. `gotchaStatus` has always sent
581
- // `fields`; the panel rendered six columns and discarded the rest.
582
- dtd.append(kvList(Object.keys(e.fields || {}).map((k) => [k, e.fields[k]])));
583
- const toggle = () => {
584
- const willOpen = detail.hidden;
585
- if (open.row && open.row !== detail) {
586
- open.row.hidden = true;
587
- open.row.previousSibling.classList.remove("open");
588
- }
589
- detail.hidden = !willOpen;
590
- tr.classList.toggle("open", willOpen);
591
- open.row = willOpen ? detail : null;
592
- };
593
- tr.addEventListener("click", toggle);
594
- tr.addEventListener("keydown", (ev) => {
595
- if (ev.key === "Enter" || ev.key === " ") {
596
- ev.preventDefault();
597
- toggle();
598
- }
599
- });
600
- tr.append(
601
- el("td", "mono", e.id),
602
- el("td", "mono", e.area),
603
- el("td", null, e.kind),
604
- el("td", null, String(e.hits)),
605
- el("td", "note", e.last_seen || "?"),
606
- el("td", "note", e.trigger || "")
607
- );
608
- tb.append(tr, detail);
609
- }
610
- table.append(thead, tb);
611
- scroll.append(table);
612
- gc.append(scroll);
613
- gc.append(gotchaPruneBox(g, body));
614
- out.append(gc);
615
-
616
- // The archive is reachable and clearly labelled as recoverable: eviction is
617
- // an ARCHIVE, never a delete.
618
- const ac = card(t("knowledge.archive.title"));
619
- const load = el("button", "btn btn-ghost btn-sm", t("knowledge.archive.load"));
620
- load.type = "button";
621
- load.addEventListener("click", async () => {
622
- load.disabled = true;
623
- const r = await read("/api/gotchas/archived").catch((e) => ({ data: null, error: e }));
624
- const a = r.data;
625
- const box = el("div", "stack stack-sm");
626
- if (!a || !a.count) box.append(el("div", "note", t("knowledge.archive.empty")));
627
- else for (const e of a.gotchas) box.append(el("div", "note mono", `${e.id} · ${e.area} · ${e.kind} · hits ${e.hits}`));
628
- load.replaceWith(box);
629
- });
630
- ac.append(el("div", "note", t("knowledge.archive.note")));
631
- ac.append(load);
632
- out.append(ac);
633
- return out;
634
- }
635
-
636
- /* PREVIEW-THEN-APPLY. The Apply button stays disabled until a preview has been
637
- fetched, the exact command is on screen throughout, and THE PREVIEW NAMES
638
- EVERY ENTRY that would be archived — a count is not consent. */
639
- function gotchaPruneBox(g, body) {
640
- const box = el("div", "kn-prune");
641
- const cfgMax = g.gotchas_max;
642
- box.append(el("div", "kn-prune-head", t("knowledge.gotchas.headroom", { n: g.count, max: cfgMax === undefined ? "?" : cfgMax })));
643
- box.append(el("pre", "cmd", "orc gotcha prune"));
644
- const acts = el("div", "row-actions");
645
- const preview = el("button", "btn btn-sm", t("knowledge.gotchas.preview"));
646
- preview.type = "button";
647
- const apply = el("button", "btn btn-sm btn-primary", t("knowledge.gotchas.prune"));
648
- apply.type = "button";
649
- apply.disabled = true;
650
- const list = el("div", "stack stack-sm");
651
- preview.addEventListener("click", async () => {
652
- const r = await read("/api/gotcha/prune/preview").catch((e) => ({ data: null, error: e }));
653
- const d = r.data;
654
- list.replaceChildren();
655
- if (!d || !(d.would_archive || []).length) {
656
- list.append(el("div", "note ok", t("knowledge.gotchas.pruneNone")));
657
- apply.disabled = true;
658
- return;
659
- }
660
- list.append(el("div", "note warn", t("knowledge.gotchas.pruneWould", { n: d.would_archive.length })));
661
- for (const e of d.would_archive) list.append(el("div", "note mono", `${e.id} · ${e.area} · ${e.kind} · hits ${e.hits} · ${e.last_seen || "?"}`));
662
- list.append(el("div", "note", d.honesty));
663
- apply.disabled = false;
664
- });
665
- apply.addEventListener("click", async () => {
666
- const r = await post("/api/gotcha/prune", {});
667
- toast(r.command, r.ok ? "ok" : "bad", r.output);
668
- renderKnowledge(body);
669
- });
670
- acts.append(preview, apply);
671
- box.append(acts, list);
672
- return box;
673
- }
674
-
675
- /* ── TAB 5 — PEERS ───────────────────────────────────────────────────────── */
676
- /* COMPACT and READ-ONLY. It links to Crosslink and NEVER duplicates its editor:
677
- one boundary, one picture. This tab exists because "what does ORC know?"
678
- includes what it knows from next door, and that is where a user looks first. */
679
- function knPeersTab(d) {
680
- const out = frag();
681
- const c = card(t("knowledge.peers.title"));
682
- c.append(el("div", "note", t("knowledge.peers.note")));
683
- const go = el("button", "btn btn-ghost btn-sm", t("knowledge.peers.open"));
684
- go.type = "button";
685
- go.addEventListener("click", () => {
686
- location.hash = "#/crosslink";
687
- });
688
- c.append(go);
689
- out.append(c);
690
- renderPeers(c);
691
- return out;
692
- }
693
-
694
- async function renderPeers(host) {
695
- const r = await read("/api/crosslink").catch((e) => ({ data: null, error: e }));
696
- const d = r.data;
697
- if (!d) return;
698
- if (!d.configured || !(d.nodes || []).length) {
699
- host.append(el("div", "note", t("knowledge.peers.none")));
700
- return;
701
- }
702
- const scroll = el("div", "scroll-x");
703
- const table = el("table");
704
- const thead = el("thead");
705
- const hr = el("tr");
706
- for (const h of [
707
- t("knowledge.peers.col.repo"),
708
- t("knowledge.peers.col.state"),
709
- t("knowledge.peers.col.tier"),
710
- t("knowledge.peers.col.tags"),
711
- ])
712
- hr.append(el("th", null, h));
713
- thead.append(hr);
714
- const tb = el("tbody");
715
- for (const n of d.nodes) {
716
- const prov = n.provider || {};
717
- const tr = el("tr");
718
- tr.append(el("td", "mono", n.name));
719
- const st = el("td");
720
- // Every word here is the CLI's own — the peer's state, and the peer's
721
- // freshness tier as IT computed it. A peer's config is not ours to read
722
- // (`crosslinkProviderInfo`), so nothing here is recomputed on this side.
723
- st.append(chip(prov.state || "—", prov.state === "wiki" ? "ok" : "warn"));
724
- tr.append(st);
725
- const tc = el("td");
726
- if (prov.tier) tc.append(chip(prov.tier, prov.tier === "FRESH" ? "ok" : prov.tier === "AGING" ? "warn" : "bad"));
727
- else tc.append(el("span", "note", "—"));
728
- tr.append(tc);
729
- tr.append(el("td", "num", prov.tags === undefined ? "—" : String(prov.tags)));
730
- tb.append(tr);
731
- }
732
- table.append(thead, tb);
733
- scroll.append(table);
734
- host.append(scroll);
735
- }
736
-
737
- /* PART B MADE VISIBLE (v0.46.0).
738
-
739
- THE PANEL MUST NEVER COMPUTE THE ORDER, THE TIER OR THE ESTIMATE ITSELF — it
740
- renders `orc wiki plan --json`'s rows in the order they arrive and nothing
741
- else, the same rule the Flow stepper lives under. A second idea of "which doc
742
- matters most" is exactly the drift this panel exists to make impossible.
743
-
744
- And: a `used 0/20` row KEEPS ITS SLOT, rendered muted with a retire hint.
745
- Filtering it out would make "nobody reads this" and "this does not exist"
746
- look identical — the same rule as an OFF phase in the stepper. */
747
- function wikiPlanCard(plan, debt, body) {
748
- const c = card(t("knowledge.plan"), wikiPlanActions(body, plan));
749
-
750
- if (!plan || !plan.ok) {
751
- c.append(empty(t("knowledge.plan.na"), t("knowledge.plan.naHint")));
752
- return c;
753
- }
754
-
755
- // The debt line first: the habit this whole workstream is aiming at.
756
- if (debt && debt.ok && debt.pending) {
757
- const chips = el("div", "row-actions");
758
- chips.append(chip(tn(debt.pending, "knowledge.debt.pending"), "warn"));
759
- if (debt.tokens) chips.append(chip(kTokUi(debt.tokens.input + debt.tokens.cache_write + debt.tokens.cache_read + debt.tokens.output), null));
760
- if (debt.usd !== null && debt.usd !== undefined) chips.append(chip("$" + debt.usd.toFixed(2), null));
761
- if (debt.oldest_commits_behind !== null) chips.append(chip(tn(debt.oldest_commits_behind, "knowledge.debt.oldest"), null));
762
- c.append(chips);
763
- c.append(el("div", "note", t("knowledge.debt.nothingBroken")));
764
- } else if (debt && debt.ok) {
765
- c.append(el("div", "note ok", t("knowledge.debt.none")));
766
- }
767
-
768
- if (!plan.rows || !plan.rows.length) {
769
- c.append(el("div", "note ok", t("knowledge.plan.clean")));
770
- return c;
771
- }
772
-
773
- // FREE REPAIRS FIRST — a user must never be able to pay for something a free
774
- // step would have fixed, so they render ABOVE the priced table.
775
- if (plan.free_repairs && plan.free_repairs.length) {
776
- const box = el("div", "free-box");
777
- box.append(el("div", "free-head", t("knowledge.plan.freeFirst")));
778
- for (const r of plan.free_repairs) {
779
- const row = el("div", "free-row");
780
- row.append(chip(t("knowledge.plan.free"), "ok"));
781
- row.append(el("span", null, r.what));
782
- row.append(el("code", "mono", r.cmd));
783
- box.append(row);
784
- }
785
- c.append(box);
786
- }
787
-
788
- const tbl = el("table", "tbl");
789
- const th = el("tr");
790
- for (const h of ["knowledge.plan.col.doc", "knowledge.plan.col.state", "knowledge.plan.col.delta", "knowledge.plan.col.used", "knowledge.plan.col.tier", "knowledge.plan.col.tokens", "knowledge.plan.col.usd"])
791
- th.append(el("th", null, t(h)));
792
- tbl.append(th);
793
- for (const r of plan.rows) {
794
- const tr = el("tr", r.retire_hint ? "row-muted" : null);
795
- tr.append(el("td", "mono", r.doc.replace(/^wiki\//, "")));
796
- // The CLI's exact state words. Never a friendlier synonym.
797
- const stateCell = el("td");
798
- stateCell.append(chip(r.state, r.state === "STRUCTURAL" ? "bad" : "warn"));
799
- tr.append(stateCell);
800
- tr.append(el("td", "num", r.state === "STRUCTURAL" ? "—" : String(r.delta)));
801
- tr.append(el("td", "num", r.used === null ? "?" : `${r.used}/${r.used_of}`));
802
- tr.append(el("td", null, r.tier));
803
- const est = r.estimate;
804
- tr.append(el("td", "num", est ? kTokUi(est.p50.input + est.p50.cache_write + est.p50.cache_read + est.p50.output) : "—"));
805
- // The dollar figure is the CLI's — the panel never prices anything itself,
806
- // and a row the CLI could not price shows an em dash rather than a guess.
807
- tr.append(el("td", "num", r.usd === null || r.usd === undefined ? "—" : "$" + r.usd.toFixed(2)));
808
- tbl.append(tr);
809
- if (r.state === "STRUCTURAL" && r.gone && r.gone.length) {
810
- const note = el("tr", "row-note");
811
- const td = el("td", null, t("knowledge.plan.gone", { files: r.gone.slice(0, 3).join(", ") }));
812
- td.setAttribute("colspan", "7");
813
- note.append(td);
814
- tbl.append(note);
815
- }
816
- if (r.retire_hint) {
817
- const note = el("tr", "row-note");
818
- const td = el("td", null, t("knowledge.plan.retireHint", { n: r.used_of }));
819
- td.setAttribute("colspan", "7");
820
- note.append(td);
821
- tbl.append(note);
822
- }
823
- }
824
- c.append(tbl);
825
- if (plan.estimate_unavailable) c.append(el("div", "note", t("knowledge.plan.noEstimate")));
826
- c.append(el("div", "note", t("knowledge.plan.tierNote", { mode: plan.scan_tier_mode, deep: plan.deep, light: plan.light })));
827
- // A refresh COSTS MONEY, so it is a command, never a button.
828
- c.append(laneCommand(`/orc-wiki refresh --top ${Math.min(2, plan.rows.length)}`, t("knowledge.plan.refreshWhy")));
829
- return c;
830
- }
831
-
832
- function wikiPlanActions(body, plan) {
833
- const wrap = el("div", "row-actions");
834
- // `orc wiki sync` is FREE ($0.00), so it gets a button.
835
- const s = el("button", "btn btn-sm", t("knowledge.syncFree"));
836
- s.type = "button";
837
- s.addEventListener("click", async () => {
838
- const r = await post("/api/wiki/sync", {});
839
- toast(r.ok ? t("knowledge.syncOk") : t("common.writeFail"), r.ok ? "ok" : "bad", r.output);
840
- renderKnowledge(body);
841
- });
842
- wrap.append(s);
843
- void plan;
844
- return wrap;
845
- }
846
-
847
- function wikiUsageCard(u, body) {
848
- const c = card(t("knowledge.usage"), (() => {
849
- const wrap = el("div", "row-actions");
850
- const b = el("button", "btn btn-ghost btn-sm", t("knowledge.usage.rebuild"));
851
- b.type = "button";
852
- b.addEventListener("click", async () => {
853
- const r = await post("/api/wiki/usage/rebuild", {});
854
- toast(r.ok ? t("knowledge.usage.rebuilt") : t("common.writeFail"), r.ok ? "ok" : "bad", r.output);
855
- renderKnowledge(body);
856
- });
857
- wrap.append(b);
858
- return wrap;
859
- })());
860
- const chips = el("div", "row-actions");
861
- chips.append(chip(t("knowledge.usage.registered", { n: u.registered }), null));
862
- chips.append(chip(t("knowledge.usage.active", { n: u.in_active_use }), "ok"));
863
- if (u.never_used) chips.append(chip(t("knowledge.usage.never", { n: u.never_used, runs: u.window_runs }), "warn"));
864
- c.append(chips);
865
- const body2 = el("div", "usage-rows");
866
- for (const r of u.rows) {
867
- const row = el("div", "usage-row" + (r.used ? "" : " row-muted"));
868
- row.append(el("span", "mono", r.doc.replace(/^wiki\//, "")));
869
- const track = el("div", "bar-track");
870
- const fill = el("div", "bar-fill");
871
- track.append(fill);
872
- requestAnimationFrame(() => fill.style.setProperty("width", Math.max(2, (r.used / (r.of || 1)) * 100) + "%"));
873
- row.append(track);
874
- row.append(el("span", "bar-value", `${r.used}/${r.of}`));
875
- row.append(el("span", "note", r.last_used || t("knowledge.usage.neverUsed")));
876
- body2.append(row);
877
- }
878
- c.append(body2);
879
- c.append(el("div", "note", t("knowledge.usage.note")));
880
- return c;
881
- }
882
-
883
- /* ONE DOC AT A TIME (v1.7.0).
884
- Two halves of the same question, and neither of them scans.
885
-
886
- The RESOLVER answers "is this topic already in the wiki?" — free, from the
887
- doc headers. It is a probe, so the panel runs it; the refresh or the add that
888
- follows is a LANE, so the panel prints the command and stops. That is the
889
- panel's standing line, visible here rather than implied: a free action gets a
890
- button, a paid one gets a command you can copy.
891
-
892
- The SWEEP is `orc wiki refs --check`. It exists because after ONE doc changes,
893
- the surfaces DERIVED from the doc set are behind it — and on a small run
894
- nobody remembers to look. `--check`, always: opening a panel must never write
895
- to a repo. */
896
- const REFS_STATE_KIND = { clean: "ok", repaired: "ok", absent: "", behind: "warn", outstanding: "warn", missing: "warn", drifted: "bad", dead: "warn" };
897
-
898
- function wikiOneDocCard(refs) {
899
- const c = card(t("knowledge.one.title"));
900
- c.append(el("div", "note", t("knowledge.one.lead")));
901
-
902
- // ── the resolver ──────────────────────────────────────────────────────────
903
- const row = el("div", "row-actions");
904
- const input = el("input", "text-input kn-one-topic");
905
- input.type = "text";
906
- input.placeholder = t("knowledge.one.placeholder");
907
- input.setAttribute("aria-label", t("knowledge.one.placeholder"));
908
- const go = el("button", "btn btn-sm btn-primary", t("knowledge.one.resolve"));
909
- go.type = "button";
910
- row.append(input, go);
911
- c.append(row);
912
-
913
- const out = el("div", "kn-one-out");
914
- c.append(out);
915
-
916
- const resolve = async () => {
917
- const topic = input.value.trim();
918
- if (!topic) return;
919
- go.disabled = true;
920
- out.replaceChildren(skeleton(1));
921
- try {
922
- const r = await read("/api/wiki/resolve?topic=" + encodeURIComponent(topic));
923
- const d = r.data || {};
924
- out.replaceChildren();
925
- if (!d.ok) {
926
- out.append(el("div", "note warn", d.hint || t("knowledge.one.cannot")));
927
- return;
928
- }
929
- const head = el("div", "row-actions");
930
- // The CLI's own verdict word, never a friendlier synonym.
931
- head.append(chip(d.verdict, d.verdict === "match" ? "ok" : d.verdict === "ambiguous" ? "warn" : ""));
932
- if (d.verdict === "match") head.append(el("span", "mono", d.match.file));
933
- out.append(head);
934
-
935
- if (d.verdict === "match") out.append(el("div", "note", d.match.why.slice(0, 4).join(" · ")));
936
- // AMBIGUOUS is a question, not a guess: every candidate is listed with its
937
- // score, and no command is offered until a human picks one.
938
- if (d.verdict === "ambiguous")
939
- for (const cand of d.candidates || []) {
940
- const cr = el("div", "kn-one-cand");
941
- cr.append(el("span", "note mono", String(cand.score)));
942
- cr.append(el("span", "mono", cand.file));
943
- cr.append(el("span", "note", (cand.why || []).slice(0, 3).join(" · ")));
944
- out.append(cr);
945
- }
946
- if (d.verdict === "new" && (d.suggested_covers || []).length) {
947
- out.append(el("div", "note", t("knowledge.one.proposed")));
948
- for (const s of d.suggested_covers) {
949
- const sr = el("div", "kn-one-cand");
950
- sr.append(el("span", "mono", s.glob));
951
- sr.append(el("span", "note", tn(s.matches, "knowledge.one.files")));
952
- out.append(sr);
953
- }
954
- // The CLI's own caveat, relayed. A proposal that arrives without it
955
- // reads as an answer.
956
- out.append(el("div", "note", d.note));
957
- }
958
- // A LANE costs money, so it is a command to copy, never a button.
959
- out.append(laneCommand(d.next, t("knowledge.one.next")));
960
- } catch (e) {
961
- out.replaceChildren(failBox(e));
962
- } finally {
963
- go.disabled = false;
964
- }
965
- };
966
- go.addEventListener("click", resolve);
967
- input.addEventListener("keydown", (ev) => {
968
- if (ev.key === "Enter") resolve();
969
- });
970
-
971
- // ── the sweep ─────────────────────────────────────────────────────────────
972
- if (refs && refs.items) {
973
- const sweep = el("div", "kn-one-sweep");
974
- sweep.append(el("div", "checklist-head", t("knowledge.one.sweep")));
975
- for (const i of refs.items) {
976
- const ir = el("div", "kn-one-ref");
977
- ir.append(chip(i.state, REFS_STATE_KIND[i.state] || ""));
978
- ir.append(el("span", "mono", i.id));
979
- ir.append(el("span", "note", i.what));
980
- sweep.append(ir);
981
- if (i.fix) sweep.append(el("div", "note kn-one-fix", "→ " + i.fix + " (" + i.cost + ")"));
982
- }
983
- // WHY only one surface is ever repaired. Relayed from the CLI, because a
984
- // sweep that looked like it fixed everything would be the more dangerous
985
- // reading.
986
- sweep.append(el("div", "note", refs.note));
987
- c.append(sweep);
988
- } else if (refs && refs.hint) {
989
- c.append(el("div", "note", refs.hint));
990
- }
991
- return c;
992
- }
993
-
994
- function wikiActions(body, w) {
995
- if (!w || !w.state || w.state === "none") return null;
996
- const wrap = el("div", "row-actions");
997
- const sync = el("button", "btn btn-sm", "orc wiki sync");
998
- sync.type = "button";
999
- sync.addEventListener("click", async () => {
1000
- const r = await post("/api/wiki/sync", {});
1001
- toast(r.command, r.ok ? "ok" : "bad", r.output);
1002
- renderKnowledge(body);
1003
- });
1004
- wrap.append(sync);
1005
- return wrap;
1006
- }
1
+ "use strict";
2
+ /* panels/knowledge.js — orc ui client
3
+ What ORC knows about this repo: the wiki, its coverage, the code patterns,
4
+ the repair memory, and what it knows from next door.
5
+
6
+ FIVE TABS since v0.49.1, the Crosslink two-tab precedent. This was one
7
+ scrolling column of six cards, and the release that made the CLI stop
8
+ discarding what it computes roughly tripled the content — which is not
9
+ survivable as one scroll.
10
+
11
+ THE PANEL DERIVES NOTHING. It never computes a tier, a distance, a coverage
12
+ percentage, an order, an estimate or a price: `computeWikiFreshness` is the
13
+ one engine, `orc wiki plan` decides the order, and a value the CLI could not
14
+ compute renders as an em dash, never as a guess.
15
+
16
+ A FREE ACTION GETS A BUTTON; A PAID ACTION GETS A COPY-ABLE COMMAND — and
17
+ that line is visible on the panel rather than implied.
18
+
19
+ Loaded by app.html in the order its numeric prefix names. Classic
20
+ script, no import/export: an ES module import carries no query string,
21
+ and every static request here needs the per-launch session token. */
22
+
23
+ /* =============================================================== KNOWLEDGE == */
24
+
25
+ PANELS.knowledge = function (host) {
26
+ head(host, t("knowledge.title"), t("knowledge.sub"));
27
+ const body = el("div", "stack");
28
+ host.append(body);
29
+ renderKnowledge(body);
30
+ };
31
+
32
+ // Which tab was open, so a write that re-renders the panel does not throw the
33
+ // user back to the first one.
34
+ let KN_TAB = "wiki";
35
+
36
+ async function renderKnowledge(body) {
37
+ body.replaceChildren(skeleton(6));
38
+ const [wikiRes, impactRes, patRes, gotRes, planRes, debtRes, usageRes, docsRes, covRes, refsRes, graphRes] = await Promise.all([
39
+ read("/api/wiki").catch((e) => ({ data: null, error: e })),
40
+ read("/api/wiki/impact").catch((e) => ({ data: null, error: e })),
41
+ read("/api/patterns").catch((e) => ({ data: null, error: e })),
42
+ read("/api/gotchas").catch((e) => ({ data: null, error: e })),
43
+ read("/api/wiki/plan").catch((e) => ({ data: null, error: e })),
44
+ read("/api/wiki/debt").catch((e) => ({ data: null, error: e })),
45
+ read("/api/wiki/usage").catch((e) => ({ data: null, error: e })),
46
+ read("/api/wiki/docs").catch((e) => ({ data: null, error: e })),
47
+ read("/api/wiki/coverage").catch((e) => ({ data: null, error: e })),
48
+ // v1.7.0 — the derived-reference sweep. `--check`, always: opening a panel
49
+ // must never write to a repo.
50
+ read("/api/wiki/refs").catch((e) => ({ data: null, error: e })),
51
+ // v1.8.0 — the code graph. `graph status` exits 0-3 and every code is DATA.
52
+ read("/api/graph").catch((e) => ({ data: null, error: e })),
53
+ ]);
54
+ const d = {
55
+ wiki: wikiRes.data || {},
56
+ impact: impactRes.data,
57
+ patterns: patRes.data || {},
58
+ gotchas: gotRes.data || {},
59
+ plan: planRes.data,
60
+ debt: debtRes.data,
61
+ usage: usageRes.data,
62
+ docs: docsRes.data,
63
+ coverage: covRes.data,
64
+ refs: refsRes.data,
65
+ graph: graphRes.data,
66
+ // A read that FAILED is not the same as a read that came back empty, and
67
+ // rendering them identically is what turned a truncated 30 KB payload into
68
+ // "this repo has no wiki and no git" (v0.49.4). The server already puts the
69
+ // CLI's own words in the 500 body; the panel just has to stop discarding
70
+ // them.
71
+ errors: { coverage: covRes.error || null, docs: docsRes.error || null },
72
+ };
73
+
74
+ const out = frag();
75
+ // THE HEADER STRIP, above the tabs and on every one of them: the numbers that
76
+ // answer "what does ORC know about this repo?" in one line. Every value is
77
+ // CLI-computed, and one it could not compute renders as an em dash.
78
+ out.append(knowledgeHeaderStrip(d));
79
+
80
+ const tabs = el("div", "tabs");
81
+ const pane = el("div", "tab-pane stack");
82
+ const views = {
83
+ wiki: () => knWikiTab(d, body),
84
+ coverage: () => knCoverageTab(d),
85
+ patterns: () => knPatternsTab(d),
86
+ memory: () => knMemoryTab(d, body),
87
+ peers: () => knPeersTab(d),
88
+ };
89
+ const select = (which) => {
90
+ KN_TAB = which;
91
+ for (const b of tabs.children) b.setAttribute("aria-selected", String(b.dataset.tab === which));
92
+ pane.replaceChildren(views[which]());
93
+ };
94
+ // Keys are written out in full, never assembled from the tab id — a key built
95
+ // from a fragment is invisible to every check that looks for one.
96
+ for (const [which, label] of [
97
+ ["wiki", t("knowledge.tab.wiki")],
98
+ ["coverage", t("knowledge.tab.coverage")],
99
+ ["patterns", t("knowledge.tab.patterns")],
100
+ ["memory", t("knowledge.tab.memory")],
101
+ ["peers", t("knowledge.tab.peers")],
102
+ ]) {
103
+ const b = el("button", null, label);
104
+ b.type = "button";
105
+ b.dataset.tab = which;
106
+ b.addEventListener("click", () => select(which));
107
+ tabs.append(b);
108
+ }
109
+ out.append(tabs, pane);
110
+ body.replaceChildren(out);
111
+ select(views[KN_TAB] ? KN_TAB : "wiki");
112
+ }
113
+
114
+ /* THE HEADER STRIP. Six numbers, all of them the CLI's. A `—` means the CLI
115
+ could not compute it, and that is an ANSWER — never a zero, never a guess. */
116
+ function knowledgeHeaderStrip(d) {
117
+ const w = d.wiki || {};
118
+ const strip = el("div", "kn-strip");
119
+ const item = (label, value, kind) => {
120
+ const box = el("div", "kn-strip-item" + (kind ? " kn-" + kind : ""));
121
+ box.append(el("span", "kn-strip-value", value));
122
+ box.append(el("span", "kn-strip-label", label));
123
+ strip.append(box);
124
+ };
125
+ // The tier word is the CLI's own. Never a friendlier synonym.
126
+ item(t("knowledge.strip.wiki"), w.tier || (w.state ? String(w.state).toUpperCase() : "—"));
127
+ item(t("knowledge.strip.docs"), w.docs === undefined ? "—" : String(w.docs));
128
+ item(
129
+ t("knowledge.strip.covered"),
130
+ d.coverage && d.coverage.ok ? d.coverage.coverage_pct + "%" : "—"
131
+ );
132
+ item(t("knowledge.strip.blind"), w.blind === undefined ? "—" : String(w.blind));
133
+ item(
134
+ t("knowledge.strip.pending"),
135
+ d.debt && d.debt.ok && d.debt.pending !== undefined ? String(d.debt.pending) : "—"
136
+ );
137
+ item(t("knowledge.strip.patterns"), String((d.patterns.patterns || []).length));
138
+ item(t("knowledge.strip.gotchas"), d.gotchas.count === undefined ? "—" : String(d.gotchas.count));
139
+ // The graph's state word is the CLI's own, upper-cased like the wiki tier.
140
+ item(t("knowledge.strip.graph"), d.graph && d.graph.state ? String(d.graph.state).toUpperCase() : "—");
141
+ return strip;
142
+ }
143
+
144
+ /* THE CODE GRAPH CARD (v1.8.0). Every value is `orc graph status --json`'s, and
145
+ the state word is never replaced by a friendlier synonym. An update is FREE
146
+ (parser only, no model), so it is a button; an OFF graph shows the one config
147
+ command that turns it on, because turning a feature on is the user's call. */
148
+ function graphCard(g, body) {
149
+ const c = card(t("knowledge.graph.title"));
150
+ if (!g || !g.state) {
151
+ c.append(empty(t("knowledge.graph.unknown"), t("knowledge.graph.unknownHint")));
152
+ return c;
153
+ }
154
+ if (g.state === "off") {
155
+ c.append(empty(t("knowledge.graph.off"), t("knowledge.graph.offHint")));
156
+ c.append(el("pre", "cmd", "orc config set code_graph on"));
157
+ return c;
158
+ }
159
+ const head = el("div", "row-actions");
160
+ head.append(chip(String(g.state).toUpperCase(), g.state === "fresh" ? "ok" : g.state === "drifted" ? "warn" : "idle", g.state === "drifted"));
161
+ const upd = el("button", "btn btn-sm", "orc graph update");
162
+ upd.type = "button";
163
+ upd.addEventListener("click", async () => {
164
+ const r = await post("/api/graph/update", {});
165
+ toast(r.command, r.ok ? "ok" : "bad", r.output);
166
+ renderKnowledge(body);
167
+ });
168
+ head.append(upd);
169
+ c.append(head);
170
+ if (g.state === "none") {
171
+ c.append(el("div", "note", t("knowledge.graph.none")));
172
+ return c;
173
+ }
174
+ const b = g.behind || {};
175
+ c.append(
176
+ kvList([
177
+ [t("knowledge.graph.files"), String(g.files)],
178
+ [t("knowledge.graph.symbols"), String(g.symbols)],
179
+ // EW1 — the generation is what a card quotes back, so the panel shows the
180
+ // one on disk right now. `gen_id` names the CONTENT behind that number.
181
+ [t("knowledge.graph.generation"), g.generation ? `${g.generation}${g.gen_id ? ` · ${g.gen_id}` : ""}` : "—"],
182
+ [t("knowledge.graph.updated"), g.updated_at || "—"],
183
+ [t("knowledge.graph.behind"), g.behind ? `+${b.added || 0} · ~${b.changed || 0} · -${b.deleted || 0}` : "—"],
184
+ // `code_graph_notes` is a config VALUE — shown, never translated.
185
+ [t("knowledge.graph.notes"), g.notes || "—"],
186
+ ])
187
+ );
188
+ if (g.state === "drifted") c.append(el("div", "note warn", t("knowledge.graph.drifted")));
189
+ // EW3/EW4 — two things keep the map fresh without a lane step. A panel that
190
+ // shows a DRIFTED map without saying that is a panel that invites a needless
191
+ // click.
192
+ c.append(el("div", "note", t("knowledge.graph.selfheal")));
193
+ c.append(el("div", "note", t("knowledge.graph.locator")));
194
+ return c;
195
+ }
196
+
197
+ /* ── TAB 1 — WIKI ────────────────────────────────────────────────────────── */
198
+ function knWikiTab(d, body) {
199
+ const out = frag();
200
+ out.append(wikiPlanCard(d.plan, d.debt, body));
201
+ out.append(wikiOneDocCard(d.refs));
202
+ out.append(graphCard(d.graph, body));
203
+ if (d.usage && d.usage.rows) out.append(wikiUsageCard(d.usage, body));
204
+
205
+ const w = d.wiki;
206
+ const wc = card(t("knowledge.wiki"), wikiActions(body, w));
207
+ if (!w.state || w.state === "none") {
208
+ wc.append(empty(t("knowledge.wiki.none"), t("knowledge.wiki.noneHint")));
209
+ } else if (w.state !== "registered") {
210
+ wc.append(el("div", "note", t("knowledge.wiki.unregistered", { state: String(w.state).toUpperCase() })));
211
+ wc.append(el("div", "note", t("knowledge.wiki.syncHint")));
212
+ } else {
213
+ const tierChip = chip(w.tier || t("overview.tile.wikiUnknown"), w.tier === "FRESH" ? "ok" : w.tier === "AGING" ? "warn" : "bad", w.tier === "STALE");
214
+ const headRow = el("div", "row-actions");
215
+ headRow.append(tierChip);
216
+ // The per-doc split as a small stacked bar. The counts are the CLI's.
217
+ if (w.counts) {
218
+ const total = Math.max(1, w.counts.FRESH + w.counts.AGING + w.counts.STALE + (w.counts.unknown || 0));
219
+ const bar = el("div", "kn-tierbar");
220
+ for (const [tier, cls] of [["FRESH", "ok"], ["AGING", "warn"], ["STALE", "bad"], ["unknown", "idle"]]) {
221
+ const n = w.counts[tier] || 0;
222
+ if (!n) continue;
223
+ const seg = el("div", "kn-tierbar-seg kn-tier-" + cls);
224
+ seg.style.setProperty("--w", ((n / total) * 100).toFixed(2) + "%");
225
+ seg.title = `${tier} ${n}`;
226
+ bar.append(seg);
227
+ }
228
+ headRow.append(bar);
229
+ }
230
+ wc.append(headRow);
231
+ const rows = [
232
+ [t("knowledge.field.docs"), String(w.docs)],
233
+ [t("knowledge.field.lastScan"), w.last_scan],
234
+ ];
235
+ // THE DOC PINNING THE TIER, BY NAME. A hash is not a thing anybody can go
236
+ // and refresh — and until v0.49.1 the hash is all `--json` carried.
237
+ if (w.worst)
238
+ rows.push([t("knowledge.field.worst"), `${w.worst.file} (${w.worst.distance}c)`]);
239
+ rows.push(
240
+ [
241
+ t("knowledge.field.distance"),
242
+ w.distance === null ? t("knowledge.field.unmeasurable") : t("knowledge.field.distanceValue", { n: w.distance }),
243
+ ],
244
+ [t("knowledge.field.anchor"), w.anchor ? String(w.anchor).slice(0, 8) : ""],
245
+ // `wiki_fresh_max` / `wiki_aging_max` are config keys — the numbers are
246
+ // shown, the key names are not paraphrased.
247
+ [t("knowledge.field.edges"), w.edges ? `fresh < ${w.edges.freshMax}c · aging <= ${w.edges.agingMax}c` : ""],
248
+ [t("knowledge.field.tags"), w.crosslink_tags === undefined ? "" : String(w.crosslink_tags)],
249
+ [t("knowledge.field.blind"), w.blind ? String(w.blind) : "0"]
250
+ );
251
+ wc.append(kvList(rows));
252
+ // The reason text is the CLI's own sentence about a real doc — verbatim.
253
+ for (const r of w.reasons || []) wc.append(el("div", "note", t("knowledge.wiki.why", { reason: r })));
254
+ wc.append(el("div", "note", t("knowledge.wiki.freshNote")));
255
+ // The orientation doc is read FIRST by every consumer, and the panel used
256
+ // not to mention it exists.
257
+ if (w.orientation) {
258
+ if (w.orientation.present)
259
+ wc.append(el("div", "note ok", t("knowledge.orientation.present", { file: w.orientation.file })));
260
+ else {
261
+ wc.append(el("div", "note warn", t("knowledge.orientation.missing")));
262
+ wc.append(el("pre", "cmd", w.orientation.regenerate));
263
+ }
264
+ }
265
+ }
266
+ out.append(wc);
267
+
268
+ // THE DOC TABLE — the headline addition. A row EXPANDS IN PLACE (one at a
269
+ // time, detail fetched on first open); there is no detail box below the
270
+ // table, which is the Runs-row rule.
271
+ out.append(d.errors && d.errors.docs ? failBox(d.errors.docs) : wikiDocsCard(d.docs));
272
+
273
+ // impact, unchanged, moved onto this tab
274
+ const imp = d.impact;
275
+ if (imp && imp.ok) {
276
+ const c = card(t("knowledge.impact.title"));
277
+ const rec = el("div", "row-actions");
278
+ rec.append(chip(imp.recommendation, imp.recommendation === "CLEAN" ? "ok" : imp.recommendation === "DELTA" ? "info" : "warn"));
279
+ c.append(rec);
280
+ for (const r of imp.reasons || []) c.append(el("div", "note", r));
281
+ c.append(
282
+ el(
283
+ "div",
284
+ "note",
285
+ t("knowledge.impact.counts", {
286
+ registered: imp.registered,
287
+ touched: imp.touched,
288
+ structural: imp.structural,
289
+ pct: imp.affected_pct,
290
+ threshold: imp.threshold,
291
+ })
292
+ )
293
+ );
294
+ const scroll = el("div", "scroll-x");
295
+ const table = el("table");
296
+ const thead = el("thead");
297
+ const hr = el("tr");
298
+ for (const h of [t("knowledge.impact.col.doc"), t("knowledge.impact.col.state"), t("knowledge.impact.col.detail")])
299
+ hr.append(el("th", null, h));
300
+ thead.append(hr);
301
+ const tb = el("tbody");
302
+ for (const doc of imp.docs) {
303
+ const tr = el("tr");
304
+ tr.append(el("td", "mono", doc.file));
305
+ const st = el("td");
306
+ st.append(chip(doc.state, doc.state === "CLEAN" ? "ok" : doc.state === "TOUCHED" ? "info" : "warn"));
307
+ tr.append(st);
308
+ tr.append(el("td", "note", doc.gone.length ? "gone: " + doc.gone.join(", ") : doc.hits.slice(0, 4).join(", ")));
309
+ tb.append(tr);
310
+ }
311
+ table.append(thead, tb);
312
+ scroll.append(table);
313
+ c.append(scroll);
314
+ out.append(c);
315
+ } else if (imp && !imp.ok) {
316
+ const c = card(t("knowledge.impact.title"));
317
+ c.append(el("div", "note", imp.hint || `unavailable (${imp.reason})`));
318
+ out.append(c);
319
+ }
320
+ return out;
321
+ }
322
+
323
+ /* THE DOC TABLE. Ordered exactly as the CLI ordered it — THE PANEL NEVER
324
+ INVENTS A RANK (the `wiki plan` rule). A row expands IN PLACE. */
325
+ function wikiDocsCard(docs) {
326
+ const c = card(t("knowledge.docs.title"));
327
+ if (!docs || !docs.ok || !(docs.docs || []).length) {
328
+ c.append(empty(t("knowledge.docs.none"), (docs && docs.hint) || t("knowledge.docs.noneHint")));
329
+ return c;
330
+ }
331
+ c.append(el("div", "note", t("knowledge.docs.note")));
332
+ const scroll = el("div", "scroll-x");
333
+ const table = el("table");
334
+ const thead = el("thead");
335
+ const hr = el("tr");
336
+ for (const h of [
337
+ t("knowledge.docs.col.doc"),
338
+ t("knowledge.docs.col.tier"),
339
+ t("knowledge.docs.col.distance"),
340
+ t("knowledge.docs.col.covers"),
341
+ t("knowledge.docs.col.used"),
342
+ t("knowledge.docs.col.tags"),
343
+ ])
344
+ hr.append(el("th", null, h));
345
+ thead.append(hr);
346
+ const tb = el("tbody");
347
+ const open = { row: null };
348
+ for (const r of docs.docs) {
349
+ const tr = el("tr", "kn-doc-row" + (r.retire_hint ? " row-muted" : ""));
350
+ tr.tabIndex = 0;
351
+ const detail = el("tr", "kn-doc-detail");
352
+ const dtd = el("td");
353
+ dtd.setAttribute("colspan", "6");
354
+ detail.append(dtd);
355
+ detail.hidden = true;
356
+ let loaded = false;
357
+ const toggle = () => {
358
+ const willOpen = detail.hidden;
359
+ // One row open at a time — the Runs-row rule.
360
+ if (open.row && open.row !== detail) {
361
+ open.row.hidden = true;
362
+ open.row.previousSibling.classList.remove("open");
363
+ }
364
+ detail.hidden = !willOpen;
365
+ tr.classList.toggle("open", willOpen);
366
+ open.row = willOpen ? detail : null;
367
+ if (willOpen && !loaded) {
368
+ loaded = true;
369
+ loadWikiDoc(dtd, r.file);
370
+ }
371
+ };
372
+ tr.addEventListener("click", toggle);
373
+ tr.addEventListener("keydown", (e) => {
374
+ if (e.key === "Enter" || e.key === " ") {
375
+ e.preventDefault();
376
+ toggle();
377
+ }
378
+ });
379
+ tr.append(el("td", "mono", r.file.replace(/^wiki\//, "")));
380
+ const tc = el("td");
381
+ tc.append(chip(r.tier, r.tier === "FRESH" ? "ok" : r.tier === "AGING" ? "warn" : r.tier === "STALE" ? "bad" : ""));
382
+ tr.append(tc);
383
+ tr.append(el("td", "num", r.distance === null ? "?" : r.distance + "c"));
384
+ tr.append(el("td", "note", (r.covers || []).join(", ")));
385
+ // `used: null` is NOT zero-use — unknown must never be reported as dead.
386
+ tr.append(el("td", "num", r.used === null ? "?" : `${r.used}/${r.used_of}`));
387
+ tr.append(el("td", "num", String(r.crosslink_tags || 0)));
388
+ tb.append(tr, detail);
389
+ }
390
+ table.append(thead, tb);
391
+ scroll.append(table);
392
+ c.append(scroll);
393
+ return c;
394
+ }
395
+
396
+ async function loadWikiDoc(host, file) {
397
+ host.replaceChildren(skeleton(2));
398
+ const r = await read("/api/wiki/show?doc=" + encodeURIComponent(file)).catch(() => ({ data: null }));
399
+ const d = r.data;
400
+ if (!d || !d.ok) {
401
+ host.replaceChildren(el("div", "note", (d && d.hint) || t("common.loadFail")));
402
+ return;
403
+ }
404
+ const out = frag();
405
+ out.append(
406
+ kvList([
407
+ [t("knowledge.docs.field.title"), d.title || ""],
408
+ [t("knowledge.docs.field.type"), d.doc_type || ""],
409
+ [t("knowledge.docs.field.scanned"), d.scanned_commit ? String(d.scanned_commit).slice(0, 8) : ""],
410
+ [t("knowledge.docs.field.covers"), (d.covers || []).join(", ")],
411
+ [t("knowledge.docs.field.coveredFiles"), String(d.covered_files)],
412
+ [t("knowledge.docs.field.lastUsed"), d.last_used || t("knowledge.usage.neverUsed")],
413
+ ])
414
+ );
415
+ if ((d.tags || []).length) {
416
+ const tl = el("div", "row-actions");
417
+ for (const tag of d.tags) tl.append(chip(tag.tag, "info"));
418
+ out.append(tl);
419
+ }
420
+ // Free repairs FIRST, as everywhere else on this panel.
421
+ for (const rep of d.free_repairs || []) {
422
+ const row = el("div", "free-row");
423
+ row.append(chip(t("knowledge.plan.free"), "ok"));
424
+ row.append(el("span", null, rep.what));
425
+ row.append(el("code", "mono", rep.cmd));
426
+ out.append(row);
427
+ }
428
+ // REVEAL. The body is fetched only on an explicit request, one artifact at a
429
+ // time, and it is rendered as DOM — never as HTML.
430
+ const reveal = el("button", "btn btn-ghost btn-sm", t("knowledge.reveal"));
431
+ reveal.type = "button";
432
+ reveal.addEventListener("click", async () => {
433
+ reveal.disabled = true;
434
+ const b = await read("/api/wiki/show?doc=" + encodeURIComponent(file) + "&body=1").catch(() => ({ data: null }));
435
+ const box = el("div", "kn-body");
436
+ box.append(renderMd((b.data && b.data.body) || ""));
437
+ reveal.replaceWith(box);
438
+ });
439
+ out.append(reveal);
440
+ host.replaceChildren(out);
441
+ }
442
+
443
+ /* ── TAB 2 — COVERAGE ────────────────────────────────────────────────────── */
444
+ function knCoverageTab(d) {
445
+ const out = frag();
446
+ const cov = d.coverage;
447
+ const c = card(t("knowledge.coverage.title"));
448
+ // The read itself failed — a crash, a timeout, output the server could not
449
+ // parse. That is a BROKEN PANEL, not a repo without a wiki, and it must never
450
+ // again be reported as the latter.
451
+ if (d.errors && d.errors.coverage) {
452
+ c.append(failBox(d.errors.coverage));
453
+ out.append(c);
454
+ return out;
455
+ }
456
+ if (!cov || !cov.ok) {
457
+ c.append(empty(t("knowledge.coverage.na"), (cov && cov.hint) || t("knowledge.coverage.naHint")));
458
+ out.append(c);
459
+ return out;
460
+ }
461
+ const big = el("div", "kn-coverage");
462
+ big.append(el("span", "kn-coverage-num", cov.coverage_pct + "%"));
463
+ big.append(el("span", "note", t("knowledge.coverage.sub", { covered: cov.covered, tracked: cov.tracked })));
464
+ c.append(big);
465
+ const bar = el("div", "kn-tierbar");
466
+ const seg = el("div", "kn-tierbar-seg kn-tier-ok");
467
+ seg.style.setProperty("--w", cov.coverage_pct + "%");
468
+ bar.append(seg);
469
+ const rest = el("div", "kn-tierbar-seg kn-tier-idle");
470
+ rest.style.setProperty("--w", 100 - cov.coverage_pct + "%");
471
+ bar.append(rest);
472
+ c.append(bar);
473
+ // NOT OPTIONAL CHROME. There is no threshold, no config key, and nothing
474
+ // branches on this number — a coverage percentage that starts nagging becomes
475
+ // a number people game.
476
+ c.append(el("div", "note", t("knowledge.coverage.notATarget")));
477
+ out.append(c);
478
+
479
+ if ((cov.uncovered_dirs || []).length) {
480
+ const uc = card(t("knowledge.coverage.uncovered"));
481
+ uc.append(el("div", "note", t("knowledge.coverage.uncoveredNote")));
482
+ const scroll = el("div", "scroll-x");
483
+ const table = el("table");
484
+ const thead = el("thead");
485
+ const hr = el("tr");
486
+ for (const h of [t("knowledge.coverage.col.dir"), t("knowledge.coverage.col.files"), t("knowledge.coverage.col.last")])
487
+ hr.append(el("th", null, h));
488
+ thead.append(hr);
489
+ const tb = el("tbody");
490
+ for (const row of cov.uncovered_dirs) {
491
+ const tr = el("tr");
492
+ tr.append(el("td", "mono", row.dir));
493
+ tr.append(el("td", "num", String(row.files)));
494
+ tr.append(el("td", "note", row.newest_commit || ""));
495
+ tb.append(tr);
496
+ }
497
+ table.append(thead, tb);
498
+ scroll.append(table);
499
+ uc.append(scroll);
500
+ out.append(uc);
501
+ }
502
+
503
+ // THE STRUCTURAL BLIND SPOT AS THE FILE LIST IT ALWAYS WAS — no longer as the
504
+ // number `2`, which tells a user nothing they can act on.
505
+ const blind = (d.wiki && d.wiki.blind_spot) || (d.impact && d.impact.blind_spot) || [];
506
+ const bc = card(t("knowledge.blind.title"));
507
+ if (!blind.length) {
508
+ bc.append(el("div", "note ok", t("knowledge.blind.none")));
509
+ } else {
510
+ bc.append(el("div", "note", t("knowledge.blind.note")));
511
+ const fl = el("div", "file-list");
512
+ for (const f of blind) fl.append(el("div", null, f));
513
+ bc.append(fl);
514
+ }
515
+ out.append(bc);
516
+ return out;
517
+ }
518
+
519
+ /* ── TAB 3 — PATTERNS ────────────────────────────────────────────────────── */
520
+ function knPatternsTab(d) {
521
+ const out = frag();
522
+ const p = d.patterns || {};
523
+ const pc = card(t("knowledge.patterns.title"));
524
+ pc.append(el("div", "note", t("knowledge.patterns.note")));
525
+ if (!(p.patterns || []).length) {
526
+ pc.append(empty(t("knowledge.patterns.none"), t("knowledge.patterns.noneHint")));
527
+ } else {
528
+ for (const row of p.patterns) {
529
+ const box = el("div", "kn-pattern");
530
+ const hdr = el("div", "row-actions");
531
+ // The language KEY is the CLI's framework id (`react`, `nestjs`, …) — a
532
+ // translated one would resolve to no playbook.
533
+ hdr.append(el("span", "mono", row.lang));
534
+ hdr.append(el("span", "note", relAge(row.mtime_ms)));
535
+ box.append(hdr);
536
+ box.append(el("div", "note mono", row.path));
537
+ const more = el("button", "btn btn-ghost btn-sm", t("knowledge.patterns.inspect"));
538
+ more.type = "button";
539
+ const detail = el("div", "stack stack-sm");
540
+ more.addEventListener("click", async () => {
541
+ more.disabled = true;
542
+ await loadPattern(detail, row.lang);
543
+ more.remove();
544
+ });
545
+ box.append(more, detail);
546
+ pc.append(box);
547
+ }
548
+ }
549
+ // Known-but-uncached languages: PAID, so a command and never a button.
550
+ const cached = new Set((p.patterns || []).map((x) => x.lang));
551
+ const uncached = (p.known_languages || []).filter((l) => !cached.has(l));
552
+ if (uncached.length) {
553
+ pc.append(el("div", "note", t("knowledge.patterns.known", { list: uncached.join(", ") })));
554
+ for (const l of uncached.slice(0, 6)) pc.append(el("pre", "cmd", `/orc-pattern ${l}`));
555
+ pc.append(el("div", "note", t("knowledge.patterns.paidWhy")));
556
+ }
557
+ out.append(pc);
558
+ return out;
559
+ }
560
+
561
+ async function loadPattern(host, lang) {
562
+ host.replaceChildren(skeleton(2));
563
+ const r = await read("/api/pattern/show?lang=" + encodeURIComponent(lang)).catch(() => ({ data: null }));
564
+ const d = r.data;
565
+ if (!d || !d.ok) {
566
+ host.replaceChildren(el("div", "note", (d && d.hint) || t("common.loadFail")));
567
+ return;
568
+ }
569
+ const out = frag();
570
+ out.append(
571
+ kvList([
572
+ [t("knowledge.patterns.field.codified"), d.codified_at || "—"],
573
+ [t("knowledge.patterns.field.source"), d.source_commit ? String(d.source_commit).slice(0, 8) : "—"],
574
+ [t("knowledge.patterns.field.playbook"), d.playbook || "—"],
575
+ [t("knowledge.patterns.field.size"), `${d.lines} · ${d.bytes}`],
576
+ [t("knowledge.patterns.field.counts"), `${d.conventions} · ${d.invariants}`],
577
+ ])
578
+ );
579
+ // An unheadered file SAYS SO in one line. No date is ever derived from an
580
+ // mtime — that is the `/orc-pact` UNCHECKABLE rule.
581
+ if (!d.headered) out.append(el("div", "note warn", t("knowledge.patterns.unheadered")));
582
+ if ((d.headings || []).length) {
583
+ const hl = el("div", "row-actions");
584
+ for (const h of d.headings.slice(0, 20)) hl.append(chip(h, ""));
585
+ out.append(hl);
586
+ }
587
+ // CONFLICTS get their own block: they are the most decision-shaped thing in
588
+ // the file (the project does X, the invariant says Y) and until now they were
589
+ // invisible outside it.
590
+ if ((d.conflicts || []).length) {
591
+ const cb = el("div", "kn-conflicts");
592
+ cb.append(el("div", "kn-conflicts-head", t("knowledge.patterns.conflicts")));
593
+ for (const c of d.conflicts) cb.append(el("div", "note", c));
594
+ out.append(cb);
595
+ }
596
+ const reveal = el("button", "btn btn-ghost btn-sm", t("knowledge.reveal"));
597
+ reveal.type = "button";
598
+ reveal.addEventListener("click", async () => {
599
+ reveal.disabled = true;
600
+ const b = await read("/api/pattern/show?lang=" + encodeURIComponent(lang) + "&body=1").catch(() => ({ data: null }));
601
+ const box = el("div", "kn-body");
602
+ box.append(renderMd((b.data && b.data.body) || ""));
603
+ reveal.replaceWith(box);
604
+ });
605
+ out.append(reveal);
606
+ out.append(el("div", "note", t("knowledge.patterns.literalNote")));
607
+ host.replaceChildren(out);
608
+ }
609
+
610
+ /* ── TAB 4 — MEMORY (gotchas) ────────────────────────────────────────────── */
611
+ function knMemoryTab(d, body) {
612
+ const out = frag();
613
+ const g = d.gotchas || {};
614
+ const gc = card(t("knowledge.gotchas.title"));
615
+ gc.append(el("div", "note", t("knowledge.gotchas.note")));
616
+ if (!g.count) {
617
+ gc.append(empty(t("knowledge.gotchas.none"), t("knowledge.gotchas.noneHint")));
618
+ out.append(gc);
619
+ return out;
620
+ }
621
+ const scroll = el("div", "scroll-x");
622
+ const table = el("table");
623
+ const thead = el("thead");
624
+ const hr = el("tr");
625
+ // These are the gotcha record's own field names, printed by `orc gotcha
626
+ // list` — column headers stay in the file's vocabulary.
627
+ for (const h of ["Id", "Area", "Kind", "Hits", "Last seen", "Trigger"]) hr.append(el("th", null, h));
628
+ thead.append(hr);
629
+ const tb = el("tbody");
630
+ const open = { row: null };
631
+ for (const e of g.gotchas) {
632
+ const tr = el("tr", "kn-doc-row");
633
+ tr.tabIndex = 0;
634
+ const detail = el("tr", "kn-doc-detail");
635
+ const dtd = el("td");
636
+ dtd.setAttribute("colspan", "6");
637
+ detail.append(dtd);
638
+ detail.hidden = true;
639
+ // EVERY FIELD the CLI already emits. `gotchaStatus` has always sent
640
+ // `fields`; the panel rendered six columns and discarded the rest.
641
+ dtd.append(kvList(Object.keys(e.fields || {}).map((k) => [k, e.fields[k]])));
642
+ const toggle = () => {
643
+ const willOpen = detail.hidden;
644
+ if (open.row && open.row !== detail) {
645
+ open.row.hidden = true;
646
+ open.row.previousSibling.classList.remove("open");
647
+ }
648
+ detail.hidden = !willOpen;
649
+ tr.classList.toggle("open", willOpen);
650
+ open.row = willOpen ? detail : null;
651
+ };
652
+ tr.addEventListener("click", toggle);
653
+ tr.addEventListener("keydown", (ev) => {
654
+ if (ev.key === "Enter" || ev.key === " ") {
655
+ ev.preventDefault();
656
+ toggle();
657
+ }
658
+ });
659
+ tr.append(
660
+ el("td", "mono", e.id),
661
+ el("td", "mono", e.area),
662
+ el("td", null, e.kind),
663
+ el("td", null, String(e.hits)),
664
+ el("td", "note", e.last_seen || "?"),
665
+ el("td", "note", e.trigger || "")
666
+ );
667
+ tb.append(tr, detail);
668
+ }
669
+ table.append(thead, tb);
670
+ scroll.append(table);
671
+ gc.append(scroll);
672
+ gc.append(gotchaPruneBox(g, body));
673
+ out.append(gc);
674
+
675
+ // The archive is reachable and clearly labelled as recoverable: eviction is
676
+ // an ARCHIVE, never a delete.
677
+ const ac = card(t("knowledge.archive.title"));
678
+ const load = el("button", "btn btn-ghost btn-sm", t("knowledge.archive.load"));
679
+ load.type = "button";
680
+ load.addEventListener("click", async () => {
681
+ load.disabled = true;
682
+ const r = await read("/api/gotchas/archived").catch((e) => ({ data: null, error: e }));
683
+ const a = r.data;
684
+ const box = el("div", "stack stack-sm");
685
+ if (!a || !a.count) box.append(el("div", "note", t("knowledge.archive.empty")));
686
+ else for (const e of a.gotchas) box.append(el("div", "note mono", `${e.id} · ${e.area} · ${e.kind} · hits ${e.hits}`));
687
+ load.replaceWith(box);
688
+ });
689
+ ac.append(el("div", "note", t("knowledge.archive.note")));
690
+ ac.append(load);
691
+ out.append(ac);
692
+ return out;
693
+ }
694
+
695
+ /* PREVIEW-THEN-APPLY. The Apply button stays disabled until a preview has been
696
+ fetched, the exact command is on screen throughout, and THE PREVIEW NAMES
697
+ EVERY ENTRY that would be archived — a count is not consent. */
698
+ function gotchaPruneBox(g, body) {
699
+ const box = el("div", "kn-prune");
700
+ const cfgMax = g.gotchas_max;
701
+ box.append(el("div", "kn-prune-head", t("knowledge.gotchas.headroom", { n: g.count, max: cfgMax === undefined ? "?" : cfgMax })));
702
+ box.append(el("pre", "cmd", "orc gotcha prune"));
703
+ const acts = el("div", "row-actions");
704
+ const preview = el("button", "btn btn-sm", t("knowledge.gotchas.preview"));
705
+ preview.type = "button";
706
+ const apply = el("button", "btn btn-sm btn-primary", t("knowledge.gotchas.prune"));
707
+ apply.type = "button";
708
+ apply.disabled = true;
709
+ const list = el("div", "stack stack-sm");
710
+ preview.addEventListener("click", async () => {
711
+ const r = await read("/api/gotcha/prune/preview").catch((e) => ({ data: null, error: e }));
712
+ const d = r.data;
713
+ list.replaceChildren();
714
+ if (!d || !(d.would_archive || []).length) {
715
+ list.append(el("div", "note ok", t("knowledge.gotchas.pruneNone")));
716
+ apply.disabled = true;
717
+ return;
718
+ }
719
+ list.append(el("div", "note warn", t("knowledge.gotchas.pruneWould", { n: d.would_archive.length })));
720
+ for (const e of d.would_archive) list.append(el("div", "note mono", `${e.id} · ${e.area} · ${e.kind} · hits ${e.hits} · ${e.last_seen || "?"}`));
721
+ list.append(el("div", "note", d.honesty));
722
+ apply.disabled = false;
723
+ });
724
+ apply.addEventListener("click", async () => {
725
+ const r = await post("/api/gotcha/prune", {});
726
+ toast(r.command, r.ok ? "ok" : "bad", r.output);
727
+ renderKnowledge(body);
728
+ });
729
+ acts.append(preview, apply);
730
+ box.append(acts, list);
731
+ return box;
732
+ }
733
+
734
+ /* ── TAB 5 — PEERS ───────────────────────────────────────────────────────── */
735
+ /* COMPACT and READ-ONLY. It links to Crosslink and NEVER duplicates its editor:
736
+ one boundary, one picture. This tab exists because "what does ORC know?"
737
+ includes what it knows from next door, and that is where a user looks first. */
738
+ function knPeersTab(d) {
739
+ const out = frag();
740
+ const c = card(t("knowledge.peers.title"));
741
+ c.append(el("div", "note", t("knowledge.peers.note")));
742
+ const go = el("button", "btn btn-ghost btn-sm", t("knowledge.peers.open"));
743
+ go.type = "button";
744
+ go.addEventListener("click", () => {
745
+ location.hash = "#/crosslink";
746
+ });
747
+ c.append(go);
748
+ out.append(c);
749
+ renderPeers(c);
750
+ return out;
751
+ }
752
+
753
+ async function renderPeers(host) {
754
+ const r = await read("/api/crosslink").catch((e) => ({ data: null, error: e }));
755
+ const d = r.data;
756
+ if (!d) return;
757
+ if (!d.configured || !(d.nodes || []).length) {
758
+ host.append(el("div", "note", t("knowledge.peers.none")));
759
+ return;
760
+ }
761
+ const scroll = el("div", "scroll-x");
762
+ const table = el("table");
763
+ const thead = el("thead");
764
+ const hr = el("tr");
765
+ for (const h of [
766
+ t("knowledge.peers.col.repo"),
767
+ t("knowledge.peers.col.state"),
768
+ t("knowledge.peers.col.tier"),
769
+ t("knowledge.peers.col.tags"),
770
+ ])
771
+ hr.append(el("th", null, h));
772
+ thead.append(hr);
773
+ const tb = el("tbody");
774
+ for (const n of d.nodes) {
775
+ const prov = n.provider || {};
776
+ const tr = el("tr");
777
+ tr.append(el("td", "mono", n.name));
778
+ const st = el("td");
779
+ // Every word here is the CLI's own — the peer's state, and the peer's
780
+ // freshness tier as IT computed it. A peer's config is not ours to read
781
+ // (`crosslinkProviderInfo`), so nothing here is recomputed on this side.
782
+ st.append(chip(prov.state || "—", prov.state === "wiki" ? "ok" : "warn"));
783
+ tr.append(st);
784
+ const tc = el("td");
785
+ if (prov.tier) tc.append(chip(prov.tier, prov.tier === "FRESH" ? "ok" : prov.tier === "AGING" ? "warn" : "bad"));
786
+ else tc.append(el("span", "note", "—"));
787
+ tr.append(tc);
788
+ tr.append(el("td", "num", prov.tags === undefined ? "—" : String(prov.tags)));
789
+ tb.append(tr);
790
+ }
791
+ table.append(thead, tb);
792
+ scroll.append(table);
793
+ host.append(scroll);
794
+ }
795
+
796
+ /* PART B MADE VISIBLE (v0.46.0).
797
+
798
+ THE PANEL MUST NEVER COMPUTE THE ORDER, THE TIER OR THE ESTIMATE ITSELF — it
799
+ renders `orc wiki plan --json`'s rows in the order they arrive and nothing
800
+ else, the same rule the Flow stepper lives under. A second idea of "which doc
801
+ matters most" is exactly the drift this panel exists to make impossible.
802
+
803
+ And: a `used 0/20` row KEEPS ITS SLOT, rendered muted with a retire hint.
804
+ Filtering it out would make "nobody reads this" and "this does not exist"
805
+ look identical — the same rule as an OFF phase in the stepper. */
806
+ function wikiPlanCard(plan, debt, body) {
807
+ const c = card(t("knowledge.plan"), wikiPlanActions(body, plan));
808
+
809
+ if (!plan || !plan.ok) {
810
+ c.append(empty(t("knowledge.plan.na"), t("knowledge.plan.naHint")));
811
+ return c;
812
+ }
813
+
814
+ // The debt line first: the habit this whole workstream is aiming at.
815
+ if (debt && debt.ok && debt.pending) {
816
+ const chips = el("div", "row-actions");
817
+ chips.append(chip(tn(debt.pending, "knowledge.debt.pending"), "warn"));
818
+ if (debt.tokens) chips.append(chip(kTokUi(debt.tokens.input + debt.tokens.cache_write + debt.tokens.cache_read + debt.tokens.output), null));
819
+ if (debt.usd !== null && debt.usd !== undefined) chips.append(chip("$" + debt.usd.toFixed(2), null));
820
+ if (debt.oldest_commits_behind !== null) chips.append(chip(tn(debt.oldest_commits_behind, "knowledge.debt.oldest"), null));
821
+ c.append(chips);
822
+ c.append(el("div", "note", t("knowledge.debt.nothingBroken")));
823
+ } else if (debt && debt.ok) {
824
+ c.append(el("div", "note ok", t("knowledge.debt.none")));
825
+ }
826
+
827
+ if (!plan.rows || !plan.rows.length) {
828
+ c.append(el("div", "note ok", t("knowledge.plan.clean")));
829
+ return c;
830
+ }
831
+
832
+ // FREE REPAIRS FIRST — a user must never be able to pay for something a free
833
+ // step would have fixed, so they render ABOVE the priced table.
834
+ if (plan.free_repairs && plan.free_repairs.length) {
835
+ const box = el("div", "free-box");
836
+ box.append(el("div", "free-head", t("knowledge.plan.freeFirst")));
837
+ for (const r of plan.free_repairs) {
838
+ const row = el("div", "free-row");
839
+ row.append(chip(t("knowledge.plan.free"), "ok"));
840
+ row.append(el("span", null, r.what));
841
+ row.append(el("code", "mono", r.cmd));
842
+ box.append(row);
843
+ }
844
+ c.append(box);
845
+ }
846
+
847
+ const tbl = el("table", "tbl");
848
+ const th = el("tr");
849
+ for (const h of ["knowledge.plan.col.doc", "knowledge.plan.col.state", "knowledge.plan.col.delta", "knowledge.plan.col.used", "knowledge.plan.col.tier", "knowledge.plan.col.tokens", "knowledge.plan.col.usd"])
850
+ th.append(el("th", null, t(h)));
851
+ tbl.append(th);
852
+ for (const r of plan.rows) {
853
+ const tr = el("tr", r.retire_hint ? "row-muted" : null);
854
+ tr.append(el("td", "mono", r.doc.replace(/^wiki\//, "")));
855
+ // The CLI's exact state words. Never a friendlier synonym.
856
+ const stateCell = el("td");
857
+ stateCell.append(chip(r.state, r.state === "STRUCTURAL" ? "bad" : "warn"));
858
+ tr.append(stateCell);
859
+ tr.append(el("td", "num", r.state === "STRUCTURAL" ? "—" : String(r.delta)));
860
+ tr.append(el("td", "num", r.used === null ? "?" : `${r.used}/${r.used_of}`));
861
+ tr.append(el("td", null, r.tier));
862
+ const est = r.estimate;
863
+ tr.append(el("td", "num", est ? kTokUi(est.p50.input + est.p50.cache_write + est.p50.cache_read + est.p50.output) : "—"));
864
+ // The dollar figure is the CLI's — the panel never prices anything itself,
865
+ // and a row the CLI could not price shows an em dash rather than a guess.
866
+ tr.append(el("td", "num", r.usd === null || r.usd === undefined ? "—" : "$" + r.usd.toFixed(2)));
867
+ tbl.append(tr);
868
+ if (r.state === "STRUCTURAL" && r.gone && r.gone.length) {
869
+ const note = el("tr", "row-note");
870
+ const td = el("td", null, t("knowledge.plan.gone", { files: r.gone.slice(0, 3).join(", ") }));
871
+ td.setAttribute("colspan", "7");
872
+ note.append(td);
873
+ tbl.append(note);
874
+ }
875
+ if (r.retire_hint) {
876
+ const note = el("tr", "row-note");
877
+ const td = el("td", null, t("knowledge.plan.retireHint", { n: r.used_of }));
878
+ td.setAttribute("colspan", "7");
879
+ note.append(td);
880
+ tbl.append(note);
881
+ }
882
+ }
883
+ c.append(tbl);
884
+ if (plan.estimate_unavailable) c.append(el("div", "note", t("knowledge.plan.noEstimate")));
885
+ c.append(el("div", "note", t("knowledge.plan.tierNote", { mode: plan.scan_tier_mode, deep: plan.deep, light: plan.light })));
886
+ // A refresh COSTS MONEY, so it is a command, never a button.
887
+ c.append(laneCommand(`/orc-wiki refresh --top ${Math.min(2, plan.rows.length)}`, t("knowledge.plan.refreshWhy")));
888
+ return c;
889
+ }
890
+
891
+ function wikiPlanActions(body, plan) {
892
+ const wrap = el("div", "row-actions");
893
+ // `orc wiki sync` is FREE ($0.00), so it gets a button.
894
+ const s = el("button", "btn btn-sm", t("knowledge.syncFree"));
895
+ s.type = "button";
896
+ s.addEventListener("click", async () => {
897
+ const r = await post("/api/wiki/sync", {});
898
+ toast(r.ok ? t("knowledge.syncOk") : t("common.writeFail"), r.ok ? "ok" : "bad", r.output);
899
+ renderKnowledge(body);
900
+ });
901
+ wrap.append(s);
902
+ void plan;
903
+ return wrap;
904
+ }
905
+
906
+ function wikiUsageCard(u, body) {
907
+ const c = card(t("knowledge.usage"), (() => {
908
+ const wrap = el("div", "row-actions");
909
+ const b = el("button", "btn btn-ghost btn-sm", t("knowledge.usage.rebuild"));
910
+ b.type = "button";
911
+ b.addEventListener("click", async () => {
912
+ const r = await post("/api/wiki/usage/rebuild", {});
913
+ toast(r.ok ? t("knowledge.usage.rebuilt") : t("common.writeFail"), r.ok ? "ok" : "bad", r.output);
914
+ renderKnowledge(body);
915
+ });
916
+ wrap.append(b);
917
+ return wrap;
918
+ })());
919
+ const chips = el("div", "row-actions");
920
+ chips.append(chip(t("knowledge.usage.registered", { n: u.registered }), null));
921
+ chips.append(chip(t("knowledge.usage.active", { n: u.in_active_use }), "ok"));
922
+ if (u.never_used) chips.append(chip(t("knowledge.usage.never", { n: u.never_used, runs: u.window_runs }), "warn"));
923
+ c.append(chips);
924
+ const body2 = el("div", "usage-rows");
925
+ for (const r of u.rows) {
926
+ const row = el("div", "usage-row" + (r.used ? "" : " row-muted"));
927
+ row.append(el("span", "mono", r.doc.replace(/^wiki\//, "")));
928
+ const track = el("div", "bar-track");
929
+ const fill = el("div", "bar-fill");
930
+ track.append(fill);
931
+ requestAnimationFrame(() => fill.style.setProperty("width", Math.max(2, (r.used / (r.of || 1)) * 100) + "%"));
932
+ row.append(track);
933
+ row.append(el("span", "bar-value", `${r.used}/${r.of}`));
934
+ row.append(el("span", "note", r.last_used || t("knowledge.usage.neverUsed")));
935
+ body2.append(row);
936
+ }
937
+ c.append(body2);
938
+ c.append(el("div", "note", t("knowledge.usage.note")));
939
+ return c;
940
+ }
941
+
942
+ /* ONE DOC AT A TIME (v1.7.0).
943
+ Two halves of the same question, and neither of them scans.
944
+
945
+ The RESOLVER answers "is this topic already in the wiki?" — free, from the
946
+ doc headers. It is a probe, so the panel runs it; the refresh or the add that
947
+ follows is a LANE, so the panel prints the command and stops. That is the
948
+ panel's standing line, visible here rather than implied: a free action gets a
949
+ button, a paid one gets a command you can copy.
950
+
951
+ The SWEEP is `orc wiki refs --check`. It exists because after ONE doc changes,
952
+ the surfaces DERIVED from the doc set are behind it — and on a small run
953
+ nobody remembers to look. `--check`, always: opening a panel must never write
954
+ to a repo. */
955
+ const REFS_STATE_KIND = { clean: "ok", repaired: "ok", absent: "", behind: "warn", outstanding: "warn", missing: "warn", drifted: "bad", dead: "warn" };
956
+
957
+ function wikiOneDocCard(refs) {
958
+ const c = card(t("knowledge.one.title"));
959
+ c.append(el("div", "note", t("knowledge.one.lead")));
960
+
961
+ // ── the resolver ──────────────────────────────────────────────────────────
962
+ const row = el("div", "row-actions");
963
+ const input = el("input", "text-input kn-one-topic");
964
+ input.type = "text";
965
+ input.placeholder = t("knowledge.one.placeholder");
966
+ input.setAttribute("aria-label", t("knowledge.one.placeholder"));
967
+ const go = el("button", "btn btn-sm btn-primary", t("knowledge.one.resolve"));
968
+ go.type = "button";
969
+ row.append(input, go);
970
+ c.append(row);
971
+
972
+ const out = el("div", "kn-one-out");
973
+ c.append(out);
974
+
975
+ const resolve = async () => {
976
+ const topic = input.value.trim();
977
+ if (!topic) return;
978
+ go.disabled = true;
979
+ out.replaceChildren(skeleton(1));
980
+ try {
981
+ const r = await read("/api/wiki/resolve?topic=" + encodeURIComponent(topic));
982
+ const d = r.data || {};
983
+ out.replaceChildren();
984
+ if (!d.ok) {
985
+ out.append(el("div", "note warn", d.hint || t("knowledge.one.cannot")));
986
+ return;
987
+ }
988
+ const head = el("div", "row-actions");
989
+ // The CLI's own verdict word, never a friendlier synonym.
990
+ head.append(chip(d.verdict, d.verdict === "match" ? "ok" : d.verdict === "ambiguous" ? "warn" : ""));
991
+ if (d.verdict === "match") head.append(el("span", "mono", d.match.file));
992
+ out.append(head);
993
+
994
+ if (d.verdict === "match") out.append(el("div", "note", d.match.why.slice(0, 4).join(" · ")));
995
+ // AMBIGUOUS is a question, not a guess: every candidate is listed with its
996
+ // score, and no command is offered until a human picks one.
997
+ if (d.verdict === "ambiguous")
998
+ for (const cand of d.candidates || []) {
999
+ const cr = el("div", "kn-one-cand");
1000
+ cr.append(el("span", "note mono", String(cand.score)));
1001
+ cr.append(el("span", "mono", cand.file));
1002
+ cr.append(el("span", "note", (cand.why || []).slice(0, 3).join(" · ")));
1003
+ out.append(cr);
1004
+ }
1005
+ if (d.verdict === "new" && (d.suggested_covers || []).length) {
1006
+ out.append(el("div", "note", t("knowledge.one.proposed")));
1007
+ for (const s of d.suggested_covers) {
1008
+ const sr = el("div", "kn-one-cand");
1009
+ sr.append(el("span", "mono", s.glob));
1010
+ sr.append(el("span", "note", tn(s.matches, "knowledge.one.files")));
1011
+ out.append(sr);
1012
+ }
1013
+ // The CLI's own caveat, relayed. A proposal that arrives without it
1014
+ // reads as an answer.
1015
+ out.append(el("div", "note", d.note));
1016
+ }
1017
+ // A LANE costs money, so it is a command to copy, never a button.
1018
+ out.append(laneCommand(d.next, t("knowledge.one.next")));
1019
+ } catch (e) {
1020
+ out.replaceChildren(failBox(e));
1021
+ } finally {
1022
+ go.disabled = false;
1023
+ }
1024
+ };
1025
+ go.addEventListener("click", resolve);
1026
+ input.addEventListener("keydown", (ev) => {
1027
+ if (ev.key === "Enter") resolve();
1028
+ });
1029
+
1030
+ // ── the sweep ─────────────────────────────────────────────────────────────
1031
+ if (refs && refs.items) {
1032
+ const sweep = el("div", "kn-one-sweep");
1033
+ sweep.append(el("div", "checklist-head", t("knowledge.one.sweep")));
1034
+ for (const i of refs.items) {
1035
+ const ir = el("div", "kn-one-ref");
1036
+ ir.append(chip(i.state, REFS_STATE_KIND[i.state] || ""));
1037
+ ir.append(el("span", "mono", i.id));
1038
+ ir.append(el("span", "note", i.what));
1039
+ sweep.append(ir);
1040
+ if (i.fix) sweep.append(el("div", "note kn-one-fix", "→ " + i.fix + " (" + i.cost + ")"));
1041
+ }
1042
+ // WHY only one surface is ever repaired. Relayed from the CLI, because a
1043
+ // sweep that looked like it fixed everything would be the more dangerous
1044
+ // reading.
1045
+ sweep.append(el("div", "note", refs.note));
1046
+ c.append(sweep);
1047
+ } else if (refs && refs.hint) {
1048
+ c.append(el("div", "note", refs.hint));
1049
+ }
1050
+ return c;
1051
+ }
1052
+
1053
+ function wikiActions(body, w) {
1054
+ if (!w || !w.state || w.state === "none") return null;
1055
+ const wrap = el("div", "row-actions");
1056
+ const sync = el("button", "btn btn-sm", "orc wiki sync");
1057
+ sync.type = "button";
1058
+ sync.addEventListener("click", async () => {
1059
+ const r = await post("/api/wiki/sync", {});
1060
+ toast(r.command, r.ok ? "ok" : "bad", r.output);
1061
+ renderKnowledge(body);
1062
+ });
1063
+ wrap.append(sync);
1064
+ return wrap;
1065
+ }