@event4u/agent-config 17.3.0 → 17.4.0
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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +164 -342
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +175 -183
- package/dist/cli-delegate/cmd_doctor.js +5 -5
- package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
- package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
- package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
- package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
- package/dist/hooks/dispatch.js +982 -929
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +56 -6
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/hooks/update_notice.chunk.cjs +914 -0
- package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +71259 -66437
- package/dist/scripts/condense.js +9 -6
- package/package.json +4 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/hook-bundle-budget.json +31 -6
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/config/sigstore/npm_keys.json +50 -0
- package/src/config/sigstore/trust-material.json +9 -0
- package/src/config/sigstore/trusted_root.json +126 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_cli/cmd_update.ts +48 -20
- package/src/scripts/_cli/cmd_upgrade.ts +7 -1
- package/src/scripts/_cli/upgrade_unattended.ts +90 -76
- package/src/scripts/_dispatch.bash +3 -2
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_text.ts +5 -3
- package/src/scripts/_lib/egress_token.ts +7 -5
- package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
- package/src/scripts/_lib/git_common_dir.ts +10 -0
- package/src/scripts/_lib/graph_feeder_record.ts +3 -2
- package/src/scripts/_lib/install_reach_checks.ts +32 -2
- package/src/scripts/_lib/lineage_backfill.ts +136 -0
- package/src/scripts/_lib/lineage_ledger.ts +211 -0
- package/src/scripts/_lib/lineage_map.ts +55 -0
- package/src/scripts/_lib/lineage_query.ts +140 -0
- package/src/scripts/_lib/lineage_seed.ts +116 -0
- package/src/scripts/_lib/lineage_watch.ts +249 -0
- package/src/scripts/_lib/link_crypto.ts +8 -4
- package/src/scripts/_lib/production_reach.ts +3 -3
- package/src/scripts/_lib/project_registry.ts +227 -0
- package/src/scripts/_lib/provenance_verify.ts +14 -242
- package/src/scripts/_lib/sigstore_provenance.ts +177 -0
- package/src/scripts/_lib/source_id.ts +48 -0
- package/src/scripts/_lib/source_shape.ts +4 -0
- package/src/scripts/_lib/unattended_gates.ts +6 -1
- package/src/scripts/advisories.ts +26 -10
- package/src/scripts/ai_council/council_egress.ts +5 -6
- package/src/scripts/bench_graph_feeder_latency.ts +10 -3
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_egress_mutations.ts +1 -0
- package/src/scripts/code_graph/build.ts +67 -3
- package/src/scripts/code_graph/cli.ts +131 -28
- package/src/scripts/code_graph/conventions_default.ts +20 -1
- package/src/scripts/code_graph/detect.ts +5 -5
- package/src/scripts/code_graph/estate_cli.ts +135 -0
- package/src/scripts/code_graph/estate_index.ts +394 -0
- package/src/scripts/code_graph/estate_manifests.ts +223 -0
- package/src/scripts/code_graph/estate_map.ts +141 -0
- package/src/scripts/code_graph/extract.ts +4 -1
- package/src/scripts/code_graph/extract_more.ts +137 -2
- package/src/scripts/code_graph/graph_budget.ts +158 -0
- package/src/scripts/code_graph/graph_home.ts +80 -0
- package/src/scripts/code_graph/lineage_cli.ts +315 -0
- package/src/scripts/code_graph/map.ts +22 -13
- package/src/scripts/code_graph/map_model.ts +67 -12
- package/src/scripts/code_graph/map_verify.ts +68 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
- package/src/scripts/code_graph/project_ref.ts +25 -0
- package/src/scripts/code_graph/projects_cli.ts +254 -0
- package/src/scripts/code_graph/providers/catalog.ts +14 -5
- package/src/scripts/code_graph/providers/django.ts +52 -9
- package/src/scripts/code_graph/providers/index.ts +2 -0
- package/src/scripts/code_graph/providers/rails.ts +204 -0
- package/src/scripts/code_graph/providers/spring.ts +38 -1
- package/src/scripts/code_graph/query.ts +9 -9
- package/src/scripts/code_graph/refresh_lock.ts +12 -8
- package/src/scripts/code_graph/repo_identity.ts +169 -0
- package/src/scripts/code_graph/sqlite_store.ts +22 -25
- package/src/scripts/code_graph/types.ts +6 -2
- package/src/scripts/condense.ts +6 -6
- package/src/scripts/hook_chunks.mjs +7 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +20 -17
- package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
- package/src/scripts/hooks/dispatch_hook.ts +6 -7
- package/src/scripts/hooks/host_semantics.ts +72 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
- package/src/scripts/mcp_server/graph_tools.ts +161 -41
- package/src/scripts/network_sources.ts +24 -6
- package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- package/src/vendor/grammars/README.md +7 -6
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a project's manifests declare and publish, for the estate index
|
|
3
|
+
* (ADR-299, step 4.2). Read from the same manifest set the providers detect a
|
|
4
|
+
* stack by (`readProjectProbe`), plus `Cargo.toml`. Line-level parsing on
|
|
5
|
+
* purpose: the index needs package names, not a resolver, and a malformed
|
|
6
|
+
* manifest costs its own rows rather than the refresh.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
13
|
+
import { MANIFEST_NAME, readProjectProbe, walkedPath } from './build.js';
|
|
14
|
+
|
|
15
|
+
export type Ecosystem = 'npm' | 'composer' | 'pypi' | 'go' | 'cargo' | 'rubygems';
|
|
16
|
+
|
|
17
|
+
export interface Declared {
|
|
18
|
+
ecosystem: Ecosystem;
|
|
19
|
+
package: string;
|
|
20
|
+
manifest: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Published {
|
|
24
|
+
ecosystem: Ecosystem;
|
|
25
|
+
package: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ManifestFacts {
|
|
29
|
+
declares: Declared[];
|
|
30
|
+
publishes: Published[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PY_NAME = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/;
|
|
34
|
+
|
|
35
|
+
/** PEP 503 normalisation, so `Foo_Bar` and `foo-bar` are one package. */
|
|
36
|
+
function pyName(n: string): string {
|
|
37
|
+
return n.toLowerCase().replace(/[-_.]+/g, '-');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function json(text: string): Record<string, unknown> | null {
|
|
41
|
+
try {
|
|
42
|
+
const v = JSON.parse(text) as unknown;
|
|
43
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function keys(v: unknown): string[] {
|
|
50
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? Object.keys(v) : [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `[section]` → its lines, for the flat TOML tables manifests use. */
|
|
54
|
+
function tomlSections(text: string): Map<string, string[]> {
|
|
55
|
+
const out = new Map<string, string[]>();
|
|
56
|
+
let cur = '';
|
|
57
|
+
out.set(cur, []);
|
|
58
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
59
|
+
const line = raw.replace(/#.*$/, '').trimEnd();
|
|
60
|
+
const head = /^\s*\[\s*([^\]]+?)\s*\]\s*$/.exec(line);
|
|
61
|
+
if (head) {
|
|
62
|
+
cur = head[1] as string;
|
|
63
|
+
if (!out.has(cur)) out.set(cur, []);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
out.get(cur)?.push(line);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tomlKeys(lines: readonly string[] | undefined): string[] {
|
|
72
|
+
const out: string[] = [];
|
|
73
|
+
for (const l of lines ?? []) {
|
|
74
|
+
const m = /^\s*([A-Za-z0-9_.-]+|"[^"]+")\s*=/.exec(l);
|
|
75
|
+
if (m) out.push((m[1] as string).replace(/^"|"$/g, ''));
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function tomlString(lines: readonly string[] | undefined, key: string): string | null {
|
|
81
|
+
for (const l of lines ?? []) {
|
|
82
|
+
const m = new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']+)["']`).exec(l);
|
|
83
|
+
if (m) return m[1] as string;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The quoted entries of a TOML array that may span lines (`dependencies = [ … ]`). */
|
|
89
|
+
function tomlArray(lines: readonly string[] | undefined, key: string): string[] {
|
|
90
|
+
const body = (lines ?? []).join('\n');
|
|
91
|
+
const m = new RegExp(`^\\s*${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`, 'm').exec(body);
|
|
92
|
+
if (!m) return [];
|
|
93
|
+
return [...(m[1] as string).matchAll(/["']([^"']+)["']/g)].map((x) => x[1] as string);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parse(rel: string, text: string, facts: ManifestFacts): void {
|
|
97
|
+
const name = path.posix.basename(rel);
|
|
98
|
+
const dec = (ecosystem: Ecosystem, pkg: string): void => {
|
|
99
|
+
if (pkg) facts.declares.push({ ecosystem, package: pkg, manifest: rel });
|
|
100
|
+
};
|
|
101
|
+
const pub = (ecosystem: Ecosystem, pkg: unknown): void => {
|
|
102
|
+
if (typeof pkg === 'string' && pkg) facts.publishes.push({ ecosystem, package: pkg });
|
|
103
|
+
};
|
|
104
|
+
if (name === 'package.json') {
|
|
105
|
+
const j = json(text);
|
|
106
|
+
if (!j) return;
|
|
107
|
+
pub('npm', j['name']);
|
|
108
|
+
for (const f of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) for (const k of keys(j[f])) dec('npm', k);
|
|
109
|
+
} else if (name === 'composer.json') {
|
|
110
|
+
const j = json(text);
|
|
111
|
+
if (!j) return;
|
|
112
|
+
pub('composer', typeof j['name'] === 'string' ? j['name'].toLowerCase() : null);
|
|
113
|
+
for (const f of ['require', 'require-dev']) {
|
|
114
|
+
for (const k of keys(j[f])) if (k !== 'php' && !k.startsWith('ext-') && !k.startsWith('lib-')) dec('composer', k.toLowerCase());
|
|
115
|
+
}
|
|
116
|
+
} else if (name === 'pyproject.toml') {
|
|
117
|
+
const s = tomlSections(text);
|
|
118
|
+
const own = tomlString(s.get('project'), 'name') ?? tomlString(s.get('tool.poetry'), 'name');
|
|
119
|
+
if (own) pub('pypi', pyName(own));
|
|
120
|
+
for (const d of tomlArray(s.get('project'), 'dependencies')) {
|
|
121
|
+
const m = PY_NAME.exec(d);
|
|
122
|
+
if (m) dec('pypi', pyName(m[1] as string));
|
|
123
|
+
}
|
|
124
|
+
for (const sec of ['tool.poetry.dependencies', 'tool.poetry.dev-dependencies', 'tool.poetry.group.dev.dependencies']) {
|
|
125
|
+
for (const k of tomlKeys(s.get(sec))) if (k !== 'python') dec('pypi', pyName(k));
|
|
126
|
+
}
|
|
127
|
+
} else if (/^requirements[\w.-]*\.txt$/.test(name)) {
|
|
128
|
+
for (const line of text.split(/\r?\n/)) {
|
|
129
|
+
const l = line.replace(/#.*$/, '').trim();
|
|
130
|
+
if (!l || l.startsWith('-')) continue;
|
|
131
|
+
const m = PY_NAME.exec(l);
|
|
132
|
+
if (m) dec('pypi', pyName(m[1] as string));
|
|
133
|
+
}
|
|
134
|
+
} else if (name === 'go.mod') {
|
|
135
|
+
let block = false;
|
|
136
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
137
|
+
const l = raw.replace(/\/\/.*$/, '').trim();
|
|
138
|
+
const mod = /^module\s+(\S+)/.exec(l);
|
|
139
|
+
if (mod) pub('go', mod[1]);
|
|
140
|
+
if (/^require\s*\($/.test(l)) {
|
|
141
|
+
block = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (block && l === ')') {
|
|
145
|
+
block = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const req = block ? /^(\S+)\s+\S+/.exec(l) : /^require\s+(\S+)\s+\S+/.exec(l);
|
|
149
|
+
if (req) dec('go', req[1] as string);
|
|
150
|
+
}
|
|
151
|
+
} else if (name === 'Cargo.toml') {
|
|
152
|
+
const s = tomlSections(text);
|
|
153
|
+
pub('cargo', tomlString(s.get('package'), 'name'));
|
|
154
|
+
for (const sec of ['dependencies', 'dev-dependencies', 'build-dependencies']) for (const k of tomlKeys(s.get(sec))) dec('cargo', k);
|
|
155
|
+
} else if (name === 'Gemfile') {
|
|
156
|
+
for (const m of text.matchAll(/^\s*gem\s+["']([^"']+)["']/gm)) dec('rubygems', m[1] as string);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function dedupe<T>(rows: T[]): T[] {
|
|
161
|
+
const seen = new Set<string>();
|
|
162
|
+
return rows.filter((r) => {
|
|
163
|
+
const k = JSON.stringify(r);
|
|
164
|
+
if (seen.has(k)) return false;
|
|
165
|
+
seen.add(k);
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isManifest = (name: string): boolean => name === 'Cargo.toml' || MANIFEST_NAME.test(name);
|
|
171
|
+
const ROOT_MANIFESTS = ['package.json', 'composer.json', 'pyproject.toml', 'requirements.txt', 'go.mod', 'Cargo.toml', 'Gemfile'];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Manifest paths from git's index, plus an untracked one at the root, then the
|
|
175
|
+
* discovery walk's own directory filter. A refresh already walked the tree
|
|
176
|
+
* once; this keeps the index from walking it a second time (6.4 measured the
|
|
177
|
+
* walk, and then git's untracked scan, as most of the index's cost). `null`
|
|
178
|
+
* outside a repository.
|
|
179
|
+
*/
|
|
180
|
+
function manifestPathsFromGit(root: string): string[] | null {
|
|
181
|
+
let out: string;
|
|
182
|
+
try {
|
|
183
|
+
out = execFileSync('git', ['-C', root, 'ls-files', '-z', '--cached'], {
|
|
184
|
+
env: hardenedSpawnEnv(),
|
|
185
|
+
encoding: 'utf-8',
|
|
186
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
187
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
188
|
+
});
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const rels = new Set(out.split('\0').filter((rel) => rel && isManifest(path.posix.basename(rel)) && walkedPath(rel)));
|
|
193
|
+
for (const name of ROOT_MANIFESTS) if (fs.existsSync(path.join(root, name))) rels.add(name);
|
|
194
|
+
return [...rels];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function manifestFacts(root: string): ManifestFacts {
|
|
198
|
+
const facts: ManifestFacts = { declares: [], publishes: [] };
|
|
199
|
+
const texts = new Map<string, string>();
|
|
200
|
+
const fromGit = manifestPathsFromGit(root);
|
|
201
|
+
if (fromGit !== null) {
|
|
202
|
+
for (const rel of fromGit) {
|
|
203
|
+
try {
|
|
204
|
+
texts.set(rel, fs.readFileSync(path.join(root, rel), 'utf-8'));
|
|
205
|
+
} catch {
|
|
206
|
+
/* deleted in the working tree: no rows */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
const probe = readProjectProbe(root);
|
|
211
|
+
for (const [rel, text] of probe.manifests) texts.set(rel, text);
|
|
212
|
+
for (const rel of probe.paths) {
|
|
213
|
+
if (path.posix.basename(rel) !== 'Cargo.toml') continue;
|
|
214
|
+
try {
|
|
215
|
+
texts.set(rel, fs.readFileSync(path.join(root, rel), 'utf-8'));
|
|
216
|
+
} catch {
|
|
217
|
+
/* unreadable: no rows */
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const [rel, text] of [...texts.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) parse(rel, text, facts);
|
|
222
|
+
return { declares: dedupe(facts.declares), publishes: dedupe(facts.publishes) };
|
|
223
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-graph map --estate` — one offline page of the registered projects and
|
|
3
|
+
* the edges the estate index derived between them (ADR-299, step 5.4).
|
|
4
|
+
*
|
|
5
|
+
* Projects are nodes; a `package` edge (a declared dependency on a package
|
|
6
|
+
* another project publishes) is drawn solid, the two guesses (`unique-name`,
|
|
7
|
+
* `mention`) dashed. The page names client projects, so it is written under
|
|
8
|
+
* the user-global home and refused anywhere inside a git work tree.
|
|
9
|
+
*
|
|
10
|
+
* The page exposes `window.__mapLabelBoxes` and a `#panel .plist` list in the
|
|
11
|
+
* shape `map_verify` reads, so the same three-width render check applies.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { enclosingWorkTree } from '../_lib/git_common_dir.js';
|
|
17
|
+
import { write_atomic } from '../_lib/fs_atomic.js';
|
|
18
|
+
import { event4u_root } from '../_lib/user_global_paths.js';
|
|
19
|
+
import { openEstate, type XEdge } from './estate_index.js';
|
|
20
|
+
import { verifyMap } from './map_verify.js';
|
|
21
|
+
|
|
22
|
+
export interface EstateMapData {
|
|
23
|
+
projects: Array<{ id: string; name: string; indexed_at: string }>;
|
|
24
|
+
edges: XEdge[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function estateMapDir(): string {
|
|
28
|
+
return path.join(event4u_root(), 'runtime', 'estate', 'map');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** JSON safe inside a `<script>` element: no `</script>`, no `<!--`. */
|
|
32
|
+
function scriptJson(v: unknown): string {
|
|
33
|
+
return JSON.stringify(v).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CSS = `
|
|
37
|
+
:root{--bg:#fbfbf9;--fg:#1d1d1b;--muted:#6b6b66;--line:#3c6e8f;--guess:#b0703a;--node:#ffffff;--ring:#3c6e8f;--hl:#d9e8f2}
|
|
38
|
+
@media (prefers-color-scheme:dark){:root{--bg:#161615;--fg:#ecece8;--muted:#a3a39c;--line:#7fb2d4;--guess:#e0a46b;--node:#22221f;--ring:#7fb2d4;--hl:#25343e}}
|
|
39
|
+
*{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
40
|
+
header{padding:16px 16px 4px}h1{font-size:18px;margin:0 0 4px}p{margin:0;color:var(--muted);max-width:60ch}
|
|
41
|
+
main{display:flex;flex-wrap:wrap;gap:16px;padding:8px 16px 16px}.stage{flex:1 1 320px;min-width:0}svg{display:block;width:100%;height:auto}
|
|
42
|
+
#panel{flex:0 1 260px;min-width:0}.plist{list-style:none;margin:0;padding:0}.plist button{display:flex;align-items:center;gap:8px;width:100%;
|
|
43
|
+
border:0;background:none;color:inherit;font:inherit;text-align:left;padding:6px 4px;cursor:pointer;border-radius:6px}
|
|
44
|
+
.plist button[aria-pressed=true]{background:var(--hl)}.swatch{flex:0 0 10px;height:10px;border-radius:50%;background:var(--ring)}
|
|
45
|
+
.fname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;color:var(--muted);font-size:12px}
|
|
46
|
+
.k{display:inline-block;width:24px;border-top:2px solid var(--line);vertical-align:middle;margin-right:4px}.k.g{border-top:2px dashed var(--guess)}
|
|
47
|
+
line.package{stroke:var(--line);stroke-width:2}line.guess{stroke:var(--guess);stroke-width:1.5;stroke-dasharray:5 4}
|
|
48
|
+
line.dim{opacity:.15}circle{fill:var(--node);stroke:var(--ring);stroke-width:2}text{fill:var(--fg);font-size:12px}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const JS = `
|
|
52
|
+
(function(){
|
|
53
|
+
var data=JSON.parse(document.getElementById('estate-data').textContent);
|
|
54
|
+
var svg=document.getElementById('stage');var NS='http://www.w3.org/2000/svg';
|
|
55
|
+
var list=document.getElementById('plist');var selected=null;
|
|
56
|
+
function short(s){return s.length>18?s.slice(0,17)+'\\u2026':s}
|
|
57
|
+
function draw(){
|
|
58
|
+
while(svg.firstChild)svg.removeChild(svg.firstChild);
|
|
59
|
+
var w=Math.max(240,svg.parentNode.clientWidth),h=Math.min(w,520);
|
|
60
|
+
svg.setAttribute('viewBox','0 0 '+w+' '+h);svg.setAttribute('width',w);svg.setAttribute('height',h);
|
|
61
|
+
var n=data.projects.length,texts=[],maxw=0;
|
|
62
|
+
var gl=document.createElementNS(NS,'g'),gn=document.createElementNS(NS,'g');svg.appendChild(gl);svg.appendChild(gn);
|
|
63
|
+
data.projects.forEach(function(p){var t=document.createElementNS(NS,'text');t.textContent=short(p.name);t.setAttribute('text-anchor','middle');gn.appendChild(t);texts.push(t);maxw=Math.max(maxw,t.getComputedTextLength())});
|
|
64
|
+
var r=n<2?0:Math.max(24,Math.min(w/2-maxw/2-12,h/2-40));var cx=w/2,cy=h/2-8,pos={};
|
|
65
|
+
data.projects.forEach(function(p,i){var a=-Math.PI/2+2*Math.PI*i/Math.max(1,n);pos[p.id]={x:cx+r*Math.cos(a),y:cy+r*Math.sin(a)}});
|
|
66
|
+
data.edges.forEach(function(e){var s=pos[e.source],t=pos[e.target];if(!s||!t)return;var l=document.createElementNS(NS,'line');
|
|
67
|
+
l.setAttribute('x1',s.x);l.setAttribute('y1',s.y);l.setAttribute('x2',t.x);l.setAttribute('y2',t.y);
|
|
68
|
+
var cls=e.via==='package'?'package':'guess';if(selected&&e.source!==selected&&e.target!==selected)cls+=' dim';l.setAttribute('class',cls);
|
|
69
|
+
var tt=document.createElementNS(NS,'title');tt.textContent=e.via+': '+e.detail;l.appendChild(tt);gl.appendChild(l)});
|
|
70
|
+
var boxes=[];
|
|
71
|
+
data.projects.forEach(function(p,i){var c=document.createElementNS(NS,'circle');var q=pos[p.id];c.setAttribute('cx',q.x);c.setAttribute('cy',q.y);c.setAttribute('r',p.id===selected?9:7);
|
|
72
|
+
gn.insertBefore(c,texts[0]);var t=texts[i];t.setAttribute('x',q.x);t.setAttribute('y',q.y+22);var b=t.getBBox();boxes.push({x:b.x,y:b.y,w:b.width,h:b.height})});
|
|
73
|
+
window.__mapLabelBoxes={boxes:boxes,width:w,height:h};
|
|
74
|
+
}
|
|
75
|
+
data.projects.forEach(function(p){var li=document.createElement('li');var b=document.createElement('button');b.type='button';b.setAttribute('aria-pressed','false');
|
|
76
|
+
var s=document.createElement('span');s.className='swatch';var f=document.createElement('span');f.className='fname';f.textContent=p.name;f.title=p.id;
|
|
77
|
+
b.appendChild(s);b.appendChild(f);b.addEventListener('click',function(){selected=selected===p.id?null:p.id;
|
|
78
|
+
Array.prototype.forEach.call(list.querySelectorAll('button'),function(x){x.setAttribute('aria-pressed','false')});if(selected)b.setAttribute('aria-pressed','true');draw()});
|
|
79
|
+
li.appendChild(b);list.appendChild(li)});
|
|
80
|
+
draw();window.addEventListener('resize',draw);
|
|
81
|
+
})();
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export function renderEstateHtml(data: EstateMapData): string {
|
|
85
|
+
const packages = data.edges.filter((e) => e.via === 'package').length;
|
|
86
|
+
const guesses = data.edges.length - packages;
|
|
87
|
+
return [
|
|
88
|
+
'<!doctype html>',
|
|
89
|
+
'<html lang="en">',
|
|
90
|
+
'<head>',
|
|
91
|
+
'<meta charset="utf-8">',
|
|
92
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
93
|
+
'<title>Project estate</title>',
|
|
94
|
+
`<style>${CSS}</style>`,
|
|
95
|
+
'</head>',
|
|
96
|
+
'<body>',
|
|
97
|
+
'<header>',
|
|
98
|
+
'<h1>Project estate</h1>',
|
|
99
|
+
`<p>${data.projects.length} registered projects · ${packages} package links · ${guesses} guessed links. Local to this machine; never committed.</p>`,
|
|
100
|
+
'<div class="legend"><span><span class="k"></span>declared dependency</span><span><span class="k g"></span>guess (unique name or mention)</span></div>',
|
|
101
|
+
'</header>',
|
|
102
|
+
'<main>',
|
|
103
|
+
'<div class="stage"><svg id="stage" role="img" aria-label="Projects and the links between them"></svg></div>',
|
|
104
|
+
'<aside id="panel"><ul class="plist" id="plist"></ul></aside>',
|
|
105
|
+
'</main>',
|
|
106
|
+
`<script type="application/json" id="estate-data">${scriptJson(data)}</script>`,
|
|
107
|
+
`<script>${JS}</script>`,
|
|
108
|
+
'</body>',
|
|
109
|
+
'</html>',
|
|
110
|
+
'',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function estateMapMain(argv: string[]): Promise<number> {
|
|
115
|
+
const i = argv.indexOf('--out');
|
|
116
|
+
const outDir = i >= 0 && i + 1 < argv.length ? path.resolve(argv[i + 1] as string) : estateMapDir();
|
|
117
|
+
const tree = enclosingWorkTree(outDir);
|
|
118
|
+
if (tree !== null) {
|
|
119
|
+
process.stderr.write(`code-graph: refusing to write the estate map inside a git work tree (${tree}): it names client projects\n`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
const db = openEstate();
|
|
123
|
+
if (!db) {
|
|
124
|
+
process.stderr.write('code-graph: no estate index — `code-graph projects index on`, then build or refresh each project\n');
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
let data: EstateMapData;
|
|
128
|
+
try {
|
|
129
|
+
data = { projects: db.projects().map(({ id, name, indexed_at }) => ({ id, name, indexed_at })), edges: db.xedges() };
|
|
130
|
+
} finally {
|
|
131
|
+
db.close();
|
|
132
|
+
}
|
|
133
|
+
const page = path.join(outDir, 'index.html');
|
|
134
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
135
|
+
write_atomic(page, renderEstateHtml(data));
|
|
136
|
+
process.stdout.write(`✅ estate map — ${data.projects.length} projects · ${data.edges.length} links\n wrote ${page}\n`);
|
|
137
|
+
if (argv.includes('--no-verify')) return 0;
|
|
138
|
+
const check = await verifyMap(page);
|
|
139
|
+
process.stdout.write(` render check: ${check.verification}${check.reason ? ` — ${check.reason}` : ''}\n`);
|
|
140
|
+
return check.verification === 'failed' ? 1 : 0;
|
|
141
|
+
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
MAX_FILE_BYTES,
|
|
21
21
|
type Relation,
|
|
22
22
|
} from './types.js';
|
|
23
|
-
import { extractCSharp, extractGo, extractJava, extractPython } from './extract_more.js';
|
|
23
|
+
import { extractCSharp, extractGo, extractJava, extractPython, extractRuby } from './extract_more.js';
|
|
24
24
|
import { getParser, nodeHasError, type TsNode } from './loader.js';
|
|
25
25
|
|
|
26
26
|
export type CallStyle = 'new' | 'free' | 'this' | 'self' | 'static' | 'parent' | 'scoped' | 'dynamic';
|
|
@@ -90,6 +90,8 @@ export interface FileExtract {
|
|
|
90
90
|
parseError: boolean;
|
|
91
91
|
/** C#: the namespaces the file declares, for `using` resolution in the build pass. */
|
|
92
92
|
namespaces?: string[];
|
|
93
|
+
/** C#: the namespace each declared type sits in, by node id, for namespace-scope resolution. */
|
|
94
|
+
typeNamespaces?: Record<string, string>;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export function loc(n: TsNode): number[] {
|
|
@@ -703,6 +705,7 @@ export async function extractFile(relPath: string, source: string, lang: Lang):
|
|
|
703
705
|
else if (lang === 'java') extractJava(tree.rootNode, relPath, out);
|
|
704
706
|
else if (lang === 'go') extractGo(tree.rootNode, relPath, out);
|
|
705
707
|
else if (lang === 'csharp') extractCSharp(tree.rootNode, relPath, out);
|
|
708
|
+
else if (lang === 'ruby') extractRuby(tree.rootNode, relPath, out);
|
|
706
709
|
else extractTsJs(tree.rootNode, relPath, out);
|
|
707
710
|
} finally {
|
|
708
711
|
tree.delete(); // delete the tree, never the cached parser (loader.ts)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Python, Go, Java
|
|
2
|
+
* Python, Go, Java, C# and Ruby extraction — the same shape `extract.ts` emits for
|
|
3
3
|
* PHP / TS / JS, so the build pass resolves them with no language branch
|
|
4
4
|
* beyond module lookup.
|
|
5
5
|
*
|
|
@@ -113,11 +113,18 @@ export function extractPython(root: TsNode, file: string, out: FileExtract): voi
|
|
|
113
113
|
case 'call': {
|
|
114
114
|
const fn = n.childForFieldName('function');
|
|
115
115
|
if (fn?.type === 'identifier') {
|
|
116
|
-
|
|
116
|
+
// `Order(...)` instantiates: a CapWords name is a class by Python convention.
|
|
117
|
+
const isNew = /^[A-Z]/.test(fn.text);
|
|
118
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: fn.text, confidenceHint: 'EXTRACTED', callStyle: isNew ? 'new' : 'free' });
|
|
117
119
|
} else if (fn?.type === 'attribute') {
|
|
118
120
|
const obj = fn.childForFieldName('object');
|
|
119
121
|
const attr = text(fn.childForFieldName('attribute'));
|
|
120
122
|
const isSelf = obj?.type === 'identifier' && (obj.text === 'self' || obj.text === 'cls');
|
|
123
|
+
let base = obj;
|
|
124
|
+
while (base?.type === 'attribute') base = base.childForFieldName('object');
|
|
125
|
+
// `Post.objects.count()` / `Service.run()` name a class at the root of the chain.
|
|
126
|
+
const classRoot = base?.type === 'identifier' && /^[A-Z]/.test(base.text) ? base.text : null;
|
|
127
|
+
if (classRoot) out.rawEdges.push({ sourceId: scopeId, relation: 'uses', targetName: classRoot, confidenceHint: 'EXTRACTED' });
|
|
121
128
|
out.rawEdges.push({
|
|
122
129
|
sourceId: scopeId,
|
|
123
130
|
relation: 'calls',
|
|
@@ -125,6 +132,7 @@ export function extractPython(root: TsNode, file: string, out: FileExtract): voi
|
|
|
125
132
|
confidenceHint: isSelf && classId ? 'INFERRED' : 'AMBIGUOUS',
|
|
126
133
|
callStyle: isSelf ? 'this' : 'dynamic',
|
|
127
134
|
...(classId ? { enclosingClassId: classId } : {}),
|
|
135
|
+
...(classRoot && obj === base ? { scopeName: classRoot } : {}),
|
|
128
136
|
});
|
|
129
137
|
}
|
|
130
138
|
break;
|
|
@@ -330,12 +338,25 @@ export function extractCSharp(root: TsNode, file: string, out: FileExtract): voi
|
|
|
330
338
|
enum_declaration: 'enum',
|
|
331
339
|
};
|
|
332
340
|
const namespaces: string[] = [];
|
|
341
|
+
const typeNamespaces: Record<string, string> = {};
|
|
342
|
+
/** Every project-type name a type expression mentions: `List<Order>` names `List` and `Order`; `int` names none. */
|
|
343
|
+
const typeNames = (t: TsNode | null, acc: string[] = []): string[] => {
|
|
344
|
+
if (!t || t.type === 'predefined_type') return acc;
|
|
345
|
+
if (t.type === 'identifier' || t.type === 'qualified_name') return [...acc, csType(t)];
|
|
346
|
+
if (t.type === 'generic_name') acc.push(csType(t));
|
|
347
|
+
for (const c of namedChildren(t)) if (t.type !== 'generic_name' || c.type === 'type_argument_list') acc = typeNames(c, acc);
|
|
348
|
+
return acc;
|
|
349
|
+
};
|
|
350
|
+
const usesTypes = (sourceId: string, t: TsNode | null): void => {
|
|
351
|
+
for (const nm of new Set(typeNames(t))) out.rawEdges.push({ sourceId, relation: 'uses', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
352
|
+
};
|
|
333
353
|
const walk = (n: TsNode, classId: string | null, scopeId: string, ns = ''): void => {
|
|
334
354
|
const kind = TYPES[n.type];
|
|
335
355
|
if (kind) {
|
|
336
356
|
const nm = nameField(n) ?? '<anon>';
|
|
337
357
|
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
338
358
|
out.nodes.push({ id, label: nm, kind, source_file: file, source_location: loc(n) });
|
|
359
|
+
if (!classId && ns) typeNamespaces[id] = ns;
|
|
339
360
|
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
340
361
|
for (const c of namedChildren(n)) {
|
|
341
362
|
if (c.type !== 'base_list') continue;
|
|
@@ -376,12 +397,21 @@ export function extractCSharp(root: TsNode, file: string, out: FileExtract): voi
|
|
|
376
397
|
}
|
|
377
398
|
return;
|
|
378
399
|
}
|
|
400
|
+
case 'field_declaration':
|
|
401
|
+
case 'property_declaration': {
|
|
402
|
+
const decl = n.type === 'field_declaration' ? namedChildren(n).find((c) => c.type === 'variable_declaration') : n;
|
|
403
|
+
if (classId) usesTypes(classId, decl?.childForFieldName('type') ?? null);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
379
406
|
case 'method_declaration':
|
|
380
407
|
case 'constructor_declaration': {
|
|
381
408
|
const nm = nameField(n) ?? '<anon>';
|
|
382
409
|
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
383
410
|
out.nodes.push({ id, label: nm, kind: 'method', source_file: file, source_location: loc(n) });
|
|
384
411
|
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
412
|
+
usesTypes(id, n.childForFieldName('returns') ?? n.childForFieldName('type'));
|
|
413
|
+
for (const c of namedChildren(n))
|
|
414
|
+
if (c.type === 'parameter_list') for (const p of namedChildren(c)) if (p.type === 'parameter') usesTypes(id, p.childForFieldName('type'));
|
|
385
415
|
for (const c of namedChildren(n)) if (c.type === 'block' || c.type === 'arrow_expression_clause') walk(c, classId, id, ns);
|
|
386
416
|
return;
|
|
387
417
|
}
|
|
@@ -415,4 +445,109 @@ export function extractCSharp(root: TsNode, file: string, out: FileExtract): voi
|
|
|
415
445
|
};
|
|
416
446
|
walk(root, null, file);
|
|
417
447
|
out.namespaces = namespaces;
|
|
448
|
+
out.typeNamespaces = typeNamespaces;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** The last segment of a Ruby constant path: `Admin::User` → `User`. */
|
|
452
|
+
function rbConst(n: TsNode | null): string {
|
|
453
|
+
return n ? (n.text.split('::').pop() ?? n.text).trim() : '';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Ruby: classes, modules (as `trait` — a mixin is what PHP calls a trait),
|
|
458
|
+
* instance and singleton methods, `require` / `require_relative`, the
|
|
459
|
+
* superclass and every `include` / `extend` / `prepend` as `inherits`, and
|
|
460
|
+
* calls: implicit-self and `self.` calls on the enclosing class, `Foo.new`,
|
|
461
|
+
* `Foo.bar` scoped calls (plus a `uses` of `Foo`), and dynamic receivers.
|
|
462
|
+
*/
|
|
463
|
+
export function extractRuby(root: TsNode, file: string, out: FileExtract): void {
|
|
464
|
+
out.nodes.push({ id: file, label: file, kind: 'file', source_file: file, source_location: [] });
|
|
465
|
+
const MIXINS = new Set(['include', 'extend', 'prepend']);
|
|
466
|
+
const walk = (n: TsNode, classId: string | null, scopeId: string): void => {
|
|
467
|
+
switch (n.type) {
|
|
468
|
+
case 'class':
|
|
469
|
+
case 'module': {
|
|
470
|
+
const nm = rbConst(n.childForFieldName('name')) || '<anon>';
|
|
471
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
472
|
+
out.nodes.push({ id, label: nm, kind: n.type === 'class' ? 'class' : 'trait', source_file: file, source_location: loc(n) });
|
|
473
|
+
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
474
|
+
const sup = n.childForFieldName('superclass');
|
|
475
|
+
const parent = sup ? namedChildren(sup)[0] : undefined;
|
|
476
|
+
if (parent && (parent.type === 'constant' || parent.type === 'scope_resolution')) out.inherits.push({ classId: id, parentName: rbConst(parent) });
|
|
477
|
+
const body = n.childForFieldName('body');
|
|
478
|
+
if (body) for (const c of namedChildren(body)) walk(c, id, id);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
case 'method':
|
|
482
|
+
case 'singleton_method': {
|
|
483
|
+
const nm = nameField(n) ?? '<anon>';
|
|
484
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
485
|
+
out.nodes.push({ id, label: nm, kind: classId ? 'method' : 'function', source_file: file, source_location: loc(n) });
|
|
486
|
+
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
487
|
+
const body = n.childForFieldName('body');
|
|
488
|
+
for (const c of body ? namedChildren(body) : []) {
|
|
489
|
+
// A bare identifier statement is an argument-less implicit-self
|
|
490
|
+
// call (`audit`); a local read looks the same and resolves to
|
|
491
|
+
// nothing, which is what an unresolved call records.
|
|
492
|
+
if (c.type === 'identifier' && classId)
|
|
493
|
+
out.rawEdges.push({ sourceId: id, relation: 'calls', targetName: c.text, confidenceHint: 'INFERRED', callStyle: 'this', enclosingClassId: classId });
|
|
494
|
+
else walk(c, classId, id);
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
case 'call': {
|
|
499
|
+
const recv = n.childForFieldName('receiver');
|
|
500
|
+
const nm = text(n.childForFieldName('method'));
|
|
501
|
+
const args = n.childForFieldName('arguments');
|
|
502
|
+
const first = args ? namedChildren(args)[0] : undefined;
|
|
503
|
+
if (!recv && (nm === 'require' || nm === 'require_relative') && first?.type === 'string') {
|
|
504
|
+
const raw = text(namedChildren(first).find((c) => c.type === 'string_content'));
|
|
505
|
+
if (raw) {
|
|
506
|
+
const spec = nm === 'require_relative' && !raw.startsWith('.') ? `./${raw}` : raw;
|
|
507
|
+
out.rawEdges.push({
|
|
508
|
+
sourceId: file,
|
|
509
|
+
relation: 'imports',
|
|
510
|
+
targetName: lastSegment(raw),
|
|
511
|
+
confidenceHint: 'EXTRACTED',
|
|
512
|
+
moduleSpecifier: spec,
|
|
513
|
+
importKind: 'namespace',
|
|
514
|
+
// A require binds no name; a local that cannot be written keeps it from shadowing one.
|
|
515
|
+
localName: `require:${spec}`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!recv && classId && MIXINS.has(nm) && args) {
|
|
521
|
+
for (const c of namedChildren(args))
|
|
522
|
+
if (c.type === 'constant' || c.type === 'scope_resolution') out.inherits.push({ classId, parentName: rbConst(c) });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const ctx = classId ? { enclosingClassId: classId } : {};
|
|
526
|
+
if (recv && (recv.type === 'constant' || recv.type === 'scope_resolution')) {
|
|
527
|
+
const cls = rbConst(recv);
|
|
528
|
+
if (nm === 'new') out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: cls, confidenceHint: 'EXTRACTED', callStyle: 'new' });
|
|
529
|
+
else {
|
|
530
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'uses', targetName: cls, confidenceHint: 'EXTRACTED' });
|
|
531
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'scoped', scopeName: cls, ...ctx });
|
|
532
|
+
}
|
|
533
|
+
} else if (!recv || recv.type === 'self') {
|
|
534
|
+
out.rawEdges.push({
|
|
535
|
+
sourceId: scopeId,
|
|
536
|
+
relation: 'calls',
|
|
537
|
+
targetName: nm,
|
|
538
|
+
confidenceHint: classId ? 'INFERRED' : 'AMBIGUOUS',
|
|
539
|
+
callStyle: classId ? 'this' : 'free',
|
|
540
|
+
...ctx,
|
|
541
|
+
});
|
|
542
|
+
} else if (nm) {
|
|
543
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'dynamic', ...ctx });
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
default:
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
for (const c of namedChildren(n)) walk(c, classId, scopeId);
|
|
551
|
+
};
|
|
552
|
+
walk(root, null, file);
|
|
418
553
|
}
|