@azure-id/orc 1.9.0 → 1.9.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 (48) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README-id.md +57 -58
  3. package/README.md +51 -53
  4. package/bin/cli.js +227 -25
  5. package/bin/graph-audit.js +235 -0
  6. package/bin/graph-extract.js +308 -3
  7. package/bin/graph-gain.js +23 -3
  8. package/bin/graph-map.js +9 -1
  9. package/bin/graph-query.js +152 -10
  10. package/bin/graph-shard.js +6 -1
  11. package/bin/graph-signals.js +287 -4
  12. package/bin/graph.js +717 -658
  13. package/bin/verify-contracts.js +115 -0
  14. package/bin/verify-package.js +4 -0
  15. package/bin/webui/fixtures/knowledge.js +21 -4
  16. package/bin/webui/i18n/en/knowledge.json +5 -2
  17. package/bin/webui/i18n/id/knowledge.json +5 -2
  18. package/bin/webui/js/panels/knowledge.js +21 -2
  19. package/package.json +1 -1
  20. package/templates/agents/orc-executor-haiku-4-5.md +2 -1
  21. package/templates/agents/orc-executor-opus-4-7-high.md +2 -1
  22. package/templates/agents/orc-executor-opus-4-7-med.md +2 -1
  23. package/templates/agents/orc-executor-opus-4-8-high.md +2 -1
  24. package/templates/agents/orc-executor-opus-5-high.md +2 -1
  25. package/templates/agents/orc-executor-opus-5-low.md +2 -1
  26. package/templates/agents/orc-executor-opus-5-med.md +2 -1
  27. package/templates/agents/orc-executor-sonnet-4-6-high.md +2 -1
  28. package/templates/agents/orc-executor-sonnet-4-6-med.md +2 -1
  29. package/templates/agents/orc-executor-sonnet-5-high.md +2 -1
  30. package/templates/agents/orc-recon-opus-5-low.md +15 -5
  31. package/templates/agents/orc-recon-sonnet-4-6-med.md +15 -5
  32. package/templates/hooks/README.md +11 -1
  33. package/templates/hooks/orc-graph-hook.js +53 -7
  34. package/templates/skills/_shared/code-graph.md +56 -17
  35. package/templates/skills/_shared/phases/execution.md +4 -4
  36. package/templates/skills/_shared/phases/planning.md +3 -3
  37. package/templates/skills/_shared/phases/preflight.md +1 -1
  38. package/templates/skills/_shared/phases/review.md +3 -1
  39. package/templates/skills/_shared/phases/ship.md +1 -1
  40. package/templates/skills/_shared/phases/trace.md +2 -1
  41. package/templates/skills/_shared/read-ladder.md +14 -1
  42. package/templates/skills/orc/SKILL.md +1 -1
  43. package/templates/skills/orc-fast/SKILL.md +3 -3
  44. package/templates/skills/orc-mini/SKILL.md +13 -17
  45. package/templates/skills/orc-mini/references/complexity.md +25 -11
  46. package/templates/skills/orc-quick/SKILL.md +12 -13
  47. package/templates/skills/orc-quick/references/gh-mode.md +1 -1
  48. package/templates/skills/orc-quick/references/look.md +5 -5
package/bin/cli.js CHANGED
@@ -180,8 +180,10 @@ function writeStdoutSync(str) {
180
180
  }
181
181
 
182
182
  // Print the object and (optionally) exit with the human path's code.
183
- function emitJson(obj, exitCode) {
184
- writeStdoutSync(JSON.stringify(obj, null, 2) + "\n");
183
+ // `compact` (v1.9.1 B1) writes the answer with no indentation: a `--brief`
184
+ // answer is read by a model, and the card inside it carries its own newlines.
185
+ function emitJson(obj, exitCode, compact) {
186
+ writeStdoutSync((compact ? JSON.stringify(obj) : JSON.stringify(obj, null, 2)) + "\n");
185
187
  if (exitCode !== undefined) process.exit(exitCode);
186
188
  }
187
189
 
@@ -3182,9 +3184,10 @@ function laneAnnounce(lane, map, claudeDir, families) {
3182
3184
  const graphOn = String(Object.prototype.hasOwnProperty.call(map, "code_graph") ? map.code_graph : metaFor("code_graph").def) === "on";
3183
3185
  if (graphOn && LANE_CALLS["graph-status"].lanes.includes(lane))
3184
3186
  out.push(
3185
- "graph: on — use the code-graph cache: (1) now: `orc graph status --if-enabled --heal --json` (builds or updates it); " +
3186
- "(2) every code-writing slice: `orc graph ctx <declared files> --if-enabled --json` cards; " +
3187
- "(3) after every code change: `orc graph update --if-enabled --json` (keeps it for the next run). Copy each `line` and `trace` verbatim"
3187
+ "graph: on — use the code-graph cache: (1) now: `orc graph status --if-enabled --heal --json --brief` (builds or updates it); " +
3188
+ "(2) every code-writing slice: `orc graph ctx <declared files> --if-enabled --json --brief` cards; " +
3189
+ "(3) after every code change: `orc graph update --if-enabled --json --brief` (keeps it for the next run). " +
3190
+ "Copy each `line` and `trace` verbatim; `--brief` drops the rows you never print"
3188
3191
  );
3189
3192
  // A shadowed setting must never be silent, and neither must an inert one.
3190
3193
  const shadowed = [];
@@ -3480,7 +3483,24 @@ const LANE_CALLS = {
3480
3483
  on_absent: "exit 4 is an ANSWER, not a miss — plan exactly as before",
3481
3484
  canonical: "_shared/code-graph.md",
3482
3485
  never: "never read a co-change as a DEPENDENCY — it says what people changed together, never what needs what",
3483
- lanes: ["orc", "orc-diy", "orc-mini"],
3486
+ // v1.9.1: mini no longer calls it. `impact --complexity` answers the same
3487
+ // question inside the SAME process, against the same per-HEAD cache.
3488
+ lanes: ["orc", "orc-diy"],
3489
+ },
3490
+ // v1.9.1 A2 — a USER command. `lanes: []` is the contract: nothing a lane
3491
+ // does is made better by an audit, and a lane that ran one would pay for an
3492
+ // answer about the PARSER in a context that is about the code.
3493
+ "graph-audit": {
3494
+ cmd: "orc graph audit [--top=N] --if-enabled [--json]",
3495
+ what: "WHY the map is thin — density by language, which extractor read each one, the files the parser read as EMPTY longest first, and what their first declaration looks like",
3496
+ exits: { 0: "answered", 1: "no index yet", 3: "off — `code_graph` is off" },
3497
+ states: null,
3498
+ cost: "free",
3499
+ when: "by hand, after `orc graph status` says THIN — never in a lane",
3500
+ on_absent: "exit 1 means there is no graph to audit; build one first",
3501
+ canonical: "_shared/code-graph.md",
3502
+ never: "never read an empty file as a defect — a component with no named function is a file the parser read correctly; only a shape marked `parser gap` is worth an issue",
3503
+ lanes: [],
3484
3504
  },
3485
3505
  "graph-gain": {
3486
3506
  cmd: "orc graph gain --run <trace name> --if-enabled [--json]",
@@ -8742,6 +8762,14 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
8742
8762
  paid.source = K.tok(r.source.text);
8743
8763
  paid.card = Math.max(0, paid.card - paid.source);
8744
8764
  }
8765
+ // R1 (v1.9.1): the callers' blocks are source the same way — exactly the
8766
+ // lines a range read of each caller would have returned.
8767
+ const callerBlocks = Array.isArray(r.callers_source) ? r.callers_source : [];
8768
+ if (callerBlocks.length) {
8769
+ const t = callerBlocks.reduce((a, b) => a + K.tok(b.text || ""), 0);
8770
+ paid.source += t;
8771
+ paid.card = Math.max(0, paid.card - t);
8772
+ }
8745
8773
  const base = { cmd: sub, targets: operands.slice(0, 5), gen, ms, paid };
8746
8774
 
8747
8775
  if (sub === "impact" || (sub === "ctx" && forSlice)) {
@@ -8773,6 +8801,7 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
8773
8801
  const hits = Q.nameHits(model, r.target.qname);
8774
8802
  const c = K.ctxSymbol(model, r.target, hits, (r.callers || []).map((x) => x.file));
8775
8803
  if (r.source) c.avoided = addAvoided(c.avoided, K.sourceAvoided(model, r.source.file, r.source.from, r.source.to));
8804
+ for (const b of callerBlocks) c.avoided = addAvoided(c.avoided, K.sourceAvoided(model, b.file, b.from, b.to));
8776
8805
  return { ...base, ...c };
8777
8806
  }
8778
8807
  return { ...base, avoided: { low: 0, high: 0, calls_low: 0, calls_high: 0 }, basis: { files: [], grep_hits: 0 } };
@@ -8815,23 +8844,79 @@ function graphGainAppend(claudeDir, row) {
8815
8844
  // A thin router. The engine is `bin/graph.js`; this function resolves the
8816
8845
  // config (the engine never reads it) and owns the human lines. The exit code is
8817
8846
  // the same on the human path and the `--json` path (S7).
8847
+ // ── B1 (v1.9.1): the BRIEF answer ─────────────────────────────────────────
8848
+ // The orchestrator reads a graph answer on the main-session surface, where it
8849
+ // is re-sent on every later turn — and it prints the `card`, never the rows.
8850
+ // Measured on this tree the rows are 4 to 8 times the card. `--brief` keeps
8851
+ // every fact a lane prints and drops every row array it does not.
8852
+ //
8853
+ // Two rules, in this order:
8854
+ // 1. an array NAMED here is dropped (`calls[]` and `names[]` are strings, and
8855
+ // they are still rows);
8856
+ // 2. any other array OF OBJECTS is dropped, unless it IS the answer
8857
+ // (`nearest[]` and `candidates[]` on an exit-4; `cards[]` on a multi-target
8858
+ // `ctx`, whose rows are three scalars each).
8859
+ // Every drop leaves its COUNT behind, at the level it was dropped from, except
8860
+ // where the answer already carries that count under another name.
8861
+ const GRAPH_BRIEF_DROP = new Set(["callers", "calls", "effects", "symbols", "imports", "importers", "blocks", "names", "rows", "chain", "waiting"]);
8862
+ const GRAPH_BRIEF_KEEP = new Set(["nearest", "candidates", "cards"]);
8863
+ // V2/V3 answers that ARE the answer: `complexity` and `facts` are ten rows at
8864
+ // most, and they are the whole reason the call was made. They pass through
8865
+ // whole, never walked.
8866
+ const GRAPH_BRIEF_WHOLE = new Set(["complexity", "facts"]);
8867
+ const GRAPH_BRIEF_NO_COUNT = { impact: ["callers"], map: ["files"], changes: ["symbols"] };
8868
+
8869
+ function briefOf(sub, obj) {
8870
+ const noCount = new Set(GRAPH_BRIEF_NO_COUNT[sub] || []);
8871
+ const walk = (o) => {
8872
+ const out = {};
8873
+ const counts = {};
8874
+ for (const [k, v] of Object.entries(o)) {
8875
+ if (Array.isArray(v)) {
8876
+ const rows = GRAPH_BRIEF_DROP.has(k) || (!GRAPH_BRIEF_KEEP.has(k) && v.length > 0 && v[0] !== null && typeof v[0] === "object");
8877
+ if (!rows) out[k] = v;
8878
+ else if (!noCount.has(k)) counts[k] = v.length;
8879
+ continue;
8880
+ }
8881
+ if (k === "source" && v && typeof v === "object") {
8882
+ const from = Number(v.from);
8883
+ const to = Number(v.to);
8884
+ counts.source_lines = Number.isFinite(from) && Number.isFinite(to) ? Math.max(0, to - from + 1) : 0;
8885
+ continue;
8886
+ }
8887
+ out[k] = v && typeof v === "object" && !GRAPH_BRIEF_WHOLE.has(k) ? walk(v) : v;
8888
+ }
8889
+ if (Object.keys(counts).length) out.counts = { ...(out.counts || {}), ...counts };
8890
+ return out;
8891
+ };
8892
+ return walk(obj);
8893
+ }
8894
+
8818
8895
  function graphCmd() {
8819
8896
  const usage =
8820
- "Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc\n" +
8821
- " orc graph ctx <symbol|file[:line]>… [--source [N]] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
8822
- " orc graph coverage <file…> | changes [--base <ref>] | cochange <file>\n" +
8897
+ "Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc | audit [--top=N]\n" +
8898
+ " orc graph ctx <symbol|file[:line]>… [--source [N]] [--callers-source] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
8899
+ " orc graph coverage <file…> | changes [--base <ref>] [--files=<a,b>] | cochange <file>\n" +
8823
8900
  " orc graph map [--focus <file|name,…>] [--budget N]\n" +
8824
8901
  " orc graph gain [--run <name>] [--since 7d] [--history [--limit 40]] [--measured] [--reset --yes]\n" +
8825
8902
  " orc graph notes pending --files <a,b> [--cap 40] [--min 5] [--with-source] | notes apply <file|->\n" +
8826
- " [--if-enabled] [--depth N] [--budget TOKENS] [--format prose|tree] [--offset N] [--json] [--dir <path>]\n" +
8903
+ " [--if-enabled] [--depth N] [--budget TOKENS] [--format prose|tree] [--offset N] [--json] [--brief] [--dir <path>]\n" +
8904
+ " --brief with --json: the card, the line, the trace, every scalar and every count — and none of the row\n" +
8905
+ " arrays a lane never prints. Compact output. Without --brief, --json is unchanged.\n" +
8827
8906
  " status exit 0 FRESH · 1 NONE · 2 DRIFTED · 3 OFF (--heal builds or updates a NONE/DRIFTED graph first)\n" +
8828
8907
  " update exit 0 done · 1 unavailable or locked · 3 off (with --if-enabled)\n" +
8829
8908
  " --notes-pending adds the `notes pending` answer to the SAME call (one process, one lock)\n" +
8830
8909
  " gc exit 0 done · 1 no index or locked\n" +
8910
+ " audit exit 0 answered · 1 no index · 3 off (with --if-enabled) WHY the map is thin: density by language,\n" +
8911
+ " the files the parser read as EMPTY, longest first, and what their first declaration looks like\n" +
8831
8912
  " ctx exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 not found or ambiguous\n" +
8832
8913
  " --source [N] appends the target's own lines (default 80, cap 200), charged to the same budget\n" +
8914
+ " --callers-source appends 6 lines around each CONFIDENT call site (≤ 5, never the target's own\n" +
8915
+ " file), charged to the same budget after everything else; symbol cards only\n" +
8833
8916
  " --for-slice prints only the OUTSIDE view of each declared file — no symbol table\n" +
8834
8917
  " impact exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 none of the files is in the graph\n" +
8918
+ " --complexity [--risk=<class>[@<file:line>],…] adds the ONE line that says whether this change\n" +
8919
+ " fits one executor, with the numbers behind it — impact and co-change in ONE call\n" +
8835
8920
  " path exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 no confident path, or an unknown end\n" +
8836
8921
  " map exit 0 always when a graph exists · 1 no graph · 3 off (with --if-enabled) the files by RANK, most\n" +
8837
8922
  " connected first, each with its top symbols; --focus re-ranks the repository around those files or names\n" +
@@ -8852,6 +8937,16 @@ function graphCmd() {
8852
8937
  const root = repoRootOf(claudeDir);
8853
8938
  const sub = positionals()[1];
8854
8939
  const asJson = wantsJson();
8940
+ // B1: a BARE switch, read from `args`. `flag()` reads the next word as a
8941
+ // value, so `--brief` there would swallow the first operand.
8942
+ let briefOn = args.includes("--brief");
8943
+ // Every flag this release adds that carries a VALUE is written
8944
+ // `--name=value`, for the same reason: one word, so a CLI that does not
8945
+ // know the flag skips it whole instead of losing an operand behind it.
8946
+ const eqFlag = (name) => {
8947
+ const hit = args.find((a) => a.startsWith(name + "="));
8948
+ return hit === undefined ? undefined : hit.slice(name.length + 1);
8949
+ };
8855
8950
  const ovr = readOverride(claudeDir).map;
8856
8951
  const cfg = (k) => (Object.prototype.hasOwnProperty.call(ovr, k) ? ovr[k] : metaFor(k).def);
8857
8952
  const enabled = String(cfg("code_graph")) === "on";
@@ -8930,7 +9025,12 @@ function graphCmd() {
8930
9025
  return null;
8931
9026
  }
8932
9027
  };
8933
- const finish = (r, line, trace, jsonLine = true) => {
9028
+ // B3 (v1.9.1): the ledger append moved INSIDE `finish`, because the one
9029
+ // number the meter called EXACT — the card — was 4 to 8 times too small. The
9030
+ // ENVELOPE is everything the answer carries beyond its card, and it can only
9031
+ // be counted once the text that will be written exists. The append is still
9032
+ // fail-quiet and still happens after the answer is built, never before it.
9033
+ const finish = (r, line, trace, jsonLine = true, gainRow = null) => {
8934
9034
  const { exit, ...obj } = r;
8935
9035
  if (obj.generation === undefined) {
8936
9036
  const m = metaNow();
@@ -8939,7 +9039,19 @@ function graphCmd() {
8939
9039
  obj.gen_id = m.gen_id || null;
8940
9040
  }
8941
9041
  }
8942
- if (asJson) emitJson({ ...obj, ...(line && jsonLine ? { line } : {}), ...(trace ? { trace } : {}) }, exit);
9042
+ const full = { ...obj, ...(line && jsonLine ? { line } : {}), ...(trace ? { trace } : {}) };
9043
+ const payload = asJson && briefOn ? briefOf(sub, full) : full;
9044
+ if (gainRow) {
9045
+ try {
9046
+ const K = require("./graph-gain.js");
9047
+ const text = asJson ? (briefOn ? JSON.stringify(payload) : JSON.stringify(payload, null, 2)) : String(line || "");
9048
+ if (gainRow.paid) gainRow.paid.envelope = Math.max(0, K.tok(text) - K.tok(obj.card || line || ""));
9049
+ graphGainAppend(claudeDir, gainRow);
9050
+ } catch (_) {
9051
+ // K6: the meter is never allowed to fail the read it is measuring.
9052
+ }
9053
+ }
9054
+ if (asJson) emitJson(payload, exit, briefOn);
8943
9055
  if (line) console.log(line);
8944
9056
  process.exit(exit);
8945
9057
  };
@@ -8962,6 +9074,32 @@ function graphCmd() {
8962
9074
  r.auto_update = autoUpdate;
8963
9075
  r.notes = String(cfg("code_graph_notes"));
8964
9076
  if (healed) r.healed = healed;
9077
+ // A1 (v1.9.1): the density, and the BACKFILL for a store built before
9078
+ // this release. A FRESH store never updates, so waiting for the next real
9079
+ // update would leave a long-lived graph without the line forever. It runs
9080
+ // under `--heal` only, once, and it is bounded by the same cap every heal
9081
+ // obeys — the index did not change, so `generation` and `gen_id` do not.
9082
+ const SIGD = require("./graph-signals.js");
9083
+ let meta = metaNow();
9084
+ if (flag("--heal") === true && enabled && meta && !meta.density && r.exists) {
9085
+ const capMs = Number(cfg("code_graph_heal_ms")) || 0;
9086
+ if (!capMs || !meta.update_ms || meta.update_ms <= capMs) {
9087
+ try {
9088
+ const p = G.graphPaths(claudeDir);
9089
+ const idx = JSON.parse(fs.readFileSync(p.index, "utf8"));
9090
+ const fl = JSON.parse(fs.readFileSync(p.files, "utf8"));
9091
+ meta.density = G.densityOf(idx, fl);
9092
+ G.atomicWrite(p.meta, JSON.stringify(meta, null, 2) + "\n");
9093
+ r.density_backfilled = true;
9094
+ } catch (_) {
9095
+ meta = metaNow();
9096
+ }
9097
+ }
9098
+ }
9099
+ if (meta && meta.density) {
9100
+ r.density = meta.density;
9101
+ r.thin = SIGD.isThin(meta.density, r.files);
9102
+ }
8965
9103
  const b = r.behind || {};
8966
9104
  const lines = {
8967
9105
  off: "graph: off",
@@ -8975,6 +9113,12 @@ function graphCmd() {
8975
9113
  };
8976
9114
  let line = lines[r.state];
8977
9115
  let consult = r.state;
9116
+ // The density rides on the line a lane already prints, never on a line of
9117
+ // its own: a second line is a second thing to skip.
9118
+ const densityText = r.density
9119
+ ? ` · ${r.density.symbols_per_file} symbols/file` +
9120
+ (r.thin ? ` · THIN (${Math.round(r.density.zero_share * 100)} % of files have no symbol) — run: orc graph audit` : "")
9121
+ : "";
8978
9122
  if (healed && healed.parsed !== undefined) {
8979
9123
  const moved = healed.added + healed.changed + healed.deleted;
8980
9124
  const size = `${plural(r.files, "file")} · ${plural(r.symbols, "symbol")}`;
@@ -8986,7 +9130,15 @@ function graphCmd() {
8986
9130
  ? `graph: index upgraded to the new engine → updated (${healed.ms} ms) — ${size}`
8987
9131
  : `graph: ${plural(moved, "file")} changed outside ORC → updated (${healed.ms} ms) — ${size}`;
8988
9132
  } else if (healed) line += ` · could not heal: ${healed.reason}`;
8989
- return finish(r, line, consult === "off" ? "GRAPH-CONSULT off" : `GRAPH-CONSULT ${consult} :: files=${r.files} symbols=${r.symbols} gen=${r.generation}`);
9133
+ if (densityText && ["fresh", "drifted", "built", "updated"].includes(consult)) line += densityText;
9134
+ return finish(
9135
+ r,
9136
+ line,
9137
+ consult === "off"
9138
+ ? "GRAPH-CONSULT off"
9139
+ : `GRAPH-CONSULT ${consult} :: files=${r.files} symbols=${r.symbols} gen=${r.generation}` +
9140
+ (r.density ? ` density=${r.density.symbols_per_file}${r.thin ? " thin=1" : ""}` : "")
9141
+ );
8990
9142
  }
8991
9143
 
8992
9144
  if (sub === "update") {
@@ -9150,8 +9302,11 @@ function graphCmd() {
9150
9302
  const SIG = require("./graph-signals.js");
9151
9303
  if (sub === "changes") {
9152
9304
  const base = typeof flag("--base") === "string" ? flag("--base") : null;
9305
+ // V1: the lane hands the files it just wrote, and the rows for every
9306
+ // other file never leave this process.
9307
+ const onlyFiles = String(eqFlag("--files") || "").split(",").map((x) => x.trim()).filter(Boolean);
9153
9308
  const healed = healOnRead(SIG.changedPaths(root, base));
9154
- const r = SIG.graphChanges(claudeDir, root, { base });
9309
+ const r = SIG.graphChanges(claudeDir, root, { base, files: onlyFiles });
9155
9310
  if (healed) r.healed = healed;
9156
9311
  if (r.exit !== 0) return finish(r, r.reason === "no-index" ? "graph: none — no index yet (run: orc graph update)" : `graph: unavailable — ${r.reason}`);
9157
9312
  const c = r.counts;
@@ -9163,6 +9318,7 @@ function graphCmd() {
9163
9318
  : `graph changes vs ${r.base} — no indexed symbol was touched${r.not_in_graph.length ? ` (changed, but not in the graph: ${r.not_in_graph.join(", ")})` : ""}`;
9164
9319
  // K1/K2: `changes` avoids one Grep per touched symbol. The diff itself
9165
9320
  // is NOT counted — the reviewer reads it either way.
9321
+ let gainRow = null;
9166
9322
  if (r.symbols.length) {
9167
9323
  try {
9168
9324
  const K = require("./graph-gain.js");
@@ -9171,11 +9327,11 @@ function graphCmd() {
9171
9327
  if (m) {
9172
9328
  const hits = r.symbols.reduce((a, x) => a + Q.nameHits(m, x.qname), 0);
9173
9329
  const callerFiles = r.symbols.flatMap((x) => (x.callers || []).map((cc) => cc.file || cc));
9174
- graphGainAppend(claudeDir, { cmd: "changes", targets: [r.base], gen: r.generation, ms: r.ms || 0, paid: { card: K.tok(line), source: 0, hints: 0 }, ...K.changesCost(m, r.symbols.length, hits, callerFiles) });
9330
+ gainRow = { cmd: "changes", targets: [r.base], gen: r.generation, ms: r.ms || 0, paid: { card: K.tok(line), source: 0, hints: 0 }, ...K.changesCost(m, r.symbols.length, hits, callerFiles) };
9175
9331
  }
9176
9332
  } catch (_) {}
9177
9333
  }
9178
- return finish(r, line, `GRAPH-CHANGES ${r.symbols.length ? "found" : "none"} :: symbols=${r.symbols.length} high=${c.high} medium=${c.medium} low=${c.low} gen=${r.generation}`);
9334
+ return finish(r, line, `GRAPH-CHANGES ${r.symbols.length ? "found" : "none"} :: symbols=${r.symbols.length} high=${c.high} medium=${c.medium} low=${c.low} gen=${r.generation}`, true, gainRow);
9179
9335
  }
9180
9336
  const file = plain[2];
9181
9337
  if (!file) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
@@ -9190,6 +9346,18 @@ function graphCmd() {
9190
9346
  return finish(r, line, `GRAPH-COCHANGE ${r.rows.length ? "found" : "none"} :: rows=${r.rows.length} commits=${r.commits}`);
9191
9347
  }
9192
9348
 
9349
+ // A2 (v1.9.1) — `orc graph audit`. A THIN line says the map is thin; this
9350
+ // says WHY, and it is a USER command: no lane runs it (`LANE_CALLS` pins
9351
+ // `lanes: []`). It reads the index, the file table and the first lines of
9352
+ // each empty file. It never parses, never writes, and costs no model tokens.
9353
+ if (sub === "audit") {
9354
+ if (!enabled && flag("--if-enabled") === true) return finish({ ok: true, enabled: false, state: "off", exit: 3 }, "graph: off");
9355
+ const A = require("./graph-audit.js");
9356
+ const r = A.audit(claudeDir, root, { top: eqFlag("--top") });
9357
+ if (r.exit === 1) return finish(r, "graph: none — no index yet (run: orc graph update)");
9358
+ return finish(r, r.line, r.trace, false);
9359
+ }
9360
+
9193
9361
  if (sub === "gc") {
9194
9362
  const r = G.graphGc(claudeDir);
9195
9363
  const line =
@@ -9289,16 +9457,21 @@ function graphCmd() {
9289
9457
  if (flag("--history") !== undefined) {
9290
9458
  const r = K.history(claudeDir, { limit: flag("--limit") });
9291
9459
  if (!r.ok) return finish(r, "graph gain: no calls recorded yet");
9460
+ // `--history` IS its rows. Dropping them would leave an answer with
9461
+ // nothing in it, so the switch is refused here — out loud.
9462
+ const briefAsked = briefOn;
9463
+ briefOn = false;
9292
9464
  const line =
9293
9465
  `graph gain — last ${r.rows.length} of ${r.total} call(s), newest first\n` +
9294
9466
  r.rows
9295
9467
  .map(
9296
9468
  (x) =>
9297
9469
  ` ${x.at} ${String(x.cmd).padEnd(10)} ${String((x.targets || []).join(",")).slice(0, 40).padEnd(40)}` +
9298
- ` paid ${String(x.paid.card + x.paid.source + x.paid.hints).padStart(6)} avoided ~${rng(x.avoided.low, x.avoided.high)}`
9470
+ ` paid ${String(x.paid.card + x.paid.source + x.paid.hints + (x.paid.envelope || 0)).padStart(6)} avoided ~${rng(x.avoided.low, x.avoided.high)}`
9299
9471
  )
9300
9472
  .join("\n") +
9301
- "\n `avoided` is an ESTIMATE of retrieval the ladder would have paid for — never a bill";
9473
+ "\n `avoided` is an ESTIMATE of retrieval the ladder would have paid for — never a bill" +
9474
+ (briefAsked ? " (--brief ignored on --history)" : "");
9302
9475
  return finish(r, line);
9303
9476
  }
9304
9477
 
@@ -9318,11 +9491,21 @@ function graphCmd() {
9318
9491
  const scope = r.run ? `run ${r.run}` : r.since ? `last ${r.since}` : "this project";
9319
9492
  const line =
9320
9493
  U.header(`graph gain — ${scope} · ${plural(r.calls_recorded, "call")} · ${plural(r.runs, "run")}${g ? ` · gen ${g[0]}${g[1] !== g[0] ? `–${g[1]}` : ""}` : ""}`) +
9321
- `\n paid ${kTok(r.paid.total).padStart(10)} tokens (cards ${kTok(r.paid.card)} · source ${kTok(r.paid.source)} · hints ${kTok(r.paid.hints)})` +
9494
+ `\n paid ${kTok(r.paid.total).padStart(10)} tokens (cards ${kTok(r.paid.card)} · source ${kTok(r.paid.source)} · hints ${kTok(r.paid.hints)} · envelope ${kTok(r.paid.envelope)})` +
9322
9495
  `\n avoided ~${rng(r.avoided.low, r.avoided.high)} tokens (estimate — the read ladder done well … done badly)` +
9323
9496
  `\n net ~${rng(r.net.low, r.net.high)} tokens · ~${r.calls.low} – ${r.calls.high} tool calls` +
9324
9497
  (byCmd ? `\n by command ${byCmd}` : "") +
9325
9498
  `\n hints ${r.hints.injected} injected · ${r.hints.read_notes} read notes · ${r.hints.updates} hook updates` +
9499
+ // R2 (v1.9.1): the reads the `on,read` hint would have named ranges for.
9500
+ // A COUNT, printed only when there is one — it is the data DE-8 waits
9501
+ // for, and nothing changes a setting because of it.
9502
+ (r.hints.wide_unhinted
9503
+ ? `\n wide reads ${r.hints.wide_unhinted} whole-file reads of files with ${require("./graph-resolve.js").WIDE_MIN}+ symbols were not hinted — \`code_graph_hooks: on,read\` names their ranges`
9504
+ : "") +
9505
+ // M2: a read nobody made is a fact about this project, printed with no
9506
+ // advice after it. The graph is not wrong for holding an answer nobody
9507
+ // asked for, and the meter does not tell anyone to start asking.
9508
+ (r.never_called && r.never_called.length ? `\n never called ${r.never_called.join(" · ")}` : "") +
9326
9509
  (r.unsized ? `
9327
9510
  ${plural(r.unsized, "row")} answered from SHARDS, where the size of the search replaced cannot be known — their estimate leaves it out` : "") +
9328
9511
  "\n an estimate of avoided RETRIEVAL, never a bill — searches are a fraction of a percent of a session" +
@@ -9365,8 +9548,8 @@ function graphCmd() {
9365
9548
  // gets it too. Appending it here would have shown it only to a human.
9366
9549
  const line = r.card;
9367
9550
  const trace = `GRAPH-MAP ${focus.length ? "focused" : "repo"} :: files=${r.shown}/${r.total_files}${focus.length ? ` focus=${r.focus.join(",")}` : ""} gen=${r.generation}`;
9368
- if (r.exit === 0) graphGainAppend(claudeDir, gainRowFor("map", Q, model, focus, r, Date.now() - t0, false));
9369
- return finish(r, line, trace, false);
9551
+ const gainRow = r.exit === 0 ? gainRowFor("map", Q, model, focus, r, Date.now() - t0, false) : null;
9552
+ return finish(r, line, trace, false, gainRow);
9370
9553
  }
9371
9554
 
9372
9555
  if (sub === "ctx" || sub === "impact" || sub === "path") {
@@ -9391,6 +9574,8 @@ function graphCmd() {
9391
9574
  // D2 is a SWITCH, so it is read from `args` — `flag()` would read the first
9392
9575
  // declared file as its value.
9393
9576
  const forSlice = args.includes("--for-slice");
9577
+ // R1 (v1.9.1): a SWITCH too — six lines around each confident call site.
9578
+ const callersSource = args.includes("--callers-source");
9394
9579
  // `path` names two symbols, not files, so it has nothing cheap to check —
9395
9580
  // it reads whatever generation is on disk, like every other command did
9396
9581
  // before EW3.
@@ -9411,7 +9596,7 @@ function graphCmd() {
9411
9596
  let fast = null;
9412
9597
  if (sub === "ctx" && !forSlice && operands.length === 1) {
9413
9598
  try {
9414
- fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0]);
9599
+ fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0], { callersSource });
9415
9600
  } catch (_) {
9416
9601
  fast = null;
9417
9602
  }
@@ -9439,6 +9624,7 @@ function graphCmd() {
9439
9624
  format: String(flag("--format") || "prose") === "tree" ? "tree" : "prose",
9440
9625
  offset: flag("--offset"),
9441
9626
  source: sourceLines,
9627
+ callersSource,
9442
9628
  wikiDocsFor,
9443
9629
  };
9444
9630
  let r;
@@ -9498,11 +9684,22 @@ function graphCmd() {
9498
9684
  } else if (sub === "impact") {
9499
9685
  if (!operands.length) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
9500
9686
  r = Q.impact(model, operands, opts);
9687
+ // V2/V3 (v1.9.1): the complexity read, and the `graph_facts` the
9688
+ // planner pastes — computed here, in ONE process, from the model this
9689
+ // call already loaded. Mini used to run `impact` plus one `cochange`
9690
+ // per declared file and count the rows in its own context.
9691
+ if (args.includes("--complexity") && r.exit === 0) {
9692
+ const SIG = require("./graph-signals.js");
9693
+ const cx = SIG.complexityOf(claudeDir, root, model, r.files, { risk: eqFlag("--risk"), tests: r.tests });
9694
+ r.complexity = { verdict: cx.verdict, why: cx.why, numbers: cx.numbers, line: cx.line, thresholds: cx.thresholds };
9695
+ r.facts = cx.facts;
9696
+ }
9501
9697
  } else {
9502
9698
  if (operands.length < 2) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
9503
9699
  r = Q.pathBetween(model, operands[0], operands[1]);
9504
9700
  }
9505
9701
  let line = r.card;
9702
+ if (line && r.complexity) line = `${line}\n ${r.complexity.line}`;
9506
9703
  if (!line && r.state === "not-found") {
9507
9704
  const near = (r.nearest || []).map((s) => `${s.qname} (${s.file}:${s.lines[0]})`);
9508
9705
  line = r.missing
@@ -9526,21 +9723,26 @@ ${line || ""}`;
9526
9723
  ` +
9527
9724
  (line || "");
9528
9725
  }
9726
+ const cx = r.complexity || null;
9529
9727
  const trace =
9530
- sub === "ctx" && r.exit === 0
9728
+ cx
9729
+ ? `GRAPH-COMPLEXITY ${cx.verdict} :: files=${cx.numbers.files} callers=${cx.numbers.callers_outside} caller_files=${cx.numbers.caller_files_outside}` +
9730
+ ` maybe=${cx.numbers.maybe} tests=${cx.numbers.tests_reaching} risk=${cx.numbers.risk.length} cochange=${cx.numbers.cochange_missing.length} gen=${r.generation || (r.facts && r.facts.generation) || 0}`
9731
+ : sub === "ctx" && r.exit === 0
9531
9732
  ? `GRAPH-CONSULT ${r.view === "for-slice" ? "slice" : "card"} :: targets=${(r.view === "for-slice" ? r.files : r.cards ? r.cards.filter((c) => c.state === "found").map((c) => c.query) : [operands[0]]).join(",")}` +
9532
9733
  (healed && healed.state && healed.state !== "skipped" ? ` healed=${healed.trigger}` : "")
9533
9734
  : null;
9735
+ let gainRow = null;
9534
9736
  if (r.exit === 0) {
9535
9737
  try {
9536
- graphGainAppend(claudeDir, gainRowFor(sub, Q, model, operands, r, Date.now() - t0, forSlice));
9738
+ gainRow = gainRowFor(sub, Q, model, operands, r, Date.now() - t0, forSlice);
9537
9739
  } catch (_) {
9538
9740
  // K6: the meter is never allowed to fail the read it is measuring. The
9539
9741
  // append was already fail-quiet; the PRICING was not, because it was an
9540
9742
  // argument to it.
9541
9743
  }
9542
9744
  }
9543
- return finish(r, line, trace, !r.card);
9745
+ return finish(r, line, trace, !r.card, gainRow);
9544
9746
  }
9545
9747
 
9546
9748
  if (asJson) emitJson({ ok: false, reason: "unknown-subcommand", usage }, 1);