@azure-id/orc 1.7.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +3869 -3381
  2. package/README-id.md +942 -844
  3. package/README.md +882 -788
  4. package/bin/build-agents.js +43 -27
  5. package/bin/cli.js +45460 -44169
  6. package/bin/graph-extract.js +3216 -0
  7. package/bin/graph-gain.js +404 -0
  8. package/bin/graph-map.js +232 -0
  9. package/bin/graph-notes.js +229 -0
  10. package/bin/graph-query.js +1770 -0
  11. package/bin/graph-resolve.js +255 -0
  12. package/bin/graph-shard.js +325 -0
  13. package/bin/graph-signals.js +277 -0
  14. package/bin/graph.js +658 -0
  15. package/bin/verify-contracts.js +4809 -4553
  16. package/bin/verify-package.js +640 -616
  17. package/bin/webui/api.js +1425 -1414
  18. package/bin/webui/fixtures/index.js +584 -576
  19. package/bin/webui/fixtures/knowledge.js +356 -291
  20. package/bin/webui/fixtures/stats.js +107 -104
  21. package/bin/webui/i18n/en/knowledge.json +182 -151
  22. package/bin/webui/i18n/en/overview.json +101 -100
  23. package/bin/webui/i18n/id/knowledge.json +182 -151
  24. package/bin/webui/i18n/id/overview.json +101 -100
  25. package/bin/webui/js/panels/knowledge.js +1130 -1006
  26. package/bin/webui/js/panels/overview.js +492 -488
  27. package/package.json +39 -39
  28. package/templates/agents/MODEL-MAPPING.md +163 -158
  29. package/templates/agents/orc-executor-haiku-4-5.md +128 -121
  30. package/templates/agents/orc-executor-opus-4-7-high.md +129 -122
  31. package/templates/agents/orc-executor-opus-4-7-med.md +129 -122
  32. package/templates/agents/orc-executor-opus-4-8-high.md +129 -122
  33. package/templates/agents/orc-executor-opus-5-high.md +129 -122
  34. package/templates/agents/orc-executor-opus-5-low.md +129 -122
  35. package/templates/agents/orc-executor-opus-5-med.md +129 -122
  36. package/templates/agents/orc-executor-sonnet-4-6-high.md +129 -122
  37. package/templates/agents/orc-executor-sonnet-4-6-med.md +129 -122
  38. package/templates/agents/orc-executor-sonnet-5-high.md +129 -122
  39. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +89 -0
  40. package/templates/hooks/README.md +454 -396
  41. package/templates/hooks/orc-graph-hook.js +471 -0
  42. package/templates/hooks/orc-statusline-render.js +922 -921
  43. package/templates/hooks/orc-statusline.js +1596 -1545
  44. package/templates/skills/_shared/README.md +4 -0
  45. package/templates/skills/_shared/code-graph.md +348 -0
  46. package/templates/skills/_shared/opus5-only.md +4 -0
  47. package/templates/skills/_shared/phases/execution.md +168 -147
  48. package/templates/skills/_shared/phases/planning.md +149 -135
  49. package/templates/skills/_shared/phases/preflight.md +132 -118
  50. package/templates/skills/_shared/phases/review.md +63 -53
  51. package/templates/skills/_shared/phases/ship.md +100 -88
  52. package/templates/skills/_shared/phases/trace.md +8 -0
  53. package/templates/skills/_shared/phases/wiki-consult.md +194 -189
  54. package/templates/skills/_shared/read-ladder.md +132 -102
  55. package/templates/skills/_shared/return-validation.md +259 -250
  56. package/templates/skills/orc/SKILL.md +255 -254
  57. package/templates/skills/orc-analyze/SKILL.md +13 -13
  58. package/templates/skills/orc-diy/references/flow-schema.md +101 -100
  59. package/templates/skills/orc-fast/SKILL.md +236 -229
  60. package/templates/skills/orc-mini/SKILL.md +267 -259
  61. package/templates/skills/orc-quick/SKILL.md +378 -361
  62. package/templates/skills/orc-quick/references/dispatch-gate.md +6 -0
  63. package/templates/skills/orc-wiki/references/staleness.md +294 -288
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ // ── orc graph — the RESOLUTION CACHE (v1.8.0 EW2) ───────────────────────────
3
+ //
4
+ // W3 decided "resolution is computed on read, never stored". That was right for
5
+ // correctness and wrong for cost: on django/django ONE file card spent 437 ms
6
+ // working out who calls what, and `impact` up to 520 ms — every single time.
7
+ //
8
+ // EW2 measured what is worth storing, and the answer is narrower than the plan
9
+ // assumed. Two things were built, measured, and thrown away:
10
+ //
11
+ // · the resolver's whole MEMO — 21.7 MB, ~120 ms to read and parse, to save
12
+ // less than that. A net loss. Not stored.
13
+ // · CLOSURE REPAIR (re-resolve only the changed file and its dependents) —
14
+ // measured 2,669 ms against 2,695 ms for a plain full re-resolve on the
15
+ // same one-line edit. **One percent.** On a JSON store the fixed costs
16
+ // (change detection, loading the index, and rewriting the whole file) drown
17
+ // the resolve, so an incremental resolve cannot pay. It was three
18
+ // correctness bugs deep by the time the numbers came in, so it is gone.
19
+ //
20
+ // What IS stored is the REVERSE direction — who calls this symbol — which is
21
+ // the half the 437 ms walk was computing. 13 MB on django/django, and it turns
22
+ // a file card into 458 ms from 727 ms, and `impact` into 397 ms from 631 ms.
23
+ //
24
+ // The rules that keep W3's correctness:
25
+ //
26
+ // 1. `index.json` is still the only SOURCE. `resolved.json` is DERIVED, and
27
+ // throwing it away costs speed and nothing else.
28
+ // 2. Only `update` writes it, inside the same lock. A reader never writes, so
29
+ // readers stay lock-free.
30
+ // 3. A reader uses it ONLY when `resolved.generation === meta.generation`.
31
+ // Any other answer — a crash between two writes, an older engine, a hand
32
+ // deletion — falls back to computing on read. Stale is impossible: it is
33
+ // not "probably fine", it is not used at all.
34
+ // 4. A cached answer must be BYTE-IDENTICAL to a computed one, order
35
+ // included. `test/cli/graph-resolve.test.js` drives a seeded random edit
36
+ // script and compares the two after every step.
37
+ //
38
+ // WHAT IS STORED
39
+ //
40
+ // callers target symbol id → the confident callers of it, as
41
+ // `[caller symbol id, line, state, kind, extra]`, where `kind` is
42
+ // 0 a call · 1 a function passed by name · 2 a URL · 3 an inherited
43
+ // member (v1.8.2, schema 3). `extra` is the URL text for 2 and the
44
+ // alias name (`store` in `store.list()`) for a call through an
45
+ // instance alias; absent otherwise.
46
+ // maybe target symbol id → how many AMBIGUOUS calls list this target, by
47
+ // CALLER FILE. Per file, not per call site: 5 MB on django/django
48
+ // instead of 45 MB, and the card only ever shows the total.
49
+
50
+ const fs = require("fs");
51
+ const path = require("path");
52
+ const G = require("./graph.js");
53
+
54
+ // 3 (v1.8.2 W1): ROUTE rows. A 1.8.1 cache is refused by the schema check and
55
+ // recomputed on read until the next `update` rewrites it — no answer changes.
56
+ const RESOLVE_SCHEMA = 3;
57
+ const CONFIDENT = new Set(["LOCAL", "IMPORT", "UNIQUE", "ROUTE"]);
58
+ const NAMES_PER_KEY = 5;
59
+
60
+ function resolvedPath(claudeDir) {
61
+ return path.join(G.graphPaths(claudeDir).dir, "resolved.json");
62
+ }
63
+ function namesPath(claudeDir) {
64
+ return path.join(G.graphPaths(claudeDir).dir, "names.json");
65
+ }
66
+ // v1.8.2 W3 (D3) — the WIDE-FILE table: a file with many symbols → the few
67
+ // worth naming when somebody is about to read the whole thing. It is its OWN
68
+ // file, not a field on `names.json` or `files.json`, because the hint that
69
+ // reads it is OFF by default and the two hints that are ON must not pay for it.
70
+ function widePath(claudeDir) {
71
+ return path.join(G.graphPaths(claudeDir).dir, "wide.json");
72
+ }
73
+ const WIDE_MIN = 8;
74
+ const WIDE_TOP = 6;
75
+
76
+ // Every map here is keyed by data from the repository — a bare symbol name can
77
+ // be `constructor`, `toString` or `__proto__`. A plain object would hand back
78
+ // `Object.prototype`'s member instead of "nothing here", so maps are built with
79
+ // a null prototype and read through this.
80
+ function own(o, k) {
81
+ return o && Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined;
82
+ }
83
+ const bare = () => Object.create(null);
84
+
85
+ function readJson(file) {
86
+ try {
87
+ return JSON.parse(fs.readFileSync(file, "utf8"));
88
+ } catch (_) {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ // ── build ───────────────────────────────────────────────────────────────────
94
+ // Called by `graphUpdate` INSIDE the lock, after index.json is on disk. It
95
+ // never throws into the update: a cache that cannot be written is a slower
96
+ // graph, not a broken one.
97
+ function build(claudeDir, root, index) {
98
+ const t0 = Date.now();
99
+ const Q = require("./graph-query.js");
100
+ try {
101
+ // `noCache` matters: the writer must COMPUTE, never read back the cache it
102
+ // is about to replace. `index` (S2) is the in-memory index the update just
103
+ // wrote, so the file is parsed once per update, not twice.
104
+ const model = Q.loadModel(claudeDir, root, { noCache: true, index });
105
+ if (!model) return { route: "skipped", reason: "no-index", ms: Date.now() - t0 };
106
+
107
+ const callers = bare();
108
+ const maybe = bare();
109
+ // S1 (v1.8.2 W8): the FORWARD half, for the sharded fast path. Every
110
+ // resolution below is already computed for the reverse map; this records
111
+ // the same answer facing the other way, so `ctx` can read it instead of
112
+ // resolving. UNRESOLVED is counted, never stored — it is most of the calls
113
+ // in a real repository and the card only prints its total.
114
+ const S = require("./graph-shard.js");
115
+ const forward = bare();
116
+ const ambiguous = (r, rel) => {
117
+ for (const cand of r.candidates || []) {
118
+ const m = own(maybe, cand) || (maybe[cand] = bare());
119
+ m[rel] = (own(m, rel) || 0) + 1;
120
+ }
121
+ };
122
+ for (const [rel, f] of Object.entries(model.byFile)) {
123
+ for (const s of f.symbols || []) {
124
+ s.file = rel;
125
+ const calls = s.calls || [];
126
+ const fwd = { u: 0, r: [] };
127
+ const seenName = new Set();
128
+ for (let i = 0; i < calls.length; i++) {
129
+ const c = calls[i];
130
+ const r = Q.resolveCall(model, rel, s, c);
131
+ // `calleesOf` dedupes by call NAME and keeps the first, so the stored
132
+ // rows must be the ones that survive that same dedupe — otherwise a
133
+ // fast card would answer from a row the full card never reaches.
134
+ if (!seenName.has(c.name)) {
135
+ seenName.add(c.name);
136
+ const row = S.forwardRow(i, r);
137
+ if (row) fwd.r.push(row);
138
+ else fwd.u++;
139
+ }
140
+ if (r.target && CONFIDENT.has(r.state)) {
141
+ const kind = c.ref ? 1 : r.inherited ? 3 : 0;
142
+ const row = [s.id, c.line, r.state, kind];
143
+ if (kind === 0 && c.alias) row.push(c.alias);
144
+ (own(callers, r.target) || (callers[r.target] = [])).push(row);
145
+ } else if (r.state === "AMBIGUOUS") ambiguous(r, rel);
146
+ }
147
+ if (fwd.r.length || fwd.u) forward[s.id] = fwd;
148
+ // W1: the URLs this symbol sends, resolved to route symbols.
149
+ for (const u of s.urls || []) {
150
+ const r = Q.resolveUrl(model, u);
151
+ if (r.state === "ROUTE") (own(callers, r.target) || (callers[r.target] = [])).push([s.id, u.line, "ROUTE", 2, `${u.method} ${u.url}`]);
152
+ else if (r.state === "AMBIGUOUS") ambiguous(r, rel);
153
+ }
154
+ }
155
+ }
156
+
157
+ // One stable order, so the same tree always produces the same bytes.
158
+ const out = {
159
+ schema: RESOLVE_SCHEMA,
160
+ engine: G.ENGINE,
161
+ generation: model.meta.generation,
162
+ gen_id: model.meta.gen_id,
163
+ callers: sortedRows(callers),
164
+ maybe: sortedCounts(maybe),
165
+ };
166
+ G.atomicWrite(resolvedPath(claudeDir), JSON.stringify(out));
167
+ writeNames(claudeDir, model, out.callers);
168
+ writeWide(claudeDir, model, out.callers);
169
+ // D4 (v1.8.2 W7): the ranked map is built HERE, from the callers object
170
+ // this pass already holds. Building it anywhere else would mean walking
171
+ // every call a second time to learn the same thing.
172
+ const map = require("./graph-map.js").build(claudeDir, model, out.callers);
173
+ // S1: the shards come LAST. They are the only derived store a reader can do
174
+ // without entirely, so a failure here costs speed and nothing else.
175
+ const shards = S.build(claudeDir, model, out.callers, out.maybe, forward);
176
+ return { route: "full", map, shards, ms: Date.now() - t0 };
177
+ } catch (e) {
178
+ // A cache is an optimisation. Losing it must never lose the update — and a
179
+ // HALF-written one must never be read, so it goes.
180
+ try {
181
+ fs.rmSync(resolvedPath(claudeDir), { force: true });
182
+ fs.rmSync(namesPath(claudeDir), { force: true });
183
+ fs.rmSync(widePath(claudeDir), { force: true });
184
+ fs.rmSync(require("./graph-map.js").mapPath(claudeDir), { force: true });
185
+ fs.rmSync(require("./graph-shard.js").shardDir(claudeDir), { recursive: true, force: true });
186
+ } catch (_) {}
187
+ return { route: "failed", reason: String((e && e.message) || e), ms: Date.now() - t0 };
188
+ }
189
+ }
190
+
191
+ function sortedObject(o) {
192
+ const out = bare();
193
+ for (const k of Object.keys(o).sort()) out[k] = o[k];
194
+ return out;
195
+ }
196
+ function sortedRows(o) {
197
+ const out = bare();
198
+ for (const k of Object.keys(o).sort()) {
199
+ out[k] = o[k].slice().sort((a, b) => String(a[0]).localeCompare(String(b[0])) || a[1] - b[1]);
200
+ }
201
+ return out;
202
+ }
203
+ // target → [[caller file, count], …], both levels in a stable order.
204
+ function sortedCounts(o) {
205
+ const out = bare();
206
+ for (const k of Object.keys(o).sort()) out[k] = Object.keys(o[k]).sort().map((f) => [f, o[k][f]]);
207
+ return out;
208
+ }
209
+
210
+ // ── names.json ──────────────────────────────────────────────────────────────
211
+ // A bare name → the few symbols worth showing for it. This is the ONLY file a
212
+ // delivery hook is allowed to read: a name lookup, no index parse, no resolve.
213
+ function writeNames(claudeDir, model, callers) {
214
+ const out = bare();
215
+ for (const [name, syms] of model.byName) {
216
+ const rows = syms
217
+ .map((s) => ({ s, fan: (own(callers, s.id) || []).length }))
218
+ .sort((a, b) => b.fan - a.fan || a.s.file.localeCompare(b.s.file))
219
+ .slice(0, NAMES_PER_KEY)
220
+ .map(({ s, fan }) => [s.qname, s.kind, s.file, s.lines[0], s.lines[1], fan]);
221
+ if (rows.length) out[name] = rows;
222
+ }
223
+ G.atomicWrite(namesPath(claudeDir), JSON.stringify({ schema: RESOLVE_SCHEMA, generation: model.meta.generation, names: sortedObject(out) }));
224
+ }
225
+
226
+ // A file the extractor found `WIDE_MIN` or more symbols in → the `WIDE_TOP`
227
+ // most important of them, with their ranges. Only wide files are listed: the
228
+ // hint exists to turn a whole-file read into a range read the NEXT time, and a
229
+ // file with five symbols is already a range.
230
+ function writeWide(claudeDir, model, callers) {
231
+ const SIG = require("./graph-signals.js");
232
+ const out = bare();
233
+ for (const [rel, f] of Object.entries(model.byFile)) {
234
+ const syms = (f.symbols || []).filter((s) => s.kind !== "module");
235
+ if (syms.length < WIDE_MIN) continue;
236
+ out[rel] = syms
237
+ .map((s) => ({ s, imp: SIG.importance(model, s, (own(callers, s.id) || []).length) }))
238
+ .sort((a, b) => b.imp - a.imp || a.s.lines[0] - b.s.lines[0])
239
+ .slice(0, WIDE_TOP)
240
+ .map(({ s }) => [s.qname, s.lines[0], s.lines[1]]);
241
+ }
242
+ G.atomicWrite(widePath(claudeDir), JSON.stringify({ schema: RESOLVE_SCHEMA, generation: model.meta.generation, files: sortedObject(out) }));
243
+ }
244
+
245
+ // ── read ────────────────────────────────────────────────────────────────────
246
+ // Used ONLY when the generation matches. Anything else returns null and the
247
+ // reader computes, exactly as it did before this file existed.
248
+ function load(claudeDir, meta) {
249
+ const r = readJson(resolvedPath(claudeDir));
250
+ if (!r || r.schema !== RESOLVE_SCHEMA || r.engine !== G.ENGINE) return null;
251
+ if (!meta || !meta.generation || r.generation !== meta.generation) return null;
252
+ return r;
253
+ }
254
+
255
+ module.exports = { own, RESOLVE_SCHEMA, resolvedPath, namesPath, widePath, WIDE_MIN, WIDE_TOP, build, load };
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ // ── orc graph — S1: SHARDED READS (v1.8.2 W8, DE-I) ─────────────────────────
3
+ //
4
+ // EW0 dropped sharding because the parse (235 ms) was smaller than the resolve
5
+ // (437 ms). EW2 then removed the resolve half by storing the reverse edges, and
6
+ // what is left on django is the parse: a one-symbol `ctx` opens a 23 MB
7
+ // `index.json` and a 15 MB `resolved.json` to answer a question about a handful
8
+ // of records.
9
+ //
10
+ // Measured on django/django (2,977 files, 44,572 symbols):
11
+ //
12
+ // index.json 234 ms ─┐
13
+ // resolved.json 101 ms ├─ 373 ms, every `ctx <symbol>`
14
+ // names.json 37 ms ─┘
15
+ //
16
+ // files.json 3 ms ─┐
17
+ // names.json 37 ms ├─ ~42 ms for the same answer
18
+ // ~11 blobs 1 ms │
19
+ // 1-3 shards 1 ms ─┘
20
+ //
21
+ // THE RULE THAT MAKES THIS SAFE: the fast path answers, byte for byte, what the
22
+ // full model answers — or it does not answer at all. It never approximates.
23
+ // `fastModel()` returns null the moment it cannot PROVE its answer is the same
24
+ // one, and the caller loads the full model exactly as before. Every reason it
25
+ // gives up is named in `reason`, so a fast path that quietly stopped being fast
26
+ // is visible rather than invisible.
27
+ //
28
+ // The place it gives up most is target resolution. `names.json` is capped at
29
+ // `NAMES_PER_KEY` rows per bare name, so a name with six definitions shows five
30
+ // — which means "these are all of them" is only knowable BELOW the cap. Above
31
+ // it, and for a query that resolves through a URL, a file path, a line number
32
+ // or a fuzzy nearest-match, the full model is the only honest answer.
33
+ //
34
+ // WHAT IS STORED — `resolved/<ab>.json`, `<ab>` being the first two hex
35
+ // characters of the file's blob, which is the shard layout `blobs/` already
36
+ // uses:
37
+ //
38
+ // callers target symbol id -> the rows `resolved.json` holds, for targets
39
+ // whose FILE hashes into this shard
40
+ // maybe target symbol id -> the AMBIGUOUS-caller counts, sharded with
41
+ // the callers they belong to. Without it a fast card would print
42
+ // no `maybe` line where the full card prints one, which is a
43
+ // DIFFERENT card, not a shorter one.
44
+ // calls symbol id -> { u, r } for symbols whose OWN file hashes here.
45
+ // `r` is one row per RESOLVED call, positional against the
46
+ // symbol's `calls` array; `u` is how many of its calls resolved to
47
+ // nothing. UNRESOLVED rows are not stored — they are 72% of the
48
+ // calls in a real repository and the card only ever counts them.
49
+ // cands the AMBIGUOUS candidate lists, INTERNED. A name that is ambiguous
50
+ // is ambiguous the same way everywhere it is called, so the same
51
+ // list was being written once per call site: 25.3 MB of django's
52
+ // shards, against 8.2 MB once each. An AMBIGUOUS row carries the
53
+ // INDEX into this table, never the list.
54
+
55
+ const fs = require("fs");
56
+ const path = require("path");
57
+ const G = require("./graph.js");
58
+
59
+ const SHARD_SCHEMA = 1;
60
+ // The state words, as one byte each. The card prints words; the store does not
61
+ // have to repeat them 400,000 times.
62
+ const STATE_CODE = { LOCAL: 0, IMPORT: 1, UNIQUE: 2, ROUTE: 3, AMBIGUOUS: 4 };
63
+ const CODE_STATE = ["LOCAL", "IMPORT", "UNIQUE", "ROUTE", "AMBIGUOUS"];
64
+
65
+ const own = (o, k) => (o && Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined);
66
+
67
+ // Thrown when the fast path discovers, HALFWAY through a card, that it cannot
68
+ // answer identically after all. The caller catches it and re-answers from the
69
+ // full model — the same fallback every other decline takes, just later.
70
+ class FastUnavailable extends Error {}
71
+
72
+ function shardDir(claudeDir) {
73
+ return path.join(G.graphPaths(claudeDir).dir, "resolved");
74
+ }
75
+ function shardOf(blob) {
76
+ return String(blob || "").slice(0, 2) || "00";
77
+ }
78
+ function shardPath(claudeDir, ab) {
79
+ return path.join(shardDir(claudeDir), `${ab}.json`);
80
+ }
81
+
82
+ function readJson(file) {
83
+ try {
84
+ return JSON.parse(fs.readFileSync(file, "utf8"));
85
+ } catch (_) {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ // ── build (called by `update`, inside the lock) ─────────────────────────────
91
+ // `callers` is the object `graph-resolve.build` already produced. `calls` is
92
+ // the forward half, collected in the SAME pass — every resolution is computed
93
+ // once and written twice, never computed twice.
94
+ function build(claudeDir, model, callers, maybe, calls) {
95
+ const t0 = Date.now();
96
+ const dir = shardDir(claudeDir);
97
+ try {
98
+ // A shard set from another generation must never be half-replaced. The
99
+ // whole directory goes, then the new one is written.
100
+ fs.rmSync(dir, { recursive: true, force: true });
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ const buckets = new Map();
103
+ const bucket = (rel) => {
104
+ const ab = shardOf((model.byFile[rel] || {}).blob);
105
+ let b = buckets.get(ab);
106
+ if (!b) buckets.set(ab, (b = { callers: Object.create(null), maybe: Object.create(null), calls: Object.create(null), cands: [], candIdx: new Map() }));
107
+ return b;
108
+ };
109
+ for (const [target, rows] of Object.entries(callers)) {
110
+ const rel = String(target).split("#")[0];
111
+ if (model.byFile[rel]) bucket(rel).callers[target] = rows;
112
+ }
113
+ for (const [target, rows] of Object.entries(maybe)) {
114
+ const rel = String(target).split("#")[0];
115
+ if (model.byFile[rel]) bucket(rel).maybe[target] = rows;
116
+ }
117
+ for (const [id, rec] of Object.entries(calls)) {
118
+ const rel = String(id).split("#")[0];
119
+ if (!model.byFile[rel]) continue;
120
+ const b = bucket(rel);
121
+ // The interning happens HERE, per shard, because a shard is the unit a
122
+ // reader parses: a table shared across shards would mean reading a second
123
+ // file to render one row.
124
+ b.calls[id] = {
125
+ u: rec.u,
126
+ r: rec.r.map((row) => {
127
+ if (row[1] !== STATE_CODE.AMBIGUOUS) return row;
128
+ const key = (row[4] || []).join("\u0000");
129
+ let at = b.candIdx.get(key);
130
+ if (at === undefined) {
131
+ at = b.cands.length;
132
+ b.cands.push(row[4] || []);
133
+ b.candIdx.set(key, at);
134
+ }
135
+ return [row[0], row[1], null, 0, at];
136
+ }),
137
+ };
138
+ }
139
+ let bytes = 0;
140
+ for (const [ab, b] of buckets) {
141
+ const body = JSON.stringify({
142
+ schema: SHARD_SCHEMA,
143
+ engine: G.ENGINE,
144
+ generation: model.meta.generation,
145
+ callers: b.callers,
146
+ maybe: b.maybe,
147
+ cands: b.cands,
148
+ calls: b.calls,
149
+ });
150
+ bytes += body.length;
151
+ G.atomicWrite(shardPath(claudeDir, ab), body);
152
+ }
153
+ return { route: "full", shards: buckets.size, bytes, ms: Date.now() - t0 };
154
+ } catch (e) {
155
+ // A shard set that cannot be written is a slower `ctx`, never a wrong one.
156
+ try {
157
+ fs.rmSync(dir, { recursive: true, force: true });
158
+ } catch (_) {}
159
+ return { route: "failed", reason: String((e && e.message) || e), ms: Date.now() - t0 };
160
+ }
161
+ }
162
+
163
+ // One forward row: the call's INDEX in the symbol's own `calls` array, its
164
+ // state, its target, and — only for AMBIGUOUS — its candidate list.
165
+ function forwardRow(i, res) {
166
+ const code = STATE_CODE[res.state];
167
+ if (code === undefined) return null; // UNRESOLVED — counted, never stored
168
+ if (res.state === "AMBIGUOUS") return [i, code, null, 0, res.candidates || []];
169
+ return [i, code, res.target, res.inherited ? 1 : 0];
170
+ }
171
+
172
+ // ── read ────────────────────────────────────────────────────────────────────
173
+ // A mini-model that answers `ctx <symbol>` exactly as the full one does, or
174
+ // null with the reason it could not.
175
+ const NAMES_PER_KEY = 5;
176
+ const norm = (p) => String(p).split("\\").join("/").replace(/^\.\//, "");
177
+
178
+ function fastModel(claudeDir, root, query) {
179
+ const p = G.graphPaths(claudeDir);
180
+ const meta = readJson(p.meta);
181
+ if (!meta || !meta.generation) return { model: null, reason: "no-meta" };
182
+ if (!fs.existsSync(shardDir(claudeDir))) return { model: null, reason: "no-shards" };
183
+
184
+ const q = norm(query);
185
+ // Everything the fast path cannot prove it would answer identically. Each of
186
+ // these needs a whole-repository walk that `names.json` cannot stand in for.
187
+ if (q.includes("/")) return { model: null, reason: "path-or-url" };
188
+ if (/:\d+$/.test(q)) return { model: null, reason: "file-line" };
189
+ if (!q || /\s/.test(q)) return { model: null, reason: "not-a-plain-name" };
190
+
191
+ const files = readJson(p.files);
192
+ if (!files) return { model: null, reason: "no-files" };
193
+ const namesFile = readJson(require("./graph-resolve.js").namesPath(claudeDir));
194
+ if (!namesFile || namesFile.generation !== meta.generation) return { model: null, reason: "names-stale" };
195
+
196
+ const bare = q.split(".").pop();
197
+ const rows = own(namesFile.names, bare);
198
+ if (!rows) return { model: null, reason: "name-unknown" };
199
+ // AT the cap, "these are all of them" stops being knowable. Below it, it is.
200
+ if (rows.length >= NAMES_PER_KEY) return { model: null, reason: "name-at-cap" };
201
+ const hits = rows.filter((r) => r[0] === q);
202
+ if (hits.length !== 1) return { model: null, reason: hits.length ? "ambiguous-qname" : "qname-not-exact" };
203
+
204
+ const rel = hits[0][2];
205
+ const rec = files[rel];
206
+ if (!rec) return { model: null, reason: "file-unknown" };
207
+
208
+ const loaded = Object.create(null);
209
+ const loadFile = (r) => {
210
+ if (own(loaded, r) !== undefined) return loaded[r];
211
+ const f = files[r];
212
+ // A path the index never held is not an error — the full model would not
213
+ // know it either, and both answer the same "no".
214
+ if (!f) return (loaded[r] = null);
215
+ const blob = readJson(path.join(p.blobs, f.blob.slice(0, 2), `${f.blob}.json`));
216
+ // A file the index DOES hold, whose blob will not read, is different: the
217
+ // full model has that file's symbols from `index.json` and would print a
218
+ // row this path would drop. Dropping a row is a different card, so this
219
+ // throws and the caller starts again with the full model.
220
+ if (!blob) throw new FastUnavailable("blob-unreadable:" + r);
221
+ for (const s of blob.symbols || []) s.file = r;
222
+ return (loaded[r] = { ...blob, blob: f.blob });
223
+ };
224
+
225
+ let home;
226
+ try {
227
+ home = loadFile(rel);
228
+ } catch (e) {
229
+ return { model: null, reason: e instanceof FastUnavailable ? e.message : "blob-missing" };
230
+ }
231
+ if (!home) return { model: null, reason: "blob-missing" };
232
+ const mine = (home.symbols || []).filter((s) => s.kind !== "module" && s.qname === q);
233
+ // Two symbols with the same qname in one file (a Go build tag, a Python
234
+ // redefinition) are a case the full model settles by width. It is rare and it
235
+ // is not worth a second implementation.
236
+ if (mine.length !== 1) return { model: null, reason: mine.length ? "duplicate-qname" : "not-in-blob" };
237
+ const sym = mine[0];
238
+
239
+ const shards = new Map();
240
+ const shardFor = (r) => {
241
+ const f = files[r];
242
+ if (!f) return null;
243
+ const ab = shardOf(f.blob);
244
+ if (shards.has(ab)) return shards.get(ab);
245
+ const s = readJson(shardPath(claudeDir, ab));
246
+ const ok = s && s.schema === SHARD_SCHEMA && s.engine === G.ENGINE && s.generation === meta.generation ? s : null;
247
+ shards.set(ab, ok);
248
+ return ok;
249
+ };
250
+ if (!shardFor(rel)) return { model: null, reason: "shard-stale" };
251
+
252
+ // `byFile` and `symbols` are LAZY: a blob is read the first time something
253
+ // asks about the file it holds, and never again. A card about one symbol on
254
+ // django touches about a dozen files, out of nearly three thousand.
255
+ const byFile = new Proxy(Object.create(null), {
256
+ get: (_, r) => (typeof r === "string" ? loadFile(r) || undefined : undefined),
257
+ has: (_, r) => typeof r === "string" && !!files[r],
258
+ });
259
+ const symbols = {
260
+ get(id) {
261
+ const f = loadFile(String(id).split("#")[0]);
262
+ if (!f) return undefined;
263
+ return (f.symbols || []).find((s) => s.id === id);
264
+ },
265
+ has(id) {
266
+ return this.get(id) !== undefined;
267
+ },
268
+ // Nothing on the fast path may walk every symbol — that is exactly the
269
+ // whole-repository question this path exists to avoid. A caller that needs
270
+ // it has already been sent to the full model.
271
+ values() {
272
+ throw new Error("graph-shard: the fast model has no symbol table to walk");
273
+ },
274
+ };
275
+
276
+ // The target's own shard holds its callers AND its ambiguous-caller counts.
277
+ // `ctx` asks for callers of the TARGET and of nothing else, so one shard is
278
+ // the whole reverse half of this card.
279
+ const s0 = shardFor(rel);
280
+ const callersCache = Object.create(null);
281
+ const maybeCache = Object.create(null);
282
+ for (const [k, v] of Object.entries(s0.callers)) callersCache[k] = v;
283
+ for (const [k, v] of Object.entries(s0.maybe || {})) maybeCache[k] = v;
284
+
285
+ return {
286
+ model: {
287
+ root,
288
+ meta,
289
+ fast: true,
290
+ notes: require("./graph-notes.js").noteIndex(claudeDir),
291
+ byFile,
292
+ fileSet: new Set(Object.keys(files)),
293
+ symbols,
294
+ byName: new Map(),
295
+ bindings: new Map(),
296
+ resolved: new Map(),
297
+ callersCache,
298
+ maybeCache,
299
+ generation: meta.generation,
300
+ suffix: new Map(),
301
+ dirs: null,
302
+ goModule: undefined,
303
+ callIndex: null,
304
+ routes: null,
305
+ urlIndex: null,
306
+ urlMemo: new Map(),
307
+ blobStates: new Map(),
308
+ // The forward half. `calleesOf` reads this instead of resolving, and a
309
+ // symbol whose shard is missing sends the whole read back to the full
310
+ // model rather than answering with fewer calls than it has.
311
+ callsFor(s) {
312
+ const sh = shardFor(String(s.id).split("#")[0]);
313
+ if (!sh) return null;
314
+ const rec = own(sh.calls, s.id) || { u: 0, r: [] };
315
+ // The candidate table travels WITH the rows, so the caller never has to
316
+ // know which shard a row came out of.
317
+ return { u: rec.u, r: rec.r, cands: sh.cands || [] };
318
+ },
319
+ },
320
+ sym,
321
+ reason: null,
322
+ };
323
+ }
324
+
325
+ module.exports = { SHARD_SCHEMA, STATE_CODE, CODE_STATE, FastUnavailable, shardDir, shardPath, shardOf, build, forwardRow, fastModel };