@event4u/agent-config 17.0.0 → 17.2.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 +157 -468
- package/README.md +4 -3
- package/dist/agent-src/commands/project/analyze.md +9 -6
- package/dist/agent-src/commands/project/map.md +58 -0
- package/dist/agent-src/commands/project.md +5 -3
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +5 -3
- package/dist/agent-src/skills/code-intelligence/SKILL.md +6 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +5 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/{chunk-IROAQ77A.js → chunk-3PK3VZHB.js} +107 -9
- package/dist/cli-delegate/{chunk-2SFOOFJJ.js → chunk-BZJ4Y3CI.js} +1 -1
- package/dist/cli-delegate/{chunk-CAL76ADI.js → chunk-CMZEKSNN.js} +52 -10
- package/dist/cli-delegate/{chunk-ESUNQ5XR.js → chunk-E3CFULNX.js} +22 -4
- package/dist/cli-delegate/{chunk-GOMILC3O.js → chunk-QBHEI5N4.js} +4 -3
- package/dist/cli-delegate/{chunk-SIOMDAKL.js → chunk-VYZGJLCA.js} +29 -39
- package/dist/cli-delegate/chunk-W5JXMF2T.js +77 -0
- package/dist/cli-delegate/{chunk-4SHNERBL.js → chunk-XIAPY44N.js} +1 -1
- package/dist/cli-delegate/{chunk-JHMMWNJ2.js → chunk-YHAPONWV.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +32 -47
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +3 -3
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +9 -13
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +41 -15
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +163 -148
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/instructions_observer.chunk.cjs +301 -0
- package/dist/hooks/instructions_observer.chunk.cjs.sha256 +1 -0
- package/dist/install/claudeRuleRewrite.js +10 -3
- package/dist/install/conflict.js +24 -2
- package/dist/install/conflictTracker.js +14 -3
- package/dist/install/install.mjs +222 -117
- package/dist/install/installThinLayer.js +1 -0
- package/dist/install/loggedWrites.js +142 -0
- package/dist/install/paths.js +12 -2
- package/dist/install/preserve.js +25 -4
- package/dist/install/txlog.js +10 -6
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +33595 -22916
- package/dist/scripts/_lib/egress_rules.js +4 -1
- package/dist/scripts/_lib/identifier_redaction.js +35 -2
- package/dist/scripts/ai_council/seat_policy.js +7 -1
- package/dist/server/schemas/settings.js +1 -1
- package/package.json +4 -1
- package/src/config/ci-local-parity.yml +1 -1
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +27 -7
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/gitignore-block.txt +6 -0
- package/src/config/hook-bundle-budget.json +19 -4
- package/src/config/hook-token-budget.json +1 -1
- package/src/config/packed-binary-manifest.json +27 -0
- package/src/config/production-entries.json +18 -0
- package/src/config/release-gate-locality.yml +0 -7
- package/src/config/skill-body-budget.json +7 -1
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/conflict.ts +24 -1
- package/src/install/conflictTracker.ts +18 -2
- package/src/install/installThinLayer.ts +6 -0
- package/src/install/loggedWrites.ts +148 -0
- package/src/install/paths.ts +11 -2
- package/src/install/preserve.ts +40 -4
- package/src/install/txlog.ts +10 -6
- package/src/scripts/_cli/cmd_conformance.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +15 -17
- package/src/scripts/_lib/bench_ab_complexity.ts +14 -2
- package/src/scripts/_lib/collector_record.ts +1 -0
- package/src/scripts/_lib/council_settings_block.ts +9 -2
- package/src/scripts/_lib/egress_rules.ts +4 -1
- package/src/scripts/_lib/host_capability.ts +10 -5
- package/src/scripts/_lib/identifier_redaction.ts +38 -2
- package/src/scripts/_lib/price_freshness.ts +112 -0
- package/src/scripts/_lib/production_reach.ts +194 -0
- package/src/scripts/_lib/quota_parked.ts +17 -28
- package/src/scripts/_lib/release_findings_ingest.ts +13 -335
- package/src/scripts/_lib/runtime_journal.ts +7 -7
- package/src/scripts/_lib/tier_downshift_verdict.ts +81 -35
- package/src/scripts/ai_council/chairman.ts +5 -2
- package/src/scripts/ai_council/clients.ts +4 -3
- package/src/scripts/ai_council/free_hand_shadow.ts +24 -12
- package/src/scripts/ai_council/free_route_catalog.ts +142 -5
- package/src/scripts/ai_council/free_route_client.ts +4 -0
- package/src/scripts/ai_council/hand_payload.ts +115 -7
- package/src/scripts/ai_council/orchestrator.ts +7 -10
- package/src/scripts/ai_council/route_chooser.ts +1 -1
- package/src/scripts/ai_council/route_member.ts +66 -4
- package/src/scripts/ai_council/seat_policy.ts +8 -1
- package/src/scripts/ai_council/self_hosted_probe.ts +327 -0
- package/src/scripts/ai_council/status_surface.ts +2 -0
- package/src/scripts/ask_transport.ts +137 -15
- package/src/scripts/check_finding_dispositions.ts +25 -71
- package/src/scripts/check_release_pr_shape.ts +3 -3
- package/src/scripts/check_step_production_reach.ts +257 -0
- package/src/scripts/code_graph/build.ts +210 -10
- package/src/scripts/code_graph/cli.ts +168 -46
- package/src/scripts/code_graph/detect.ts +5 -4
- package/src/scripts/code_graph/docs_layer.ts +57 -0
- package/src/scripts/code_graph/extract.ts +58 -13
- package/src/scripts/code_graph/extract_more.ts +314 -0
- package/src/scripts/code_graph/laravel_overlay.ts +454 -0
- package/src/scripts/code_graph/map.ts +846 -0
- package/src/scripts/code_graph/map_model.ts +977 -0
- package/src/scripts/code_graph/map_verify.ts +140 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +278 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +756 -0
- package/src/scripts/code_graph/scip_reader.ts +201 -0
- package/src/scripts/code_graph/sqlite_store.ts +8 -1
- package/src/scripts/code_graph/types.ts +81 -15
- package/src/scripts/code_graph/validate.ts +16 -26
- package/src/scripts/code_graph/verbs.ts +7 -0
- package/src/scripts/council_cli.ts +10 -8
- package/src/scripts/hook_chunks.mjs +7 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +23 -0
- package/src/scripts/hooks/dispatch_hook.ts +4 -4
- package/src/scripts/hooks/instructions_observer_hook.ts +25 -6
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/hooks/payload_stub.ts +27 -0
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +29 -70
- package/src/scripts/hooks/subagent_ledger_hook.ts +80 -0
- package/src/scripts/install-hooks.sh +5 -0
- package/src/scripts/install.ts +2 -2
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_decision_classes.ts +34 -0
- package/src/scripts/lint_hook_manifest.ts +25 -0
- package/src/scripts/lint_roadmap_blockers.ts +52 -3
- package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -6
- package/src/scripts/mcp_server/graph_tools.ts +73 -1
- package/src/scripts/mcp_server/tools.ts +19 -36
- package/src/scripts/release.ts +5 -9
- package/src/scripts/release_drill.ts +11 -165
- package/src/scripts/release_publication.ts +0 -9
- package/src/scripts/report_production_reach.ts +35 -0
- package/src/scripts/worker_reuse_shadow_report.ts +399 -1
- package/src/server/schemas/settings.ts +1 -1
- package/src/vendor/grammars/README.md +10 -5
- package/src/vendor/grammars/tree-sitter-go.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-java.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-python.wasm +0 -0
- package/dist/cli-delegate/chunk-A4QJ6ENT.js +0 -50
- /package/dist/cli-delegate/{chunk-E6XNBVE4.js → chunk-KR5ZIFPO.js} +0 -0
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
* 0 ok · 1 not-found / validation-failed / refresh-budget-exceeded ·
|
|
48
48
|
* 2 usage · 3 internal.
|
|
49
49
|
*
|
|
50
|
+
* map [--root P] [--out DIR] [--open] draw the project map into
|
|
51
|
+
* [--no-verify] [--diff <rev>] agents/map/ (ADR-297); renders it
|
|
52
|
+
* [--prs] (open PRs by area, via gh)
|
|
53
|
+
* at three widths when Playwright
|
|
54
|
+
* is present
|
|
55
|
+
*
|
|
50
56
|
* suggest-verb "<question>" (Phase 9 D3) print the verb
|
|
51
57
|
* (query/affected/path/explain)
|
|
52
58
|
* a regex table thinks best
|
|
@@ -62,10 +68,20 @@ import * as path from 'node:path';
|
|
|
62
68
|
import process from 'node:process';
|
|
63
69
|
import { fileURLToPath } from 'node:url';
|
|
64
70
|
|
|
71
|
+
import { resolve_project_root } from '../_lib/agent_settings.js';
|
|
65
72
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
66
73
|
import { buildFromRepo, isTestFile, sidecarPath } from './build.js';
|
|
67
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
computeVerdict,
|
|
76
|
+
detectSources,
|
|
77
|
+
type GraphState,
|
|
78
|
+
graphState,
|
|
79
|
+
NATIVE_CACHE_REL,
|
|
80
|
+
pickSource,
|
|
81
|
+
} from './detect.js';
|
|
68
82
|
import { suggestVerb } from './intent.js';
|
|
83
|
+
import { buildMap, recordOutcome } from './map.js';
|
|
84
|
+
import { verifyMap } from './map_verify.js';
|
|
69
85
|
import {
|
|
70
86
|
affected,
|
|
71
87
|
explain,
|
|
@@ -94,7 +110,16 @@ import {
|
|
|
94
110
|
|
|
95
111
|
const _HERE = fileURLToPath(import.meta.url);
|
|
96
112
|
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
|
|
97
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The cache belongs to the project it describes: `<root>/agents/runtime/state/`,
|
|
115
|
+
* the path MCP, the context hook and `detect` already read. Anchoring it to
|
|
116
|
+
* the package tree put a consumer's graph inside `node_modules`, where `npm ci`
|
|
117
|
+
* deleted it, two projects sharing an install overwrote each other, and MCP
|
|
118
|
+
* answered "no graph" right after the CLI reported a fresh one.
|
|
119
|
+
*/
|
|
120
|
+
export function cacheFor(root: string): string {
|
|
121
|
+
return path.join(root, NATIVE_CACHE_REL);
|
|
122
|
+
}
|
|
98
123
|
/** Local `tsx` binary — `refresh` spawns `build` as a child of THIS file
|
|
99
124
|
* through it, so a budget-exceeded kill only ever terminates the child. */
|
|
100
125
|
const TSX_BIN = path.join(REPO_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
|
|
@@ -106,47 +131,52 @@ function flag(argv: string[], name: string): string | null {
|
|
|
106
131
|
}
|
|
107
132
|
|
|
108
133
|
/**
|
|
109
|
-
* Resolve the repository to operate on
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* `AGENT_CONFIG_PROJECT_ROOT` (`:1655`) — then this engine read neither and fell
|
|
115
|
-
* back to its own module path. So `agent-config code-graph build --root <other
|
|
116
|
-
* repo>` silently indexed THIS package. The dispatcher made the decision and the
|
|
117
|
-
* engine substituted its own, which is why every consumer-facing invocation was
|
|
118
|
-
* wrong about which repository it had just read.
|
|
134
|
+
* Resolve the repository to operate on: `--root` > the shared project-root
|
|
135
|
+
* resolver (`AGENT_CONFIG_PROJECT_ROOT`, then the anchor walk from `cwd`, then
|
|
136
|
+
* `cwd`). The package tree is never a fallback — in a consumer it is
|
|
137
|
+
* `node_modules/@event4u/agent-config`, and indexing it answered every
|
|
138
|
+
* question about the wrong repository.
|
|
119
139
|
*
|
|
120
140
|
* `realpathSync` rather than `resolve` alone: a symlinked root must normalise to
|
|
121
|
-
* what will actually be walked
|
|
122
|
-
*
|
|
123
|
-
* fallback to the package tree is the defect this function exists to remove, and
|
|
124
|
-
* reintroducing it for a typo'd path would be the same bug wearing a smaller
|
|
125
|
-
* hat.
|
|
126
|
-
*
|
|
127
|
-
* Deliberately NOT touching `DEFAULT_CACHE`, which is anchored to `REPO_ROOT`
|
|
128
|
-
* for the same reason and is a separate decision: it is a WRITE path, with its
|
|
129
|
-
* own ownership, cleanup, concurrency and multi-repo namespacing questions.
|
|
130
|
-
* Council 2026-08-22, both seats, explicitly out of scope here.
|
|
141
|
+
* what will actually be walked. A root that does not exist is returned
|
|
142
|
+
* unresolved, so the caller's existence check prints the path it was given.
|
|
131
143
|
*/
|
|
132
144
|
function resolveRoot(argv: string[]): string {
|
|
133
145
|
const explicit = flag(argv, '--root');
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
let candidate: string;
|
|
147
|
+
if (explicit !== null) {
|
|
148
|
+
candidate = explicit;
|
|
149
|
+
} else {
|
|
150
|
+
try {
|
|
151
|
+
candidate = resolve_project_root(null)[0];
|
|
152
|
+
} catch {
|
|
153
|
+
candidate = process.env['AGENT_CONFIG_PROJECT_ROOT'] ?? process.cwd();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
137
156
|
try {
|
|
138
157
|
return fs.realpathSync(path.resolve(candidate));
|
|
139
158
|
} catch {
|
|
140
|
-
// Returned unresolved on purpose: every caller checks existence and
|
|
141
|
-
// prints the path it was given, which is more useful in the error than
|
|
142
|
-
// a normalised form of a path that does not exist.
|
|
143
159
|
return path.resolve(candidate);
|
|
144
160
|
}
|
|
145
161
|
}
|
|
146
162
|
|
|
163
|
+
/**
|
|
164
|
+
* A cache an older version wrote inside the package directory is named once
|
|
165
|
+
* per build, so a consumer can delete it. Never deleted here: the path is
|
|
166
|
+
* outside the project the user pointed the verb at.
|
|
167
|
+
*/
|
|
168
|
+
function legacyCacheNotice(root: string): string {
|
|
169
|
+
const legacy = cacheFor(REPO_ROOT);
|
|
170
|
+
// Only an installed package is a wrong home for a cache; a source checkout
|
|
171
|
+
// of this repository legitimately keeps its own.
|
|
172
|
+
if (!REPO_ROOT.split(path.sep).includes('node_modules')) return '';
|
|
173
|
+
if (path.resolve(REPO_ROOT) === path.resolve(root) || !fs.existsSync(legacy)) return '';
|
|
174
|
+
return ` note: a stale cache from an older version sits inside the package: ${legacy} — safe to delete\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
147
177
|
async function cmdBuild(argv: string[]): Promise<number> {
|
|
148
178
|
const root = resolveRoot(argv);
|
|
149
|
-
const out = argv.includes('--out') ? flag(argv, '--out') :
|
|
179
|
+
const out = argv.includes('--out') ? flag(argv, '--out') : cacheFor(root);
|
|
150
180
|
if (!fs.existsSync(root)) {
|
|
151
181
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
152
182
|
return 1;
|
|
@@ -160,7 +190,8 @@ async function cmdBuild(argv: string[]): Promise<number> {
|
|
|
160
190
|
` edges: EXTRACTED ${c.EXTRACTED} · INFERRED ${c.INFERRED} · AMBIGUOUS ${c.AMBIGUOUS}\n` +
|
|
161
191
|
` resolved_via: ${resolvedViaHistogram(graph)}\n` +
|
|
162
192
|
(update ? ` incremental: ${reExtracted} re-extracted · ${reused} reused\n` : '') +
|
|
163
|
-
(out ? ` cache: ${path.relative(
|
|
193
|
+
(out ? ` cache: ${path.relative(root, out)}\n` : '') +
|
|
194
|
+
legacyCacheNotice(root),
|
|
164
195
|
);
|
|
165
196
|
return 0;
|
|
166
197
|
}
|
|
@@ -186,7 +217,8 @@ function resolvedViaHistogram(graph: CodeGraph): string {
|
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
function cmdValidate(argv: string[]): number {
|
|
189
|
-
const
|
|
220
|
+
const root = resolveRoot(argv);
|
|
221
|
+
const p = flag(argv, '--graph') ?? cacheFor(root);
|
|
190
222
|
if (!fs.existsSync(p)) {
|
|
191
223
|
process.stderr.write(`code-graph: graph not found: ${p}\n`);
|
|
192
224
|
return 1;
|
|
@@ -215,12 +247,12 @@ function cmdValidate(argv: string[]): number {
|
|
|
215
247
|
// content-addressed cache key, so agreement here means the twin was emitted
|
|
216
248
|
// from THIS graph and not merely from a graph.
|
|
217
249
|
const twinState = twinChecksumState(p, (parsed as CodeGraph).source_checksum);
|
|
218
|
-
const rel = path.relative(
|
|
250
|
+
const rel = path.relative(root, p);
|
|
219
251
|
if (twinState.kind === 'mismatch') {
|
|
220
252
|
process.stderr.write(
|
|
221
253
|
`❌ SQLite twin disagrees with the canonical JSON: twin ${twinState.twin} ≠ json ${twinState.json}\n` +
|
|
222
254
|
' The twin is derived and disposable — delete it and re-run `code-graph build`.\n' +
|
|
223
|
-
` ${path.relative(
|
|
255
|
+
` ${path.relative(root, sqliteTwinPath(p))}\n`,
|
|
224
256
|
);
|
|
225
257
|
return 1;
|
|
226
258
|
}
|
|
@@ -257,13 +289,13 @@ function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err:
|
|
|
257
289
|
const root = resolveRoot(argv);
|
|
258
290
|
if (explicit) {
|
|
259
291
|
if (!fs.existsSync(explicit)) return { err: `graph not found: ${explicit}` };
|
|
260
|
-
return { g: loadGraph(explicit, path.relative(
|
|
292
|
+
return { g: loadGraph(explicit, path.relative(root, explicit)), note: '' };
|
|
261
293
|
}
|
|
262
|
-
const picked = pickSource(detectSources(root,
|
|
294
|
+
const picked = pickSource(detectSources(root, cacheFor(root)));
|
|
263
295
|
if (!picked) return { err: 'no code-graph source found — run `code_graph build` or ship a graph.json' };
|
|
264
|
-
if (picked.kind === 'scip') return { err: `${picked.note ?? 'SCIP detected'} —
|
|
296
|
+
if (picked.kind === 'scip') return { err: `${picked.note ?? 'SCIP detected'} — run \`code-graph build\` to merge it into the native graph` };
|
|
265
297
|
const staleNote = picked.stale ? ` ⚠ index is ${picked.commits_behind ?? 'N'} commit(s) behind — rebuild before trusting` : '';
|
|
266
|
-
return { g: loadGraph(picked.path, `${picked.kind}:${path.relative(
|
|
298
|
+
return { g: loadGraph(picked.path, `${picked.kind}:${path.relative(root, picked.path)}`), note: staleNote };
|
|
267
299
|
}
|
|
268
300
|
|
|
269
301
|
function render(r: QueryResult, staleNote: string): number {
|
|
@@ -376,8 +408,8 @@ function isFlagValue(argv: string[], token: string): boolean {
|
|
|
376
408
|
*
|
|
377
409
|
* `answeredBy` is `resolveGraph`'s own picked path when it has one. Without it
|
|
378
410
|
* the two disagreed on the case `resolveRoot`'s docstring exists for: with
|
|
379
|
-
* `--root <other repo>` the verb
|
|
380
|
-
* package tree
|
|
411
|
+
* `--root <other repo>` the verb answered from a cache anchored to the
|
|
412
|
+
* package tree while `graphState` looked under the OTHER root, so the printed
|
|
381
413
|
* staleness described a cache the answer never came from. An independent review
|
|
382
414
|
* found the `--graph` half fixed and this half open.
|
|
383
415
|
*/
|
|
@@ -579,17 +611,17 @@ function cmdDetect(argv: string[]): number {
|
|
|
579
611
|
if (flag(argv, '--format') === 'json') {
|
|
580
612
|
// Machine-readable three-state verdict (Phase 2). Existing human-text
|
|
581
613
|
// output below is unchanged for existing consumers.
|
|
582
|
-
process.stdout.write(`${JSON.stringify(computeVerdict(root,
|
|
614
|
+
process.stdout.write(`${JSON.stringify(computeVerdict(root, cacheFor(root)))}\n`);
|
|
583
615
|
return 0;
|
|
584
616
|
}
|
|
585
|
-
const verdicts = detectSources(root,
|
|
617
|
+
const verdicts = detectSources(root, cacheFor(root));
|
|
586
618
|
if (!verdicts.length) {
|
|
587
619
|
process.stdout.write('no code-graph source detected\n');
|
|
588
620
|
return 1;
|
|
589
621
|
}
|
|
590
622
|
for (const v of verdicts) {
|
|
591
623
|
const fresh = v.stale === undefined ? '' : v.stale ? ` · STALE${v.commits_behind !== undefined ? ` (${v.commits_behind} behind)` : ''}` : ' · fresh';
|
|
592
|
-
process.stdout.write(`${v.kind.padEnd(9)} ${path.relative(
|
|
624
|
+
process.stdout.write(`${v.kind.padEnd(9)} ${path.relative(root, v.path)}${fresh}${v.note ? ` · ${v.note}` : ''}\n`);
|
|
593
625
|
}
|
|
594
626
|
const picked = pickSource(verdicts);
|
|
595
627
|
if (picked) process.stdout.write(`→ query would use: ${picked.kind}\n`);
|
|
@@ -644,7 +676,13 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
644
676
|
}
|
|
645
677
|
}
|
|
646
678
|
const args = ['build', '--root', root, '--out', tmpOut, ...(update ? ['--update'] : [])];
|
|
647
|
-
|
|
679
|
+
// A hoisted consumer install has no `tsx` under the package's own
|
|
680
|
+
// `node_modules`; re-entering through this process's own loader flags works
|
|
681
|
+
// wherever the parent itself started.
|
|
682
|
+
const [cmd, cmdArgs] = fs.existsSync(TSX_BIN)
|
|
683
|
+
? [TSX_BIN, [_HERE, ...args]]
|
|
684
|
+
: [process.execPath, [...process.execArgv, _HERE, ...args]];
|
|
685
|
+
const res = spawnSync(cmd, cmdArgs, {
|
|
648
686
|
env: hardenedSpawnEnv(),
|
|
649
687
|
stdio: 'ignore',
|
|
650
688
|
timeout: Math.round(budgetSeconds * 1000),
|
|
@@ -670,13 +708,13 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
670
708
|
fs.renameSync(tmpOut, out); // same-dir → atomic; real cache only ever changes here
|
|
671
709
|
const tmpSidecar = sidecarPath(tmpOut);
|
|
672
710
|
if (fs.existsSync(tmpSidecar)) fs.renameSync(tmpSidecar, sidecarPath(out));
|
|
673
|
-
process.stdout.write(`✅ code-graph refreshed — cache: ${path.relative(
|
|
711
|
+
process.stdout.write(`✅ code-graph refreshed — cache: ${path.relative(root, out)}\n`);
|
|
674
712
|
return 0;
|
|
675
713
|
}
|
|
676
714
|
|
|
677
715
|
function cmdRefresh(argv: string[]): number {
|
|
678
716
|
const root = resolveRoot(argv);
|
|
679
|
-
const out = flag(argv, '--out') ??
|
|
717
|
+
const out = flag(argv, '--out') ?? cacheFor(root);
|
|
680
718
|
const budgetSeconds = budgetSecondsOf(argv);
|
|
681
719
|
if (!fs.existsSync(root)) {
|
|
682
720
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
@@ -690,6 +728,88 @@ function cmdRefresh(argv: string[]): number {
|
|
|
690
728
|
return runBudgetedBuild(root, out, v.verdict === 'STALE', budgetSeconds);
|
|
691
729
|
}
|
|
692
730
|
|
|
731
|
+
/**
|
|
732
|
+
* `map` — draw the project map into `agents/map/` (ADR-297): `index.html`,
|
|
733
|
+
* `MAP.md`, one page per area and the `map.json` they are rendered from.
|
|
734
|
+
* Builds or updates the graph first when it is absent or behind.
|
|
735
|
+
*/
|
|
736
|
+
async function cmdMap(argv: string[]): Promise<number> {
|
|
737
|
+
const root = resolveRoot(argv);
|
|
738
|
+
if (!fs.existsSync(root)) {
|
|
739
|
+
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
742
|
+
const out = flag(argv, '--out');
|
|
743
|
+
const diffRev = flag(argv, '--diff');
|
|
744
|
+
const r = await buildMap(root, {
|
|
745
|
+
...(out ? { outDir: path.resolve(out) } : {}),
|
|
746
|
+
...(diffRev !== null ? { diffRev } : {}),
|
|
747
|
+
...(argv.includes('--prs') ? { prs: true } : {}),
|
|
748
|
+
...(argv.includes('--no-build') ? { noBuild: true } : {}),
|
|
749
|
+
});
|
|
750
|
+
const rel = path.relative(process.cwd(), r.outDir) || '.';
|
|
751
|
+
const largest = r.model.areas.find((a) => a.kind === 'area');
|
|
752
|
+
process.stdout.write(
|
|
753
|
+
`✅ code-graph map — ${r.meta.summary}\n` +
|
|
754
|
+
` graph: ${r.graphBuilt ? 'rebuilt' : 'reused'} · state ${r.meta.state} · commit ${r.meta.commit}\n` +
|
|
755
|
+
(largest ? ` largest area: ${largest.name} (${largest.files.length} files)\n` : '') +
|
|
756
|
+
(r.model.diff
|
|
757
|
+
? ` diff ${r.model.diff.rev}: ${r.model.diff.changed.length} changed · ${r.model.diff.impacted.length} linked into them · ${r.model.diff.areas.length} area(s) touched\n`
|
|
758
|
+
: '') +
|
|
759
|
+
` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n`,
|
|
760
|
+
);
|
|
761
|
+
let rc = 0;
|
|
762
|
+
if (argv.includes('--no-verify')) {
|
|
763
|
+
// A check from an earlier map must not sit beside this one as if it were about it.
|
|
764
|
+
fs.rmSync(path.join(r.outDir, 'map-render.json'), { force: true });
|
|
765
|
+
} else {
|
|
766
|
+
const check = await verifyMap(path.join(r.outDir, 'index.html'));
|
|
767
|
+
fs.writeFileSync(path.join(r.outDir, 'map-render.json'), `${JSON.stringify(check, null, 2)}\n`);
|
|
768
|
+
const problems = check.viewports
|
|
769
|
+
.flatMap((v) => [
|
|
770
|
+
...(v.pageErrors ? [`${v.name}: ${v.pageErrors} page error(s)`] : []),
|
|
771
|
+
...(v.externalRequests ? [`${v.name}: ${v.externalRequests} network request(s)`] : []),
|
|
772
|
+
...(v.horizontalScroll ? [`${v.name}: horizontal scroll`] : []),
|
|
773
|
+
...v.views.filter((x) => x.outside || x.overlapping).map((x) => `${v.name}/${x.view}: ${x.outside} label(s) cut off, ${x.overlapping} overlapping`),
|
|
774
|
+
])
|
|
775
|
+
.join('; ');
|
|
776
|
+
process.stdout.write(
|
|
777
|
+
` render check: ${check.verification}` +
|
|
778
|
+
(check.reason ? ` — ${check.reason}` : '') +
|
|
779
|
+
(problems ? ` — ${problems}` : check.verification === 'verified' ? ` — ${check.viewports.map((v) => v.name).join(', ')}` : '') +
|
|
780
|
+
'\n',
|
|
781
|
+
);
|
|
782
|
+
if (check.verification === 'failed') rc = 1;
|
|
783
|
+
}
|
|
784
|
+
if (argv.includes('--open')) {
|
|
785
|
+
const { default: open } = await import('open');
|
|
786
|
+
await open(path.join(r.outDir, 'index.html'));
|
|
787
|
+
}
|
|
788
|
+
return rc;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* `outcome <path> useful|dead-end` — record, locally, that a session found a
|
|
793
|
+
* file useful or a dead end. The map draws the time-decayed result as a ring;
|
|
794
|
+
* nothing leaves the machine and nothing is committed.
|
|
795
|
+
*/
|
|
796
|
+
function cmdOutcome(argv: string[]): number {
|
|
797
|
+
const [file, outcome] = argv.filter((a) => !a.startsWith('--') && !isFlagValue(argv, a));
|
|
798
|
+
if (!file || (outcome !== 'useful' && outcome !== 'dead-end')) {
|
|
799
|
+
process.stderr.write('usage: code_graph/cli.ts outcome <path> useful|dead-end [--root P]\n');
|
|
800
|
+
return 2;
|
|
801
|
+
}
|
|
802
|
+
const root = resolveRoot(argv);
|
|
803
|
+
try {
|
|
804
|
+
recordOutcome(root, file, outcome);
|
|
805
|
+
} catch (e) {
|
|
806
|
+
process.stderr.write(`code-graph: ${(e as Error).message}\n`);
|
|
807
|
+
return 2;
|
|
808
|
+
}
|
|
809
|
+
process.stdout.write(`recorded: ${file} ${outcome}\n`);
|
|
810
|
+
return 0;
|
|
811
|
+
}
|
|
812
|
+
|
|
693
813
|
/**
|
|
694
814
|
* `suggest-verb "<question>"` (Phase 9 D3) — print the `code_graph` verb a
|
|
695
815
|
* pure regex table thinks best answers a free-text structure question. A
|
|
@@ -719,9 +839,11 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
719
839
|
if (sub === 'untested') return cmdUntested(argv.slice(1));
|
|
720
840
|
if (sub === 'dead') return cmdDead(argv.slice(1));
|
|
721
841
|
if (sub === 'suggest-verb') return cmdSuggestVerb(argv.slice(1));
|
|
842
|
+
if (sub === 'map') return await cmdMap(argv.slice(1));
|
|
843
|
+
if (sub === 'outcome') return cmdOutcome(argv.slice(1));
|
|
722
844
|
process.stderr.write(
|
|
723
845
|
'usage: code_graph/cli.ts <build|validate|detect|refresh|query|explain|affected|path|' +
|
|
724
|
-
'impact|tests-for|untested|dead|suggest-verb> [options]\n',
|
|
846
|
+
'impact|tests-for|untested|dead|suggest-verb|map|outcome> [options]\n',
|
|
725
847
|
);
|
|
726
848
|
return 2;
|
|
727
849
|
} catch (e) {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* query use, and is it fresh?" across three sources:
|
|
4
4
|
*
|
|
5
5
|
* consumer — a `graph.json`-shaped artifact the consumer repo ships
|
|
6
|
-
* scip — an `index.scip
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* scip — an `index.scip`, read into the native build as a provider of
|
|
7
|
+
* compiler-stated edges (`scip_reader.ts`); never queried on
|
|
8
|
+
* its own
|
|
9
9
|
* native — the suite's own gitignored cache
|
|
10
10
|
*
|
|
11
11
|
* Precedence (ADR-124 § 2): a fresh consumer index wins (interop courtesy);
|
|
@@ -156,7 +156,8 @@ export function detectSources(root: string, nativeCache: string): SourceVerdict[
|
|
|
156
156
|
/* ignore */
|
|
157
157
|
}
|
|
158
158
|
const scip = scipCandidates.find((p) => fs.existsSync(p));
|
|
159
|
-
|
|
159
|
+
// Only `index.scip` is read; another `*.scip` is reported, not merged.
|
|
160
|
+
if (scip) out.push({ kind: 'scip', path: scip, present: true, note: 'SCIP — index.scip merges into the build' });
|
|
160
161
|
|
|
161
162
|
// native cache
|
|
162
163
|
if (fs.existsSync(nativeCache)) out.push({ kind: 'native', path: nativeCache, present: true, ...freshness(abs, nativeCache, null) });
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs layer: a Markdown file that names a file gets an edge to it.
|
|
3
|
+
*
|
|
4
|
+
* Read by rule, not by a model (road-to-a-map-that-draws-itself 6.3, K10):
|
|
5
|
+
*
|
|
6
|
+
* [text](relative/or/rooted/path) a link, resolved from the document's directory
|
|
7
|
+
* `src/scripts/x.ts` · `x.ts:42` a backticked path, resolved repo-relative first
|
|
8
|
+
*
|
|
9
|
+
* An edge is emitted only when the named file is in the tree, so a link to a
|
|
10
|
+
* URL, an anchor, or a path that moved away produces nothing rather than a guess.
|
|
11
|
+
*/
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import type { SourceFile } from './build.js';
|
|
15
|
+
import type { CodeEdge, CodeNode } from './types.js';
|
|
16
|
+
import { MAX_FILE_BYTES } from './types.js';
|
|
17
|
+
|
|
18
|
+
export const DOC_EXTS: ReadonlySet<string> = new Set(['.md', '.mdx']);
|
|
19
|
+
|
|
20
|
+
function normal(p: string): string {
|
|
21
|
+
return path.posix.normalize(p).replace(/^\.\//, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function docsLayer(docs: readonly SourceFile[], fileIds: ReadonlySet<string>): { nodes: CodeNode[]; edges: CodeEdge[] } {
|
|
25
|
+
const nodes: CodeNode[] = [];
|
|
26
|
+
const edges: CodeEdge[] = [];
|
|
27
|
+
for (const d of [...docs].sort((a, b) => (a.path < b.path ? -1 : 1))) {
|
|
28
|
+
nodes.push({ id: d.path, label: d.path, kind: 'file', source_file: d.path, source_location: [] });
|
|
29
|
+
if (Buffer.byteLength(d.source, 'utf-8') > MAX_FILE_BYTES) continue;
|
|
30
|
+
const dir = path.posix.dirname(d.path);
|
|
31
|
+
const targets = new Set<string>();
|
|
32
|
+
const consider = (raw: string, fromDocDir: boolean): void => {
|
|
33
|
+
const clean = raw.trim().replace(/[#?].*$/, '').replace(/:\d+(?:-\d+)?$/, '');
|
|
34
|
+
if (!clean || /^[a-z][a-z0-9+.-]*:/i.test(clean) || clean.includes('*')) return;
|
|
35
|
+
const candidates = clean.startsWith('/')
|
|
36
|
+
? [normal(clean.slice(1))]
|
|
37
|
+
: fromDocDir
|
|
38
|
+
? [normal(path.posix.join(dir, clean)), normal(clean)]
|
|
39
|
+
: [normal(clean), normal(path.posix.join(dir, clean))];
|
|
40
|
+
const hit = candidates.find((c) => !c.startsWith('..') && fileIds.has(c));
|
|
41
|
+
if (hit && hit !== d.path) targets.add(hit);
|
|
42
|
+
};
|
|
43
|
+
for (const m of d.source.matchAll(/\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g)) consider(m[1] as string, true);
|
|
44
|
+
for (const m of d.source.matchAll(/`([\w@./-]+\/[\w@.-]+\.[A-Za-z0-9]+(?::\d+(?:-\d+)?)?)`/g)) consider(m[1] as string, false);
|
|
45
|
+
for (const t of [...targets].sort()) {
|
|
46
|
+
edges.push({
|
|
47
|
+
source: d.path,
|
|
48
|
+
target: t,
|
|
49
|
+
relation: 'documents',
|
|
50
|
+
confidence: 'EXTRACTED',
|
|
51
|
+
resolved_via: 'directive-scan',
|
|
52
|
+
provider: 'native',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { nodes, edges };
|
|
57
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
MAX_FILE_BYTES,
|
|
21
21
|
type Relation,
|
|
22
22
|
} from './types.js';
|
|
23
|
+
import { extractGo, extractJava, extractPython } from './extract_more.js';
|
|
23
24
|
import { getParser, nodeHasError, type TsNode } from './loader.js';
|
|
24
25
|
|
|
25
26
|
export type CallStyle = 'new' | 'free' | 'this' | 'self' | 'static' | 'parent' | 'scoped' | 'dynamic';
|
|
@@ -89,7 +90,7 @@ export interface FileExtract {
|
|
|
89
90
|
parseError: boolean;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
function loc(n: TsNode): number[] {
|
|
93
|
+
export function loc(n: TsNode): number[] {
|
|
93
94
|
return [
|
|
94
95
|
n.startPosition.row + 1,
|
|
95
96
|
n.startPosition.column + 1,
|
|
@@ -98,7 +99,7 @@ function loc(n: TsNode): number[] {
|
|
|
98
99
|
];
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function namedChildren(n: TsNode): TsNode[] {
|
|
102
|
+
export function namedChildren(n: TsNode): TsNode[] {
|
|
102
103
|
const out: TsNode[] = [];
|
|
103
104
|
for (let i = 0; i < n.namedChildCount; i += 1) {
|
|
104
105
|
const c = n.namedChild(i);
|
|
@@ -117,7 +118,7 @@ function firstNamed(n: TsNode, types: ReadonlySet<string>): TsNode | null {
|
|
|
117
118
|
return null;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
function nameField(n: TsNode): string | null {
|
|
121
|
+
export function nameField(n: TsNode): string | null {
|
|
121
122
|
const nm = n.childForFieldName('name');
|
|
122
123
|
if (nm) return nm.text;
|
|
123
124
|
return null;
|
|
@@ -131,6 +132,26 @@ function baseName(raw: string): string {
|
|
|
131
132
|
return parts[parts.length - 1] ?? s;
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Ids are `${file}#${name}`, so two same-named declarations in one file — a
|
|
137
|
+
* `type X` beside a `const X`, a getter and its setter, two nested helpers both
|
|
138
|
+
* called `walk` — used to share one id. The SQLite twin's PRIMARY KEY then
|
|
139
|
+
* refused the insert and the index was silently never written. The second and
|
|
140
|
+
* later declarations get `@2`, `@3`, … in declaration order, which the walk
|
|
141
|
+
* visits deterministically.
|
|
142
|
+
*/
|
|
143
|
+
const takenIds = new WeakMap<FileExtract, Map<string, number>>();
|
|
144
|
+
export function uniq(out: FileExtract, base: string): string {
|
|
145
|
+
let seen = takenIds.get(out);
|
|
146
|
+
if (!seen) {
|
|
147
|
+
seen = new Map();
|
|
148
|
+
takenIds.set(out, seen);
|
|
149
|
+
}
|
|
150
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
151
|
+
seen.set(base, n);
|
|
152
|
+
return n === 1 ? base : `${base}@${n}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
// ── PHP ──────────────────────────────────────────────────────────────────────
|
|
135
156
|
|
|
136
157
|
function extractPhp(root: TsNode, file: string, out: FileExtract): void {
|
|
@@ -163,7 +184,7 @@ function extractPhp(root: TsNode, file: string, out: FileExtract): void {
|
|
|
163
184
|
: n.type === 'interface_declaration'
|
|
164
185
|
? 'interface'
|
|
165
186
|
: 'trait';
|
|
166
|
-
const id = `${file}#${nm}
|
|
187
|
+
const id = uniq(out, `${file}#${nm}`);
|
|
167
188
|
out.nodes.push({ id, label: nm, kind, source_file: file, source_location: loc(n) });
|
|
168
189
|
out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
169
190
|
// extends / implements
|
|
@@ -198,7 +219,7 @@ function extractPhp(root: TsNode, file: string, out: FileExtract): void {
|
|
|
198
219
|
case 'method_declaration':
|
|
199
220
|
case 'function_definition': {
|
|
200
221
|
const nm = nameField(n) ?? '<anon>';
|
|
201
|
-
const id = classId ? `${classId}::${nm}` : `${file}#${nm}
|
|
222
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
202
223
|
out.nodes.push({
|
|
203
224
|
id,
|
|
204
225
|
label: nm,
|
|
@@ -377,14 +398,14 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
377
398
|
: n.type === 'type_alias_declaration'
|
|
378
399
|
? 'type'
|
|
379
400
|
: 'enum';
|
|
380
|
-
const id = `${file}#${nm}
|
|
401
|
+
const id = uniq(out, `${file}#${nm}`);
|
|
381
402
|
out.nodes.push({ id, label: nm, kind, source_file: file, source_location: loc(n) });
|
|
382
403
|
out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
383
404
|
return;
|
|
384
405
|
}
|
|
385
406
|
case 'class_declaration': {
|
|
386
407
|
const nm = nameField(n) ?? '<anon>';
|
|
387
|
-
const id = `${file}#${nm}
|
|
408
|
+
const id = uniq(out, `${file}#${nm}`);
|
|
388
409
|
out.nodes.push({ id, label: nm, kind: 'class', source_file: file, source_location: loc(n) });
|
|
389
410
|
out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
390
411
|
const heritage = firstNamed(n, new Set(['class_heritage']));
|
|
@@ -399,7 +420,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
399
420
|
}
|
|
400
421
|
case 'method_definition': {
|
|
401
422
|
const nm = nameField(n) ?? '<anon>';
|
|
402
|
-
const id = classId ? `${classId}::${nm}` : `${file}#${nm}
|
|
423
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
403
424
|
out.nodes.push({ id, label: nm, kind: 'method', source_file: file, source_location: loc(n) });
|
|
404
425
|
out.rawEdges.push({
|
|
405
426
|
sourceId: classId ?? fileId,
|
|
@@ -413,7 +434,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
413
434
|
}
|
|
414
435
|
case 'function_declaration': {
|
|
415
436
|
const nm = nameField(n) ?? '<anon>';
|
|
416
|
-
const id = `${file}#${nm}
|
|
437
|
+
const id = uniq(out, `${file}#${nm}`);
|
|
417
438
|
out.nodes.push({ id, label: nm, kind: 'function', source_file: file, source_location: loc(n) });
|
|
418
439
|
out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
419
440
|
const body = n.childForFieldName('body');
|
|
@@ -457,7 +478,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
457
478
|
// none of them is a declaration this graph can id.
|
|
458
479
|
if (topLevel && nmNode?.type === 'identifier') {
|
|
459
480
|
const nm = nmNode.text;
|
|
460
|
-
const id = classId ? `${classId}::${nm}` : `${file}#${nm}
|
|
481
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
461
482
|
out.nodes.push({
|
|
462
483
|
id,
|
|
463
484
|
label: nm,
|
|
@@ -482,7 +503,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
482
503
|
continue;
|
|
483
504
|
}
|
|
484
505
|
const nm = nameField(d) ?? '<anon>';
|
|
485
|
-
const id = classId ? `${classId}::${nm}` : `${file}#${nm}
|
|
506
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
486
507
|
out.nodes.push({
|
|
487
508
|
id,
|
|
488
509
|
label: nm,
|
|
@@ -519,7 +540,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
519
540
|
const value = n.childForFieldName('value');
|
|
520
541
|
const nm = nameField(n) ?? '<anon>';
|
|
521
542
|
if (value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
|
522
|
-
const id = classId ? `${classId}::${nm}` : `${file}#${nm}
|
|
543
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
523
544
|
out.nodes.push({
|
|
524
545
|
id,
|
|
525
546
|
label: nm,
|
|
@@ -634,6 +655,27 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
634
655
|
walk(root, null, fileId, true);
|
|
635
656
|
}
|
|
636
657
|
|
|
658
|
+
/**
|
|
659
|
+
* A Vue single-file component reduced to its `<script>` blocks: every
|
|
660
|
+
* character outside them becomes a space and every newline stays, so the
|
|
661
|
+
* TypeScript grammar parses the script and every location still points at
|
|
662
|
+
* the line in the `.vue` file.
|
|
663
|
+
*/
|
|
664
|
+
export function vueScript(source: string): string {
|
|
665
|
+
const keep = new Array<boolean>(source.length).fill(false);
|
|
666
|
+
for (const m of source.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
667
|
+
const start = (m.index as number) + m[0].indexOf('>') + 1;
|
|
668
|
+
const end = start + (m[1] as string).length;
|
|
669
|
+
for (let i = start; i < end; i++) keep[i] = true;
|
|
670
|
+
}
|
|
671
|
+
let out = '';
|
|
672
|
+
for (let i = 0; i < source.length; i++) {
|
|
673
|
+
const c = source[i] as string;
|
|
674
|
+
out += keep[i] || c === '\n' ? c : ' ';
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
|
|
637
679
|
/**
|
|
638
680
|
* Extract one file. `source` is the file's UTF-8 content; `relPath` is the
|
|
639
681
|
* repo-relative POSIX path used to build collision-free node ids.
|
|
@@ -651,10 +693,13 @@ export async function extractFile(relPath: string, source: string, lang: Lang):
|
|
|
651
693
|
return out;
|
|
652
694
|
}
|
|
653
695
|
const parser = await getParser(lang);
|
|
654
|
-
const tree = parser.parse(source);
|
|
696
|
+
const tree = parser.parse(relPath.endsWith('.vue') ? vueScript(source) : source);
|
|
655
697
|
try {
|
|
656
698
|
out.parseError = nodeHasError(tree.rootNode);
|
|
657
699
|
if (lang === 'php') extractPhp(tree.rootNode, relPath, out);
|
|
700
|
+
else if (lang === 'python') extractPython(tree.rootNode, relPath, out);
|
|
701
|
+
else if (lang === 'java') extractJava(tree.rootNode, relPath, out);
|
|
702
|
+
else if (lang === 'go') extractGo(tree.rootNode, relPath, out);
|
|
658
703
|
else extractTsJs(tree.rootNode, relPath, out);
|
|
659
704
|
} finally {
|
|
660
705
|
tree.delete(); // delete the tree, never the cached parser (loader.ts)
|