@azure-id/orc 1.8.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 (46) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/README-id.md +90 -71
  3. package/README.md +77 -31
  4. package/bin/cli.js +45460 -44867
  5. package/bin/graph-extract.js +2409 -120
  6. package/bin/graph-gain.js +404 -0
  7. package/bin/graph-map.js +232 -0
  8. package/bin/graph-notes.js +49 -8
  9. package/bin/graph-query.js +1770 -808
  10. package/bin/graph-resolve.js +93 -16
  11. package/bin/graph-shard.js +325 -0
  12. package/bin/graph.js +658 -605
  13. package/bin/verify-contracts.js +140 -0
  14. package/bin/verify-package.js +14 -0
  15. package/bin/webui/api.js +6 -0
  16. package/bin/webui/fixtures/index.js +6 -1
  17. package/bin/webui/fixtures/knowledge.js +41 -1
  18. package/bin/webui/fixtures/stats.js +107 -104
  19. package/bin/webui/i18n/en/knowledge.json +16 -1
  20. package/bin/webui/i18n/id/knowledge.json +16 -1
  21. package/bin/webui/js/panels/knowledge.js +68 -3
  22. package/package.json +1 -1
  23. package/templates/agents/orc-executor-haiku-4-5.md +8 -13
  24. package/templates/agents/orc-executor-opus-4-7-high.md +8 -13
  25. package/templates/agents/orc-executor-opus-4-7-med.md +8 -13
  26. package/templates/agents/orc-executor-opus-4-8-high.md +8 -13
  27. package/templates/agents/orc-executor-opus-5-high.md +8 -13
  28. package/templates/agents/orc-executor-opus-5-low.md +8 -13
  29. package/templates/agents/orc-executor-opus-5-med.md +8 -13
  30. package/templates/agents/orc-executor-sonnet-4-6-high.md +8 -13
  31. package/templates/agents/orc-executor-sonnet-4-6-med.md +8 -13
  32. package/templates/agents/orc-executor-sonnet-5-high.md +8 -13
  33. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +15 -12
  34. package/templates/hooks/README.md +13 -3
  35. package/templates/hooks/orc-graph-hook.js +148 -13
  36. package/templates/skills/_shared/code-graph.md +148 -20
  37. package/templates/skills/_shared/phases/execution.md +13 -11
  38. package/templates/skills/_shared/phases/planning.md +8 -1
  39. package/templates/skills/_shared/phases/ship.md +5 -1
  40. package/templates/skills/_shared/phases/trace.md +2 -0
  41. package/templates/skills/_shared/phases/wiki-consult.md +1 -1
  42. package/templates/skills/_shared/read-ladder.md +10 -2
  43. package/templates/skills/orc/SKILL.md +1 -1
  44. package/templates/skills/orc-analyze/SKILL.md +13 -13
  45. package/templates/skills/orc-diy/references/flow-schema.md +1 -1
  46. package/templates/skills/orc-wiki/references/staleness.md +1 -1
@@ -38,7 +38,11 @@
38
38
  // WHAT IS STORED
39
39
  //
40
40
  // callers target symbol id → the confident callers of it, as
41
- // `[caller symbol id, line, state, ref]`.
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.
42
46
  // maybe target symbol id → how many AMBIGUOUS calls list this target, by
43
47
  // CALLER FILE. Per file, not per call site: 5 MB on django/django
44
48
  // instead of 45 MB, and the card only ever shows the total.
@@ -47,8 +51,10 @@ const fs = require("fs");
47
51
  const path = require("path");
48
52
  const G = require("./graph.js");
49
53
 
50
- const RESOLVE_SCHEMA = 2;
51
- const CONFIDENT = new Set(["LOCAL", "IMPORT", "UNIQUE"]);
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"]);
52
58
  const NAMES_PER_KEY = 5;
53
59
 
54
60
  function resolvedPath(claudeDir) {
@@ -57,6 +63,15 @@ function resolvedPath(claudeDir) {
57
63
  function namesPath(claudeDir) {
58
64
  return path.join(G.graphPaths(claudeDir).dir, "names.json");
59
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;
60
75
 
61
76
  // Every map here is keyed by data from the repository — a bare symbol name can
62
77
  // be `constructor`, `toString` or `__proto__`. A plain object would hand back
@@ -79,30 +94,62 @@ function readJson(file) {
79
94
  // Called by `graphUpdate` INSIDE the lock, after index.json is on disk. It
80
95
  // never throws into the update: a cache that cannot be written is a slower
81
96
  // graph, not a broken one.
82
- function build(claudeDir, root) {
97
+ function build(claudeDir, root, index) {
83
98
  const t0 = Date.now();
84
99
  const Q = require("./graph-query.js");
85
100
  try {
86
101
  // `noCache` matters: the writer must COMPUTE, never read back the cache it
87
- // is about to replace.
88
- const model = Q.loadModel(claudeDir, root, { noCache: true });
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 });
89
105
  if (!model) return { route: "skipped", reason: "no-index", ms: Date.now() - t0 };
90
106
 
91
107
  const callers = bare();
92
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
+ };
93
122
  for (const [rel, f] of Object.entries(model.byFile)) {
94
123
  for (const s of f.symbols || []) {
95
124
  s.file = rel;
96
- for (const c of s.calls || []) {
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];
97
130
  const r = Q.resolveCall(model, rel, s, c);
98
- if (r.target && CONFIDENT.has(r.state)) {
99
- (own(callers, r.target) || (callers[r.target] = [])).push([s.id, c.line, r.state, c.ref ? 1 : 0]);
100
- } else if (r.state === "AMBIGUOUS") {
101
- for (const cand of r.candidates || []) {
102
- const m = own(maybe, cand) || (maybe[cand] = bare());
103
- m[rel] = (own(m, rel) || 0) + 1;
104
- }
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++;
105
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);
106
153
  }
107
154
  }
108
155
  }
@@ -118,13 +165,24 @@ function build(claudeDir, root) {
118
165
  };
119
166
  G.atomicWrite(resolvedPath(claudeDir), JSON.stringify(out));
120
167
  writeNames(claudeDir, model, out.callers);
121
- return { route: "full", ms: Date.now() - t0 };
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 };
122
177
  } catch (e) {
123
178
  // A cache is an optimisation. Losing it must never lose the update — and a
124
179
  // HALF-written one must never be read, so it goes.
125
180
  try {
126
181
  fs.rmSync(resolvedPath(claudeDir), { force: true });
127
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 });
128
186
  } catch (_) {}
129
187
  return { route: "failed", reason: String((e && e.message) || e), ms: Date.now() - t0 };
130
188
  }
@@ -165,6 +223,25 @@ function writeNames(claudeDir, model, callers) {
165
223
  G.atomicWrite(namesPath(claudeDir), JSON.stringify({ schema: RESOLVE_SCHEMA, generation: model.meta.generation, names: sortedObject(out) }));
166
224
  }
167
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
+
168
245
  // ── read ────────────────────────────────────────────────────────────────────
169
246
  // Used ONLY when the generation matches. Anything else returns null and the
170
247
  // reader computes, exactly as it did before this file existed.
@@ -175,4 +252,4 @@ function load(claudeDir, meta) {
175
252
  return r;
176
253
  }
177
254
 
178
- module.exports = { own, RESOLVE_SCHEMA, resolvedPath, namesPath, build, load };
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 };