@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.
- package/CHANGELOG.md +220 -0
- package/README-id.md +90 -71
- package/README.md +77 -31
- package/bin/cli.js +45460 -44867
- package/bin/graph-extract.js +2409 -120
- package/bin/graph-gain.js +404 -0
- package/bin/graph-map.js +232 -0
- package/bin/graph-notes.js +49 -8
- package/bin/graph-query.js +1770 -808
- package/bin/graph-resolve.js +93 -16
- package/bin/graph-shard.js +325 -0
- package/bin/graph.js +658 -605
- package/bin/verify-contracts.js +140 -0
- package/bin/verify-package.js +14 -0
- package/bin/webui/api.js +6 -0
- package/bin/webui/fixtures/index.js +6 -1
- package/bin/webui/fixtures/knowledge.js +41 -1
- package/bin/webui/fixtures/stats.js +107 -104
- package/bin/webui/i18n/en/knowledge.json +16 -1
- package/bin/webui/i18n/id/knowledge.json +16 -1
- package/bin/webui/js/panels/knowledge.js +68 -3
- package/package.json +1 -1
- package/templates/agents/orc-executor-haiku-4-5.md +8 -13
- package/templates/agents/orc-executor-opus-4-7-high.md +8 -13
- package/templates/agents/orc-executor-opus-4-7-med.md +8 -13
- package/templates/agents/orc-executor-opus-4-8-high.md +8 -13
- package/templates/agents/orc-executor-opus-5-high.md +8 -13
- package/templates/agents/orc-executor-opus-5-low.md +8 -13
- package/templates/agents/orc-executor-opus-5-med.md +8 -13
- package/templates/agents/orc-executor-sonnet-4-6-high.md +8 -13
- package/templates/agents/orc-executor-sonnet-4-6-med.md +8 -13
- package/templates/agents/orc-executor-sonnet-5-high.md +8 -13
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +15 -12
- package/templates/hooks/README.md +13 -3
- package/templates/hooks/orc-graph-hook.js +148 -13
- package/templates/skills/_shared/code-graph.md +148 -20
- package/templates/skills/_shared/phases/execution.md +13 -11
- package/templates/skills/_shared/phases/planning.md +8 -1
- package/templates/skills/_shared/phases/ship.md +5 -1
- package/templates/skills/_shared/phases/trace.md +2 -0
- package/templates/skills/_shared/phases/wiki-consult.md +1 -1
- package/templates/skills/_shared/read-ladder.md +10 -2
- package/templates/skills/orc/SKILL.md +1 -1
- package/templates/skills/orc-analyze/SKILL.md +13 -13
- package/templates/skills/orc-diy/references/flow-schema.md +1 -1
- package/templates/skills/orc-wiki/references/staleness.md +1 -1
package/bin/graph-resolve.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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 };
|