@azure-id/orc 1.8.2 → 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 +299 -0
- package/README-id.md +68 -51
- package/README.md +60 -45
- package/bin/cli.js +301 -39
- 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 +277 -61
- package/bin/verify-package.js +19 -1
- 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/mock-run/orc-quick.md +141 -113
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +15 -5
- package/templates/agents/orc-executor-haiku-4-5.md +19 -1
- package/templates/agents/orc-executor-opus-4-7-high.md +19 -1
- package/templates/agents/orc-executor-opus-4-7-med.md +19 -1
- package/templates/agents/orc-executor-opus-4-8-high.md +19 -1
- package/templates/agents/orc-executor-opus-5-high.md +19 -1
- package/templates/agents/orc-executor-opus-5-low.md +19 -1
- package/templates/agents/orc-executor-opus-5-med.md +19 -1
- package/templates/agents/orc-executor-sonnet-4-6-high.md +19 -1
- package/templates/agents/orc-executor-sonnet-4-6-med.md +19 -1
- package/templates/agents/orc-executor-sonnet-5-high.md +19 -1
- package/templates/agents/orc-planner-mini-opus-5-med.md +75 -69
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +73 -67
- package/templates/agents/orc-recon-opus-5-low.md +109 -0
- package/templates/agents/orc-recon-sonnet-4-6-med.md +109 -0
- package/templates/commands/orc-mini.md +10 -12
- package/templates/commands/orc-quick.md +20 -33
- package/templates/hooks/README.md +11 -1
- package/templates/hooks/orc-graph-hook.js +53 -7
- package/templates/hooks/orc-trace.js +476 -471
- 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/rules.md +172 -159
- package/templates/skills/_shared/phases/ship.md +1 -1
- package/templates/skills/_shared/phases/trace.md +4 -2
- package/templates/skills/_shared/phases/wiki-consult.md +9 -5
- package/templates/skills/_shared/read-ladder.md +14 -1
- package/templates/skills/_shared/return-validation.md +22 -0
- package/templates/skills/context-combiner/SKILL.md +13 -13
- package/templates/skills/orc/SKILL.md +1 -1
- package/templates/skills/orc/subskills/orc-execution/core.md +171 -159
- package/templates/skills/orc-fast/SKILL.md +3 -3
- package/templates/skills/orc-mini/SKILL.md +145 -137
- package/templates/skills/orc-mini/examples/mini-run-mock.md +64 -50
- package/templates/skills/orc-mini/references/complexity.md +119 -0
- package/templates/skills/orc-quick/README.md +495 -423
- package/templates/skills/orc-quick/SKILL.md +157 -212
- package/templates/skills/orc-quick/references/context-doc.md +145 -114
- package/templates/skills/orc-quick/references/defect.md +101 -0
- package/templates/skills/orc-quick/references/dispatch-gate.md +55 -24
- package/templates/skills/orc-quick/references/gh-mode.md +148 -127
- package/templates/skills/orc-quick/references/look.md +107 -0
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 = [];
|
|
@@ -3347,7 +3350,10 @@ const LANE_CALLS = {
|
|
|
3347
3350
|
on_absent: "`none` means absent; say so in ONE user line and continue — a missing wiki is never a blocker",
|
|
3348
3351
|
canonical: "_shared/detecting-artifacts.md",
|
|
3349
3352
|
never: "never second-guess a positive probe with a raw `find` (the wiki lives under a dot-dir), and never compute the tier yourself — this command is its only executor",
|
|
3350
|
-
|
|
3353
|
+
// v1.9.0: /orc-mini joins. It always consulted the wiki; it now selects PAGE
|
|
3354
|
+
// PATHS for its slices instead of reading page bodies into its own context,
|
|
3355
|
+
// and the tier this command computes is what that selection rests on.
|
|
3356
|
+
lanes: ["orc", "orc-boundary", "orc-brainstorm", "orc-fast", "orc-grill", "orc-mini", "orc-poly", "orc-quick", "orc-route", "orc-wiki"],
|
|
3351
3357
|
},
|
|
3352
3358
|
"pattern-status": {
|
|
3353
3359
|
cmd: "orc pattern status <lang>",
|
|
@@ -3423,11 +3429,11 @@ const LANE_CALLS = {
|
|
|
3423
3429
|
exits: { 0: "answered \u2014 ALWAYS, when a graph exists", 1: "no graph index", 3: "off \u2014 `code_graph` is off" },
|
|
3424
3430
|
states: null,
|
|
3425
3431
|
cost: "free",
|
|
3426
|
-
when: "ONCE, at the START of planning \u2014 before any Glob and before `impact`, which needs the files already chosen. Planning ONLY:
|
|
3432
|
+
when: "ONCE, at the START of planning \u2014 before any Glob and before `impact`, which needs the files already chosen. Planning ONLY, and v1.9.0 widens what counts as planning rather than the rule: /orc-quick's Q1 when the request NAMES NO FILE, and /orc-mini's intake before the tiered round, are the same question asked by a lane with no planner. DE-H's 0.39 answerable calls per run measured the analyst asking it AFTER the files were known, which is the case this row still refuses",
|
|
3427
3433
|
on_absent: "exit 1 or 3 \u2192 orient with Glob and Read exactly as before the map existed",
|
|
3428
3434
|
canonical: "_shared/code-graph.md",
|
|
3429
|
-
never: "never read RANK as importance to THIS change \u2014 it is a hint about where to look first, computed from the whole repository, and a low-ranked file that the change reaches is still a file you must read",
|
|
3430
|
-
lanes: ["orc", "orc-diy"],
|
|
3435
|
+
never: "never read RANK as importance to THIS change \u2014 it is a hint about where to look first, computed from the whole repository, and a low-ranked file that the change reaches is still a file you must read. Never call it a SECOND time in one run: the ranking does not move because you read it",
|
|
3436
|
+
lanes: ["orc", "orc-diy", "orc-mini", "orc-quick"],
|
|
3431
3437
|
},
|
|
3432
3438
|
"graph-impact": {
|
|
3433
3439
|
cmd: "orc graph impact <file…> --if-enabled [--depth N] [--json]",
|
|
@@ -3439,7 +3445,7 @@ const LANE_CALLS = {
|
|
|
3439
3445
|
on_absent: "exit 3 or 4 → plan or review exactly as before; AMBIGUOUS callers are counted, never followed",
|
|
3440
3446
|
canonical: "_shared/code-graph.md",
|
|
3441
3447
|
never: "never treat an empty impact as proof nothing depends on the change — dynamic dispatch is invisible to the graph",
|
|
3442
|
-
lanes: ["orc", "orc-diy"],
|
|
3448
|
+
lanes: ["orc", "orc-diy", "orc-mini"],
|
|
3443
3449
|
},
|
|
3444
3450
|
// EW5 — the cheap signals. All three read what is already on disk (the index,
|
|
3445
3451
|
// and git's own history), so all three cost zero model tokens.
|
|
@@ -3453,7 +3459,7 @@ const LANE_CALLS = {
|
|
|
3453
3459
|
on_absent: "exit 1 or 3 → read the source, exactly as before the graph existed",
|
|
3454
3460
|
canonical: "_shared/code-graph.md",
|
|
3455
3461
|
never: "never read `no recorded gap` as proof of completeness — a file marked `full` was fully PARSED, not fully understood",
|
|
3456
|
-
lanes: ["orc", "orc-diy"],
|
|
3462
|
+
lanes: ["orc", "orc-diy", "orc-quick"],
|
|
3457
3463
|
},
|
|
3458
3464
|
"graph-changes": {
|
|
3459
3465
|
cmd: "orc graph changes [--base <ref>] --if-enabled [--json]",
|
|
@@ -3465,7 +3471,7 @@ const LANE_CALLS = {
|
|
|
3465
3471
|
on_absent: "exit 1 or 3 → review from the diff alone, exactly as before",
|
|
3466
3472
|
canonical: "_shared/code-graph.md",
|
|
3467
3473
|
never: "never report `risk` without the `why` beside it — a rating nobody can check is a rating nobody should act on",
|
|
3468
|
-
lanes: ["orc", "orc-diy"],
|
|
3474
|
+
lanes: ["orc", "orc-diy", "orc-mini", "orc-quick"],
|
|
3469
3475
|
},
|
|
3470
3476
|
"graph-cochange": {
|
|
3471
3477
|
cmd: "orc graph cochange <file> --if-enabled [--json]",
|
|
@@ -3477,8 +3483,37 @@ const LANE_CALLS = {
|
|
|
3477
3483
|
on_absent: "exit 4 is an ANSWER, not a miss — plan exactly as before",
|
|
3478
3484
|
canonical: "_shared/code-graph.md",
|
|
3479
3485
|
never: "never read a co-change as a DEPENDENCY — it says what people changed together, never what needs what",
|
|
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.
|
|
3480
3488
|
lanes: ["orc", "orc-diy"],
|
|
3481
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: [],
|
|
3504
|
+
},
|
|
3505
|
+
"graph-gain": {
|
|
3506
|
+
cmd: "orc graph gain --run <trace name> --if-enabled [--json]",
|
|
3507
|
+
what: "what the graph cost this run and what it plausibly avoided — three halves kept apart: paid (recorded) · avoided (an ESTIMATE, always a range) · measured (silent below three runs per group)",
|
|
3508
|
+
exits: { 0: "a line", 1: "no ledger, or no rows yet", 3: "off" },
|
|
3509
|
+
states: null,
|
|
3510
|
+
cost: "free",
|
|
3511
|
+
when: "ONCE, at the close of a code-writing request (quick) or after a green smoke gate (mini)",
|
|
3512
|
+
on_absent: "exit 1 is an ANSWER — there is nothing to report yet, so report nothing",
|
|
3513
|
+
canonical: "_shared/code-graph.md",
|
|
3514
|
+
never: "never restate the line as a saving, never drop the word estimate or the range, and never let this call block or fail a run",
|
|
3515
|
+
lanes: ["orc-mini", "orc-quick"],
|
|
3516
|
+
},
|
|
3482
3517
|
"graph-notes-pending": {
|
|
3483
3518
|
cmd: "orc graph notes pending --files <a,b> [--at wave|end] --if-enabled [--json]",
|
|
3484
3519
|
what: "which changed functions have no note for their current body — the input to one notes batch",
|
|
@@ -3489,7 +3524,11 @@ const LANE_CALLS = {
|
|
|
3489
3524
|
on_absent: "exit 5 is an ANSWER — the symbols wait for a later batch and nothing is lost",
|
|
3490
3525
|
canonical: "_shared/code-graph.md",
|
|
3491
3526
|
never: "never paste notes into your own context — the noter pipes them to `orc graph notes apply -` and returns one line",
|
|
3492
|
-
|
|
3527
|
+
// v1.9.0: /orc-quick and /orc-mini left this row. Neither lost the notes —
|
|
3528
|
+
// both stopped making a SECOND call for them: each now runs
|
|
3529
|
+
// `update --notes-pending`, which answers the update and the pending batch
|
|
3530
|
+
// in one, and still dispatches the noter when the answer has rows.
|
|
3531
|
+
lanes: ["orc", "orc-diy", "orc-fast"],
|
|
3493
3532
|
},
|
|
3494
3533
|
"gotcha-list": {
|
|
3495
3534
|
cmd: "orc gotcha list [--archived] [--json]",
|
|
@@ -4415,10 +4454,14 @@ const LANE_OWN_PHASES = {
|
|
|
4415
4454
|
{ ord: 6, id: "phase-s6", file: "orc-pr-setup/SKILL.md", heading: "## Phase S6 — Emit the plan, then stop", read: "section", trace_verbs: [] },
|
|
4416
4455
|
],
|
|
4417
4456
|
"orc-quick": [
|
|
4418
|
-
|
|
4419
|
-
|
|
4457
|
+
// v1.9.0 — the verbs the lane already owed, plus the graph-first look and
|
|
4458
|
+
// the defect reproduction. The HEADINGS are unchanged: a `read: section`
|
|
4459
|
+
// pointer names a heading byte-for-byte, and a payload test asserts each
|
|
4460
|
+
// one still exists in the spine.
|
|
4461
|
+
{ ord: 0, id: "q0", file: "orc-quick/SKILL.md", heading: "## Q0 — Preflight (ONE time per session, silent, nothing can stop the run)", read: "section", trace_verbs: ["GATE", "GRAPH-CONSULT"] },
|
|
4462
|
+
{ ord: 1, id: "q1", file: "orc-quick/SKILL.md", heading: "## Q1 — LOOK (silent — no questions here)", read: "section", trace_verbs: ["GATE", "WIKI-CONSULT", "GRAPH-CONSULT", "GRAPH-MAP"] },
|
|
4420
4463
|
{ ord: 2, id: "q2", file: "orc-quick/SKILL.md", heading: "## Q2 — ASK (ONE user turn: questions + the gate together)", read: "section", trace_verbs: [] },
|
|
4421
|
-
{ ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["
|
|
4464
|
+
{ ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["DISPATCH", "VERIFY", "REPRO", "GRAPH-CHANGES", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-GAIN", "OUTCOME", "FINISH"] },
|
|
4422
4465
|
],
|
|
4423
4466
|
"orc-wiki": [
|
|
4424
4467
|
{ ord: 0, id: "phase-0", file: "orc-wiki/references/phases/phase-0.md", layers: ["full"], trace_verbs: [] },
|
|
@@ -4514,6 +4557,9 @@ const PHASE_KIND = {
|
|
|
4514
4557
|
combine: "look",
|
|
4515
4558
|
// waiting on the user
|
|
4516
4559
|
intake: "ask",
|
|
4560
|
+
// v1.9.0 — /orc-quick's read-only half. It LOOKS: one question, answered
|
|
4561
|
+
// with evidence, and nothing written.
|
|
4562
|
+
recon: "look",
|
|
4517
4563
|
// deciding what to do and in what order
|
|
4518
4564
|
planning: "plan",
|
|
4519
4565
|
"plan-handoff": "plan",
|
|
@@ -4593,7 +4639,7 @@ function railKind(row) {
|
|
|
4593
4639
|
// This is a MIRROR of that hook's `roleFamily()`, and it is the one new drift
|
|
4594
4640
|
// surface this feature adds — a golden test compares the two by source text,
|
|
4595
4641
|
// the way the `OPUS5_BANDS` alias is asserted.
|
|
4596
|
-
const TRACE_FAMILIES = ["combine", "analysis", "planning", "execution", "review", "verify", "testgen", "ultra-gate"];
|
|
4642
|
+
const TRACE_FAMILIES = ["recon", "combine", "analysis", "planning", "execution", "review", "verify", "testgen", "ultra-gate"];
|
|
4597
4643
|
|
|
4598
4644
|
// The whole object, deterministic from the registries above. `--json is not a
|
|
4599
4645
|
// summary`: this is what the file holds and what `orc lane rails` prints.
|
|
@@ -8716,6 +8762,14 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
|
|
|
8716
8762
|
paid.source = K.tok(r.source.text);
|
|
8717
8763
|
paid.card = Math.max(0, paid.card - paid.source);
|
|
8718
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
|
+
}
|
|
8719
8773
|
const base = { cmd: sub, targets: operands.slice(0, 5), gen, ms, paid };
|
|
8720
8774
|
|
|
8721
8775
|
if (sub === "impact" || (sub === "ctx" && forSlice)) {
|
|
@@ -8747,6 +8801,7 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
|
|
|
8747
8801
|
const hits = Q.nameHits(model, r.target.qname);
|
|
8748
8802
|
const c = K.ctxSymbol(model, r.target, hits, (r.callers || []).map((x) => x.file));
|
|
8749
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));
|
|
8750
8805
|
return { ...base, ...c };
|
|
8751
8806
|
}
|
|
8752
8807
|
return { ...base, avoided: { low: 0, high: 0, calls_low: 0, calls_high: 0 }, basis: { files: [], grep_hits: 0 } };
|
|
@@ -8789,23 +8844,79 @@ function graphGainAppend(claudeDir, row) {
|
|
|
8789
8844
|
// A thin router. The engine is `bin/graph.js`; this function resolves the
|
|
8790
8845
|
// config (the engine never reads it) and owns the human lines. The exit code is
|
|
8791
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
|
+
|
|
8792
8895
|
function graphCmd() {
|
|
8793
8896
|
const usage =
|
|
8794
|
-
"Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc\n" +
|
|
8795
|
-
" orc graph ctx <symbol|file[:line]>… [--source [N]] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
|
|
8796
|
-
" 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" +
|
|
8797
8900
|
" orc graph map [--focus <file|name,…>] [--budget N]\n" +
|
|
8798
8901
|
" orc graph gain [--run <name>] [--since 7d] [--history [--limit 40]] [--measured] [--reset --yes]\n" +
|
|
8799
8902
|
" orc graph notes pending --files <a,b> [--cap 40] [--min 5] [--with-source] | notes apply <file|->\n" +
|
|
8800
|
-
" [--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" +
|
|
8801
8906
|
" status exit 0 FRESH · 1 NONE · 2 DRIFTED · 3 OFF (--heal builds or updates a NONE/DRIFTED graph first)\n" +
|
|
8802
8907
|
" update exit 0 done · 1 unavailable or locked · 3 off (with --if-enabled)\n" +
|
|
8803
8908
|
" --notes-pending adds the `notes pending` answer to the SAME call (one process, one lock)\n" +
|
|
8804
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" +
|
|
8805
8912
|
" ctx exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 not found or ambiguous\n" +
|
|
8806
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" +
|
|
8807
8916
|
" --for-slice prints only the OUTSIDE view of each declared file — no symbol table\n" +
|
|
8808
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" +
|
|
8809
8920
|
" path exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 no confident path, or an unknown end\n" +
|
|
8810
8921
|
" map exit 0 always when a graph exists · 1 no graph · 3 off (with --if-enabled) the files by RANK, most\n" +
|
|
8811
8922
|
" connected first, each with its top symbols; --focus re-ranks the repository around those files or names\n" +
|
|
@@ -8826,6 +8937,16 @@ function graphCmd() {
|
|
|
8826
8937
|
const root = repoRootOf(claudeDir);
|
|
8827
8938
|
const sub = positionals()[1];
|
|
8828
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
|
+
};
|
|
8829
8950
|
const ovr = readOverride(claudeDir).map;
|
|
8830
8951
|
const cfg = (k) => (Object.prototype.hasOwnProperty.call(ovr, k) ? ovr[k] : metaFor(k).def);
|
|
8831
8952
|
const enabled = String(cfg("code_graph")) === "on";
|
|
@@ -8904,7 +9025,12 @@ function graphCmd() {
|
|
|
8904
9025
|
return null;
|
|
8905
9026
|
}
|
|
8906
9027
|
};
|
|
8907
|
-
|
|
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) => {
|
|
8908
9034
|
const { exit, ...obj } = r;
|
|
8909
9035
|
if (obj.generation === undefined) {
|
|
8910
9036
|
const m = metaNow();
|
|
@@ -8913,7 +9039,19 @@ function graphCmd() {
|
|
|
8913
9039
|
obj.gen_id = m.gen_id || null;
|
|
8914
9040
|
}
|
|
8915
9041
|
}
|
|
8916
|
-
|
|
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);
|
|
8917
9055
|
if (line) console.log(line);
|
|
8918
9056
|
process.exit(exit);
|
|
8919
9057
|
};
|
|
@@ -8936,6 +9074,32 @@ function graphCmd() {
|
|
|
8936
9074
|
r.auto_update = autoUpdate;
|
|
8937
9075
|
r.notes = String(cfg("code_graph_notes"));
|
|
8938
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
|
+
}
|
|
8939
9103
|
const b = r.behind || {};
|
|
8940
9104
|
const lines = {
|
|
8941
9105
|
off: "graph: off",
|
|
@@ -8949,6 +9113,12 @@ function graphCmd() {
|
|
|
8949
9113
|
};
|
|
8950
9114
|
let line = lines[r.state];
|
|
8951
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
|
+
: "";
|
|
8952
9122
|
if (healed && healed.parsed !== undefined) {
|
|
8953
9123
|
const moved = healed.added + healed.changed + healed.deleted;
|
|
8954
9124
|
const size = `${plural(r.files, "file")} · ${plural(r.symbols, "symbol")}`;
|
|
@@ -8960,7 +9130,15 @@ function graphCmd() {
|
|
|
8960
9130
|
? `graph: index upgraded to the new engine → updated (${healed.ms} ms) — ${size}`
|
|
8961
9131
|
: `graph: ${plural(moved, "file")} changed outside ORC → updated (${healed.ms} ms) — ${size}`;
|
|
8962
9132
|
} else if (healed) line += ` · could not heal: ${healed.reason}`;
|
|
8963
|
-
|
|
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
|
+
);
|
|
8964
9142
|
}
|
|
8965
9143
|
|
|
8966
9144
|
if (sub === "update") {
|
|
@@ -9124,8 +9302,11 @@ function graphCmd() {
|
|
|
9124
9302
|
const SIG = require("./graph-signals.js");
|
|
9125
9303
|
if (sub === "changes") {
|
|
9126
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);
|
|
9127
9308
|
const healed = healOnRead(SIG.changedPaths(root, base));
|
|
9128
|
-
const r = SIG.graphChanges(claudeDir, root, { base });
|
|
9309
|
+
const r = SIG.graphChanges(claudeDir, root, { base, files: onlyFiles });
|
|
9129
9310
|
if (healed) r.healed = healed;
|
|
9130
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}`);
|
|
9131
9312
|
const c = r.counts;
|
|
@@ -9137,6 +9318,7 @@ function graphCmd() {
|
|
|
9137
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(", ")})` : ""}`;
|
|
9138
9319
|
// K1/K2: `changes` avoids one Grep per touched symbol. The diff itself
|
|
9139
9320
|
// is NOT counted — the reviewer reads it either way.
|
|
9321
|
+
let gainRow = null;
|
|
9140
9322
|
if (r.symbols.length) {
|
|
9141
9323
|
try {
|
|
9142
9324
|
const K = require("./graph-gain.js");
|
|
@@ -9145,11 +9327,11 @@ function graphCmd() {
|
|
|
9145
9327
|
if (m) {
|
|
9146
9328
|
const hits = r.symbols.reduce((a, x) => a + Q.nameHits(m, x.qname), 0);
|
|
9147
9329
|
const callerFiles = r.symbols.flatMap((x) => (x.callers || []).map((cc) => cc.file || cc));
|
|
9148
|
-
|
|
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) };
|
|
9149
9331
|
}
|
|
9150
9332
|
} catch (_) {}
|
|
9151
9333
|
}
|
|
9152
|
-
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);
|
|
9153
9335
|
}
|
|
9154
9336
|
const file = plain[2];
|
|
9155
9337
|
if (!file) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
|
|
@@ -9164,6 +9346,18 @@ function graphCmd() {
|
|
|
9164
9346
|
return finish(r, line, `GRAPH-COCHANGE ${r.rows.length ? "found" : "none"} :: rows=${r.rows.length} commits=${r.commits}`);
|
|
9165
9347
|
}
|
|
9166
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
|
+
|
|
9167
9361
|
if (sub === "gc") {
|
|
9168
9362
|
const r = G.graphGc(claudeDir);
|
|
9169
9363
|
const line =
|
|
@@ -9263,16 +9457,21 @@ function graphCmd() {
|
|
|
9263
9457
|
if (flag("--history") !== undefined) {
|
|
9264
9458
|
const r = K.history(claudeDir, { limit: flag("--limit") });
|
|
9265
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;
|
|
9266
9464
|
const line =
|
|
9267
9465
|
`graph gain — last ${r.rows.length} of ${r.total} call(s), newest first\n` +
|
|
9268
9466
|
r.rows
|
|
9269
9467
|
.map(
|
|
9270
9468
|
(x) =>
|
|
9271
9469
|
` ${x.at} ${String(x.cmd).padEnd(10)} ${String((x.targets || []).join(",")).slice(0, 40).padEnd(40)}` +
|
|
9272
|
-
` 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)}`
|
|
9273
9471
|
)
|
|
9274
9472
|
.join("\n") +
|
|
9275
|
-
"\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)" : "");
|
|
9276
9475
|
return finish(r, line);
|
|
9277
9476
|
}
|
|
9278
9477
|
|
|
@@ -9292,11 +9491,21 @@ function graphCmd() {
|
|
|
9292
9491
|
const scope = r.run ? `run ${r.run}` : r.since ? `last ${r.since}` : "this project";
|
|
9293
9492
|
const line =
|
|
9294
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]}` : ""}` : ""}`) +
|
|
9295
|
-
`\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)})` +
|
|
9296
9495
|
`\n avoided ~${rng(r.avoided.low, r.avoided.high)} tokens (estimate — the read ladder done well … done badly)` +
|
|
9297
9496
|
`\n net ~${rng(r.net.low, r.net.high)} tokens · ~${r.calls.low} – ${r.calls.high} tool calls` +
|
|
9298
9497
|
(byCmd ? `\n by command ${byCmd}` : "") +
|
|
9299
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(" · ")}` : "") +
|
|
9300
9509
|
(r.unsized ? `
|
|
9301
9510
|
${plural(r.unsized, "row")} answered from SHARDS, where the size of the search replaced cannot be known — their estimate leaves it out` : "") +
|
|
9302
9511
|
"\n an estimate of avoided RETRIEVAL, never a bill — searches are a fraction of a percent of a session" +
|
|
@@ -9339,8 +9548,8 @@ function graphCmd() {
|
|
|
9339
9548
|
// gets it too. Appending it here would have shown it only to a human.
|
|
9340
9549
|
const line = r.card;
|
|
9341
9550
|
const trace = `GRAPH-MAP ${focus.length ? "focused" : "repo"} :: files=${r.shown}/${r.total_files}${focus.length ? ` focus=${r.focus.join(",")}` : ""} gen=${r.generation}`;
|
|
9342
|
-
|
|
9343
|
-
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);
|
|
9344
9553
|
}
|
|
9345
9554
|
|
|
9346
9555
|
if (sub === "ctx" || sub === "impact" || sub === "path") {
|
|
@@ -9365,6 +9574,8 @@ function graphCmd() {
|
|
|
9365
9574
|
// D2 is a SWITCH, so it is read from `args` — `flag()` would read the first
|
|
9366
9575
|
// declared file as its value.
|
|
9367
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");
|
|
9368
9579
|
// `path` names two symbols, not files, so it has nothing cheap to check —
|
|
9369
9580
|
// it reads whatever generation is on disk, like every other command did
|
|
9370
9581
|
// before EW3.
|
|
@@ -9385,7 +9596,7 @@ function graphCmd() {
|
|
|
9385
9596
|
let fast = null;
|
|
9386
9597
|
if (sub === "ctx" && !forSlice && operands.length === 1) {
|
|
9387
9598
|
try {
|
|
9388
|
-
fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0]);
|
|
9599
|
+
fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0], { callersSource });
|
|
9389
9600
|
} catch (_) {
|
|
9390
9601
|
fast = null;
|
|
9391
9602
|
}
|
|
@@ -9413,6 +9624,7 @@ function graphCmd() {
|
|
|
9413
9624
|
format: String(flag("--format") || "prose") === "tree" ? "tree" : "prose",
|
|
9414
9625
|
offset: flag("--offset"),
|
|
9415
9626
|
source: sourceLines,
|
|
9627
|
+
callersSource,
|
|
9416
9628
|
wikiDocsFor,
|
|
9417
9629
|
};
|
|
9418
9630
|
let r;
|
|
@@ -9472,11 +9684,22 @@ function graphCmd() {
|
|
|
9472
9684
|
} else if (sub === "impact") {
|
|
9473
9685
|
if (!operands.length) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
|
|
9474
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
|
+
}
|
|
9475
9697
|
} else {
|
|
9476
9698
|
if (operands.length < 2) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
|
|
9477
9699
|
r = Q.pathBetween(model, operands[0], operands[1]);
|
|
9478
9700
|
}
|
|
9479
9701
|
let line = r.card;
|
|
9702
|
+
if (line && r.complexity) line = `${line}\n ${r.complexity.line}`;
|
|
9480
9703
|
if (!line && r.state === "not-found") {
|
|
9481
9704
|
const near = (r.nearest || []).map((s) => `${s.qname} (${s.file}:${s.lines[0]})`);
|
|
9482
9705
|
line = r.missing
|
|
@@ -9500,21 +9723,26 @@ ${line || ""}`;
|
|
|
9500
9723
|
` +
|
|
9501
9724
|
(line || "");
|
|
9502
9725
|
}
|
|
9726
|
+
const cx = r.complexity || null;
|
|
9503
9727
|
const trace =
|
|
9504
|
-
|
|
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
|
|
9505
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(",")}` +
|
|
9506
9733
|
(healed && healed.state && healed.state !== "skipped" ? ` healed=${healed.trigger}` : "")
|
|
9507
9734
|
: null;
|
|
9735
|
+
let gainRow = null;
|
|
9508
9736
|
if (r.exit === 0) {
|
|
9509
9737
|
try {
|
|
9510
|
-
|
|
9738
|
+
gainRow = gainRowFor(sub, Q, model, operands, r, Date.now() - t0, forSlice);
|
|
9511
9739
|
} catch (_) {
|
|
9512
9740
|
// K6: the meter is never allowed to fail the read it is measuring. The
|
|
9513
9741
|
// append was already fail-quiet; the PRICING was not, because it was an
|
|
9514
9742
|
// argument to it.
|
|
9515
9743
|
}
|
|
9516
9744
|
}
|
|
9517
|
-
return finish(r, line, trace, !r.card);
|
|
9745
|
+
return finish(r, line, trace, !r.card, gainRow);
|
|
9518
9746
|
}
|
|
9519
9747
|
|
|
9520
9748
|
if (asJson) emitJson({ ok: false, reason: "unknown-subcommand", usage }, 1);
|
|
@@ -44482,6 +44710,29 @@ function rulesLine(packs, blocks, overrides) {
|
|
|
44482
44710
|
// body and PURPOSE/LOCK rules carry one line each plus the file to open when one
|
|
44483
44711
|
// applies. That is the read-ladder discipline this repo already runs on, applied
|
|
44484
44712
|
// to a standing card.
|
|
44713
|
+
// v1.9.0 - the COMPACT form, and the CLI decides it, never the lane.
|
|
44714
|
+
// `_shared/phases/rules.md` names `rulesSlice()` as the only lever on the
|
|
44715
|
+
// card's cost, so a lane that wanted a smaller card had no honest way to ask
|
|
44716
|
+
// for one. /orc-quick is the lane with the SMALLEST tasks under the LARGEST
|
|
44717
|
+
// fixed card: the full card measured 13,874 chars, re-sent on every executor
|
|
44718
|
+
// turn, for a task that often edits one file. Compact keeps every HARD rule's
|
|
44719
|
+
// ID and its first line - the instruction - and drops the worked examples,
|
|
44720
|
+
// which are what a rule's body is mostly made of. Nothing is hidden: the pack
|
|
44721
|
+
// file is named beside the rules, exactly as the light tiers already are.
|
|
44722
|
+
const RULES_COMPACT_LANES = new Set(["orc-quick"]);
|
|
44723
|
+
|
|
44724
|
+
// One line out of a rule body: the first paragraph, whitespace collapsed, cut
|
|
44725
|
+
// at a word boundary. A rule whose instruction does not survive that is a rule
|
|
44726
|
+
// whose body is an example, and the pack file is one Read away.
|
|
44727
|
+
function rulesOneLine(body, max) {
|
|
44728
|
+
const first = String(body || "").split(/\n[ \t]*\n/)[0] || "";
|
|
44729
|
+
const flat = first.replace(/\s+/g, " ").trim();
|
|
44730
|
+
if (flat.length <= max) return flat;
|
|
44731
|
+
const cut = flat.slice(0, max);
|
|
44732
|
+
const at = cut.lastIndexOf(" ");
|
|
44733
|
+
return (at > max * 0.6 ? cut.slice(0, at) : cut).replace(/[ ,;:.]+$/, "") + " …";
|
|
44734
|
+
}
|
|
44735
|
+
|
|
44485
44736
|
function rulesSlice(claudeDir, lane, extraPacks) {
|
|
44486
44737
|
const packs = rulesReadPacks(claudeDir);
|
|
44487
44738
|
const led = userRulesRead(claudeDir);
|
|
@@ -44489,6 +44740,7 @@ function rulesSlice(claudeDir, lane, extraPacks) {
|
|
|
44489
44740
|
const overridden = new Set(overrides.filter((o) => o.known).map((o) => o.id));
|
|
44490
44741
|
const want = new Set([...(RULE_LANE_PACKS[lane] || []), ...(extraPacks || [])]);
|
|
44491
44742
|
const chosen = packs.packs.filter((p) => want.has(p.id));
|
|
44743
|
+
const compact = RULES_COMPACT_LANES.has(lane);
|
|
44492
44744
|
|
|
44493
44745
|
const L = [];
|
|
44494
44746
|
if (!led.empty) {
|
|
@@ -44501,19 +44753,26 @@ function rulesSlice(claudeDir, lane, extraPacks) {
|
|
|
44501
44753
|
if (chosen.length) {
|
|
44502
44754
|
L.push(
|
|
44503
44755
|
`ORC RULES — the anti-slop baseline. Shipped with ORC, read-only. ${packs.count} rules, ${plural(chosen.length, "pack")} in this slice.`,
|
|
44504
|
-
|
|
44756
|
+
// The credits line is a CREDIT, not an instruction, and `orc rules
|
|
44757
|
+
// credits` prints it in full. It is the one line a compact card can drop
|
|
44758
|
+
// without dropping a rule.
|
|
44759
|
+
...(compact ? [] : ["Source: petergyang/no-ai-slop · miqdadbadjuber/anti-slop · ehmo/slopkit · BioInfo/slopless · Karpathy · Matty Cartwright. Full credits: `orc rules credits`."]),
|
|
44505
44760
|
"HARD = absolute · PURPOSE = allowed with a written one-line reason · LOCK = consistency, reported never blocking.",
|
|
44761
|
+
...(compact ? ["Each line below is a rule ID and its instruction. Credits: `orc rules credits`."] : []),
|
|
44506
44762
|
""
|
|
44507
44763
|
);
|
|
44508
44764
|
for (const p of chosen) {
|
|
44509
44765
|
L.push(`[${p.title}]`);
|
|
44510
44766
|
for (const r of p.rules) {
|
|
44511
44767
|
if (overridden.has(r.id)) continue; // the user replaced it
|
|
44512
|
-
if (r.tier === "HARD") L.push(`${r.id} HARD · ${r.title}
|
|
44768
|
+
if (r.tier === "HARD" && compact) L.push(`${r.id} HARD · ${r.title} — ${rulesOneLine(r.body, 62)}`);
|
|
44769
|
+
else if (r.tier === "HARD") L.push(`${r.id} HARD · ${r.title}`, r.body, "");
|
|
44513
44770
|
else L.push(`${r.id} ${r.tier} · ${r.title}`);
|
|
44514
44771
|
}
|
|
44515
44772
|
const light = p.rules.filter((r) => r.tier !== "HARD" && !overridden.has(r.id)).length;
|
|
44516
|
-
if (
|
|
44773
|
+
if (compact)
|
|
44774
|
+
L.push("", `Every rule above is one line — open ${p.file} for the body and the worked examples of any that applies.`, "");
|
|
44775
|
+
else if (light) L.push("", `${plural(light, "rule")} above are one-liners — open ${p.file} when one of them applies.`, "");
|
|
44517
44776
|
}
|
|
44518
44777
|
}
|
|
44519
44778
|
if (overrides.length) {
|
|
@@ -44531,6 +44790,9 @@ function rulesSlice(claudeDir, lane, extraPacks) {
|
|
|
44531
44790
|
return {
|
|
44532
44791
|
lane,
|
|
44533
44792
|
packs: chosen.map((p) => p.id),
|
|
44793
|
+
// `compact` is REPORTED, never asked for. A reader that cannot tell a short
|
|
44794
|
+
// card from a stripped one cannot trust either.
|
|
44795
|
+
compact,
|
|
44534
44796
|
text: L.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, ""),
|
|
44535
44797
|
overrides,
|
|
44536
44798
|
line: rulesLine(packs, led.blocks, overrides),
|