@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.
- package/CHANGELOG.md +133 -0
- package/README-id.md +57 -58
- package/README.md +51 -53
- package/bin/cli.js +227 -25
- package/bin/graph-audit.js +235 -0
- package/bin/graph-extract.js +308 -3
- package/bin/graph-gain.js +23 -3
- package/bin/graph-map.js +9 -1
- package/bin/graph-query.js +152 -10
- package/bin/graph-shard.js +6 -1
- package/bin/graph-signals.js +287 -4
- package/bin/graph.js +717 -658
- package/bin/verify-contracts.js +115 -0
- package/bin/verify-package.js +4 -0
- package/bin/webui/fixtures/knowledge.js +21 -4
- package/bin/webui/i18n/en/knowledge.json +5 -2
- package/bin/webui/i18n/id/knowledge.json +5 -2
- package/bin/webui/js/panels/knowledge.js +21 -2
- package/package.json +1 -1
- package/templates/agents/orc-executor-haiku-4-5.md +2 -1
- package/templates/agents/orc-executor-opus-4-7-high.md +2 -1
- package/templates/agents/orc-executor-opus-4-7-med.md +2 -1
- package/templates/agents/orc-executor-opus-4-8-high.md +2 -1
- package/templates/agents/orc-executor-opus-5-high.md +2 -1
- package/templates/agents/orc-executor-opus-5-low.md +2 -1
- package/templates/agents/orc-executor-opus-5-med.md +2 -1
- package/templates/agents/orc-executor-sonnet-4-6-high.md +2 -1
- package/templates/agents/orc-executor-sonnet-4-6-med.md +2 -1
- package/templates/agents/orc-executor-sonnet-5-high.md +2 -1
- package/templates/agents/orc-recon-opus-5-low.md +15 -5
- package/templates/agents/orc-recon-sonnet-4-6-med.md +15 -5
- package/templates/hooks/README.md +11 -1
- package/templates/hooks/orc-graph-hook.js +53 -7
- package/templates/skills/_shared/code-graph.md +56 -17
- package/templates/skills/_shared/phases/execution.md +4 -4
- package/templates/skills/_shared/phases/planning.md +3 -3
- package/templates/skills/_shared/phases/preflight.md +1 -1
- package/templates/skills/_shared/phases/review.md +3 -1
- package/templates/skills/_shared/phases/ship.md +1 -1
- package/templates/skills/_shared/phases/trace.md +2 -1
- package/templates/skills/_shared/read-ladder.md +14 -1
- package/templates/skills/orc/SKILL.md +1 -1
- package/templates/skills/orc-fast/SKILL.md +3 -3
- package/templates/skills/orc-mini/SKILL.md +13 -17
- package/templates/skills/orc-mini/references/complexity.md +25 -11
- package/templates/skills/orc-quick/SKILL.md +12 -13
- package/templates/skills/orc-quick/references/gh-mode.md +1 -1
- 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
|
-
|
|
184
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|