@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.
- package/CHANGELOG.md +3869 -3381
- package/README-id.md +942 -844
- package/README.md +882 -788
- package/bin/build-agents.js +43 -27
- package/bin/cli.js +45460 -44169
- package/bin/graph-extract.js +3216 -0
- package/bin/graph-gain.js +404 -0
- package/bin/graph-map.js +232 -0
- package/bin/graph-notes.js +229 -0
- package/bin/graph-query.js +1770 -0
- package/bin/graph-resolve.js +255 -0
- package/bin/graph-shard.js +325 -0
- package/bin/graph-signals.js +277 -0
- package/bin/graph.js +658 -0
- package/bin/verify-contracts.js +4809 -4553
- package/bin/verify-package.js +640 -616
- package/bin/webui/api.js +1425 -1414
- package/bin/webui/fixtures/index.js +584 -576
- package/bin/webui/fixtures/knowledge.js +356 -291
- package/bin/webui/fixtures/stats.js +107 -104
- package/bin/webui/i18n/en/knowledge.json +182 -151
- package/bin/webui/i18n/en/overview.json +101 -100
- package/bin/webui/i18n/id/knowledge.json +182 -151
- package/bin/webui/i18n/id/overview.json +101 -100
- package/bin/webui/js/panels/knowledge.js +1130 -1006
- package/bin/webui/js/panels/overview.js +492 -488
- package/package.json +39 -39
- package/templates/agents/MODEL-MAPPING.md +163 -158
- package/templates/agents/orc-executor-haiku-4-5.md +128 -121
- package/templates/agents/orc-executor-opus-4-7-high.md +129 -122
- package/templates/agents/orc-executor-opus-4-7-med.md +129 -122
- package/templates/agents/orc-executor-opus-4-8-high.md +129 -122
- package/templates/agents/orc-executor-opus-5-high.md +129 -122
- package/templates/agents/orc-executor-opus-5-low.md +129 -122
- package/templates/agents/orc-executor-opus-5-med.md +129 -122
- package/templates/agents/orc-executor-sonnet-4-6-high.md +129 -122
- package/templates/agents/orc-executor-sonnet-4-6-med.md +129 -122
- package/templates/agents/orc-executor-sonnet-5-high.md +129 -122
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +89 -0
- package/templates/hooks/README.md +454 -396
- package/templates/hooks/orc-graph-hook.js +471 -0
- package/templates/hooks/orc-statusline-render.js +922 -921
- package/templates/hooks/orc-statusline.js +1596 -1545
- package/templates/skills/_shared/README.md +4 -0
- package/templates/skills/_shared/code-graph.md +348 -0
- package/templates/skills/_shared/opus5-only.md +4 -0
- package/templates/skills/_shared/phases/execution.md +168 -147
- package/templates/skills/_shared/phases/planning.md +149 -135
- package/templates/skills/_shared/phases/preflight.md +132 -118
- package/templates/skills/_shared/phases/review.md +63 -53
- package/templates/skills/_shared/phases/ship.md +100 -88
- package/templates/skills/_shared/phases/trace.md +8 -0
- package/templates/skills/_shared/phases/wiki-consult.md +194 -189
- package/templates/skills/_shared/read-ladder.md +132 -102
- package/templates/skills/_shared/return-validation.md +259 -250
- package/templates/skills/orc/SKILL.md +255 -254
- package/templates/skills/orc-analyze/SKILL.md +13 -13
- package/templates/skills/orc-diy/references/flow-schema.md +101 -100
- package/templates/skills/orc-fast/SKILL.md +236 -229
- package/templates/skills/orc-mini/SKILL.md +267 -259
- package/templates/skills/orc-quick/SKILL.md +378 -361
- package/templates/skills/orc-quick/references/dispatch-gate.md +6 -0
- 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 };
|