@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.
Files changed (137) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +164 -342
  5. package/CREDITS.md +2 -2
  6. package/README.md +1 -1
  7. package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
  8. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  9. package/dist/cli/registry.js +2 -2
  10. package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
  11. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  12. package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
  13. package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
  14. package/dist/cli-delegate/cmd_conformance.js +175 -183
  15. package/dist/cli-delegate/cmd_doctor.js +5 -5
  16. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  17. package/dist/cli-delegate/cmd_explain.js +1 -1
  18. package/dist/cli-delegate/cmd_handoff.js +2 -2
  19. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  20. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  21. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  22. package/dist/cli-delegate/cmd_update.js +24 -13
  23. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  24. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  25. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  26. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  27. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +3 -3
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
  38. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
  39. package/dist/hooks/dispatch.js +982 -929
  40. package/dist/hooks/dispatch.sha256 +1 -1
  41. package/dist/hooks/egress_engine.chunk.cjs +56 -6
  42. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  43. package/dist/hooks/update_notice.chunk.cjs +914 -0
  44. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  45. package/dist/mcp/registry-manifest.json +1 -1
  46. package/dist/mcp/server.json +2 -2
  47. package/dist/mcp/server.mjs +71259 -66437
  48. package/dist/scripts/condense.js +9 -6
  49. package/package.json +4 -1
  50. package/src/config/evaluator-budgets.json +9 -8
  51. package/src/config/gate-violation-baselines.json +2 -2
  52. package/src/config/hook-bundle-budget.json +31 -6
  53. package/src/config/packed-binary-manifest.json +9 -0
  54. package/src/config/sigstore/npm_keys.json +50 -0
  55. package/src/config/sigstore/trust-material.json +9 -0
  56. package/src/config/sigstore/trusted_root.json +126 -0
  57. package/src/config/skill-body-budget.json +7 -1
  58. package/src/scripts/_cli/cmd_update.ts +48 -20
  59. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  60. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  61. package/src/scripts/_dispatch.bash +3 -2
  62. package/src/scripts/_lib/confusables.ts +0 -19
  63. package/src/scripts/_lib/egress_normalize.ts +53 -23
  64. package/src/scripts/_lib/egress_text.ts +5 -3
  65. package/src/scripts/_lib/egress_token.ts +7 -5
  66. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  67. package/src/scripts/_lib/git_common_dir.ts +10 -0
  68. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  69. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  70. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  71. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  72. package/src/scripts/_lib/lineage_map.ts +55 -0
  73. package/src/scripts/_lib/lineage_query.ts +140 -0
  74. package/src/scripts/_lib/lineage_seed.ts +116 -0
  75. package/src/scripts/_lib/lineage_watch.ts +249 -0
  76. package/src/scripts/_lib/link_crypto.ts +8 -4
  77. package/src/scripts/_lib/production_reach.ts +3 -3
  78. package/src/scripts/_lib/project_registry.ts +227 -0
  79. package/src/scripts/_lib/provenance_verify.ts +14 -242
  80. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  81. package/src/scripts/_lib/source_id.ts +48 -0
  82. package/src/scripts/_lib/source_shape.ts +4 -0
  83. package/src/scripts/_lib/unattended_gates.ts +6 -1
  84. package/src/scripts/advisories.ts +26 -10
  85. package/src/scripts/ai_council/council_egress.ts +5 -6
  86. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  87. package/src/scripts/check_dependency_floors.ts +7 -0
  88. package/src/scripts/check_egress_mutations.ts +1 -0
  89. package/src/scripts/code_graph/build.ts +67 -3
  90. package/src/scripts/code_graph/cli.ts +131 -28
  91. package/src/scripts/code_graph/conventions_default.ts +20 -1
  92. package/src/scripts/code_graph/detect.ts +5 -5
  93. package/src/scripts/code_graph/estate_cli.ts +135 -0
  94. package/src/scripts/code_graph/estate_index.ts +394 -0
  95. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  96. package/src/scripts/code_graph/estate_map.ts +141 -0
  97. package/src/scripts/code_graph/extract.ts +4 -1
  98. package/src/scripts/code_graph/extract_more.ts +137 -2
  99. package/src/scripts/code_graph/graph_budget.ts +158 -0
  100. package/src/scripts/code_graph/graph_home.ts +80 -0
  101. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  102. package/src/scripts/code_graph/map.ts +22 -13
  103. package/src/scripts/code_graph/map_model.ts +67 -12
  104. package/src/scripts/code_graph/map_verify.ts +68 -5
  105. package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
  106. package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
  107. package/src/scripts/code_graph/project_ref.ts +25 -0
  108. package/src/scripts/code_graph/projects_cli.ts +254 -0
  109. package/src/scripts/code_graph/providers/catalog.ts +14 -5
  110. package/src/scripts/code_graph/providers/django.ts +52 -9
  111. package/src/scripts/code_graph/providers/index.ts +2 -0
  112. package/src/scripts/code_graph/providers/rails.ts +204 -0
  113. package/src/scripts/code_graph/providers/spring.ts +38 -1
  114. package/src/scripts/code_graph/query.ts +9 -9
  115. package/src/scripts/code_graph/refresh_lock.ts +12 -8
  116. package/src/scripts/code_graph/repo_identity.ts +169 -0
  117. package/src/scripts/code_graph/sqlite_store.ts +22 -25
  118. package/src/scripts/code_graph/types.ts +6 -2
  119. package/src/scripts/condense.ts +6 -6
  120. package/src/scripts/hook_chunks.mjs +7 -0
  121. package/src/scripts/hook_manifest.json +1 -1
  122. package/src/scripts/hook_manifest.yaml +20 -17
  123. package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
  124. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  125. package/src/scripts/hooks/host_semantics.ts +72 -0
  126. package/src/scripts/hooks/lazy_concern.ts +4 -0
  127. package/src/scripts/hooks/user_notice.ts +105 -0
  128. package/src/scripts/lint_harvest_provenance.ts +137 -11
  129. package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
  130. package/src/scripts/mcp_server/graph_tools.ts +161 -41
  131. package/src/scripts/network_sources.ts +24 -6
  132. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  133. package/src/scripts/update_notice_hook.ts +75 -7
  134. package/src/vendor/grammars/README.md +7 -6
  135. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  136. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  137. /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 and C# extraction — the same shape `extract.ts` emits for
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
- out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: fn.text, confidenceHint: 'EXTRACTED', callStyle: 'free' });
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
  }