@event4u/agent-config 14.17.0 → 14.18.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 +170 -79
- package/README.md +1 -1
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-review/SKILL.md +2 -0
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +12 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +335 -659
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +15 -15
- 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/dispatch.js +959 -1304
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +671 -1002
- package/dist/shared/settingsConsent.js +1 -1
- package/package.json +1 -1
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +129 -0
- package/src/scripts/_lib/release_highlights.ts +172 -1
- package/src/scripts/_lib/release_material.ts +66 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/check_finding_dispositions.ts +179 -3
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +25 -13
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +2 -2
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/git_authorization_hook.ts +153 -17
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +33 -19
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +25 -369
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/schemas/review-findings.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +11 -1
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -20,6 +20,7 @@ import { emitSqliteTwin } from './sqlite_store.js';
|
|
|
20
20
|
import {
|
|
21
21
|
type CodeEdge,
|
|
22
22
|
type CodeGraph,
|
|
23
|
+
type EdgeConfidence,
|
|
23
24
|
EXPECTED_GRAMMAR_ABI,
|
|
24
25
|
EXT_LANG,
|
|
25
26
|
type Lang,
|
|
@@ -110,6 +111,87 @@ interface SymbolTable {
|
|
|
110
111
|
classByName: Map<string, string>;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* What a resolved name is allowed to BE at a given use site.
|
|
116
|
+
*
|
|
117
|
+
* The council that reviewed this repair made the same point from both seats: a
|
|
118
|
+
* confidence label cannot contain a false edge, only describe one, so the
|
|
119
|
+
* candidate set has to be filtered by capability BEFORE a confidence is
|
|
120
|
+
* assigned. A `const` cannot satisfy `foo()`; a `type` alias cannot satisfy
|
|
121
|
+
* `new T()`. Without this, adding constant/type/enum nodes in 2.1 would feed
|
|
122
|
+
* the very same-name table that produced the false `path` edge in the first
|
|
123
|
+
* place.
|
|
124
|
+
*/
|
|
125
|
+
type Capability = 'callable' | 'constructible' | 'any';
|
|
126
|
+
const CALLABLE_KINDS: ReadonlySet<string> = new Set(['function', 'method']);
|
|
127
|
+
const CONSTRUCTIBLE_KINDS: ReadonlySet<string> = new Set(['class']);
|
|
128
|
+
|
|
129
|
+
function satisfies(kind: string | undefined, want: Capability): boolean {
|
|
130
|
+
if (kind === undefined) return true; // a file id / external ref — unknown, not disqualified
|
|
131
|
+
if (want === 'callable') return CALLABLE_KINDS.has(kind);
|
|
132
|
+
if (want === 'constructible') return CONSTRUCTIBLE_KINDS.has(kind);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* One file's name bindings — the thing this engine did not have, and whose
|
|
138
|
+
* absence is the whole of defect 1.1.
|
|
139
|
+
*
|
|
140
|
+
* `locals` is every symbol DECLARED in the file. `imports` is every name the
|
|
141
|
+
* file BINDS from elsewhere, keyed on the local binding name, carrying whether
|
|
142
|
+
* the binding was resolved from a module specifier (`exact`) or merely matched
|
|
143
|
+
* by name (PHP `use`, which names a fully-qualified symbol and is matched on
|
|
144
|
+
* its base name without checking the namespace).
|
|
145
|
+
*/
|
|
146
|
+
interface FileScope {
|
|
147
|
+
locals: Map<string, string[]>;
|
|
148
|
+
imports: Map<string, { target: string; exact: boolean }>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const REL_EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
152
|
+
|
|
153
|
+
/** POSIX-only join/dirname over graph ids, which are always POSIX-relative. */
|
|
154
|
+
function idDirname(id: string): string {
|
|
155
|
+
const i = id.lastIndexOf('/');
|
|
156
|
+
return i === -1 ? '' : id.slice(0, i);
|
|
157
|
+
}
|
|
158
|
+
function idJoin(dir: string, rel: string): string {
|
|
159
|
+
const parts = (dir === '' ? [] : dir.split('/')).concat(rel.split('/'));
|
|
160
|
+
const out: string[] = [];
|
|
161
|
+
for (const p of parts) {
|
|
162
|
+
if (p === '' || p === '.') continue;
|
|
163
|
+
if (p === '..') {
|
|
164
|
+
// A `..` that would escape the root is KEPT, so the caller can see
|
|
165
|
+
// the specifier points outside the indexed tree rather than
|
|
166
|
+
// silently resolving to something inside it.
|
|
167
|
+
if (out.length && out[out.length - 1] !== '..') out.pop();
|
|
168
|
+
else out.push('..');
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
out.push(p);
|
|
172
|
+
}
|
|
173
|
+
return out.join('/');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A relative module specifier → the id of the file node it names, or `null`
|
|
178
|
+
* when that file is not in this graph (out of root, or an unsupported
|
|
179
|
+
* extension). A bare specifier (`node:path`, `js-yaml`) is external by
|
|
180
|
+
* definition and returns `null` too — the caller distinguishes the two by
|
|
181
|
+
* looking at the specifier itself.
|
|
182
|
+
*
|
|
183
|
+
* TS source imports `./types.js` and the file on disk is `types.ts`, so the
|
|
184
|
+
* runtime extension is stripped before the candidate list is tried.
|
|
185
|
+
*/
|
|
186
|
+
function resolveSpecifier(fromFile: string, spec: string, fileIds: ReadonlySet<string>): string | null {
|
|
187
|
+
if (!spec.startsWith('.')) return null;
|
|
188
|
+
const base = idJoin(idDirname(fromFile), spec);
|
|
189
|
+
const stem = base.replace(/\.(js|jsx|mjs|cjs)$/, '');
|
|
190
|
+
const candidates = [base, ...REL_EXTS.map((e) => stem + e), ...REL_EXTS.map((e) => base + '/index' + e)];
|
|
191
|
+
for (const c of candidates) if (fileIds.has(c)) return c;
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
113
195
|
function push(map: Map<string, string[]>, k: string, v: string): void {
|
|
114
196
|
const arr = map.get(k);
|
|
115
197
|
if (arr) {
|
|
@@ -146,6 +228,11 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
146
228
|
};
|
|
147
229
|
for (const n of nodes) {
|
|
148
230
|
const lang = nodeLang.get(n.id) ?? null;
|
|
231
|
+
// `byName` stays the CALLABLE/CONSTRUCTIBLE table it always was. The
|
|
232
|
+
// kinds 2.1 added (constant / type / enum) are deliberately NOT put in
|
|
233
|
+
// it: they are reachable through a file's own scope and through
|
|
234
|
+
// `imports`, and letting them satisfy a bare `foo()` by name is the
|
|
235
|
+
// defect this roadmap removes, not a feature it adds.
|
|
149
236
|
if (n.kind === 'class' || n.kind === 'interface' || n.kind === 'trait' || n.kind === 'function') {
|
|
150
237
|
push(sym.byName, k(lang, n.label), n.id);
|
|
151
238
|
if (n.kind !== 'function' && !sym.classByName.has(k(lang, n.label)))
|
|
@@ -184,6 +271,87 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
184
271
|
return null;
|
|
185
272
|
};
|
|
186
273
|
|
|
274
|
+
const nodeKind = new Map(nodes.map((n) => [n.id, n.kind as string]));
|
|
275
|
+
const fileIds = new Set(nodes.filter((n) => n.kind === 'file').map((n) => n.id));
|
|
276
|
+
const scopes = new Map<string, FileScope>();
|
|
277
|
+
for (const ex of extracts) {
|
|
278
|
+
const locals = new Map<string, string[]>();
|
|
279
|
+
for (const n of ex.nodes) if (n.kind !== 'file') push(locals, n.label, n.id);
|
|
280
|
+
const imports = new Map<string, { target: string; exact: boolean }>();
|
|
281
|
+
for (const r of ex.rawEdges) {
|
|
282
|
+
if (r.relation !== 'imports') continue;
|
|
283
|
+
const local = r.localName ?? r.targetName;
|
|
284
|
+
if (imports.has(local)) continue; // first binding wins, deterministically
|
|
285
|
+
const spec = r.moduleSpecifier;
|
|
286
|
+
if (spec === undefined) {
|
|
287
|
+
// PHP `use A\B\C` — a fully-qualified NAME, not a module path.
|
|
288
|
+
// Resolved by base name without checking the namespace, so the
|
|
289
|
+
// binding is a lookup, not a syntactic fact: `exact: false`.
|
|
290
|
+
const hit =
|
|
291
|
+
sym.classByName.get(k(ex.lang, r.targetName)) ??
|
|
292
|
+
(sym.byName.get(k(ex.lang, r.targetName)) ?? []).find((id) => id.includes('#') && !id.includes('::'));
|
|
293
|
+
if (hit) imports.set(local, { target: hit, exact: false });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const file = resolveSpecifier(ex.file, spec, fileIds);
|
|
297
|
+
if (file === null) {
|
|
298
|
+
// Bare (`node:path`, `zod`) or relative-but-out-of-root. Either
|
|
299
|
+
// way the module is real and NAMED, and it is not this
|
|
300
|
+
// repository's same-named symbol. `import * as path from
|
|
301
|
+
// 'node:path'` ends here, which is the whole of 1.1.
|
|
302
|
+
const named = r.importKind === 'named';
|
|
303
|
+
imports.set(local, { target: named ? `external:${spec}#${r.targetName}` : `external:${spec}`, exact: true });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// A namespace or default import binds the MODULE, never a
|
|
307
|
+
// same-named export inside it.
|
|
308
|
+
const member = `${file}#${r.targetName}`;
|
|
309
|
+
const target = r.importKind === 'named' && nodeKind.has(member) ? member : file;
|
|
310
|
+
imports.set(local, { target, exact: true });
|
|
311
|
+
}
|
|
312
|
+
scopes.set(ex.file, { locals, imports });
|
|
313
|
+
}
|
|
314
|
+
const emptyScope: FileScope = { locals: new Map(), imports: new Map() };
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Resolve a bare name at a use site inside `file`, capability-filtered.
|
|
318
|
+
*
|
|
319
|
+
* The ladder is the language's own: a LOCAL declaration shadows an import
|
|
320
|
+
* (both council seats named this, and JS/TS/PHP all agree), an import
|
|
321
|
+
* resolved from a specifier is next, and only then the repo-wide same-name
|
|
322
|
+
* table — which is a guess and is labelled as one.
|
|
323
|
+
*/
|
|
324
|
+
const resolveInScope = (
|
|
325
|
+
file: string,
|
|
326
|
+
name: string,
|
|
327
|
+
want: Capability,
|
|
328
|
+
lang: Lang,
|
|
329
|
+
scopeOnly = false,
|
|
330
|
+
): { target: string; confidence: EdgeConfidence } | null => {
|
|
331
|
+
const scope = scopes.get(file) ?? emptyScope;
|
|
332
|
+
for (const id of scope.locals.get(name) ?? [])
|
|
333
|
+
if (satisfies(nodeKind.get(id), want)) return { target: id, confidence: 'EXTRACTED' };
|
|
334
|
+
const bound = scope.imports.get(name);
|
|
335
|
+
if (bound && satisfies(nodeKind.get(bound.target), want))
|
|
336
|
+
return { target: bound.target, confidence: bound.exact ? 'EXTRACTED' : 'INFERRED' };
|
|
337
|
+
if (scopeOnly) return null;
|
|
338
|
+
const wide =
|
|
339
|
+
want === 'constructible'
|
|
340
|
+
? sym.classByName.get(k(lang, name))
|
|
341
|
+
: (sym.byName.get(k(lang, name)) ?? []).find(
|
|
342
|
+
(id) => id.includes('#') && !id.includes('::') && satisfies(nodeKind.get(id), want),
|
|
343
|
+
);
|
|
344
|
+
// Reached only by matching a name across the whole repository, with no
|
|
345
|
+
// binding in this file to justify it. That is not a syntactic fact and
|
|
346
|
+
// no longer claims to be one.
|
|
347
|
+
if (wide) return { target: wide, confidence: 'INFERRED' };
|
|
348
|
+
return null;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Deduped the same way emitted edges are, so the count is comparable to
|
|
352
|
+
// `edges.length` rather than being a raw call-site tally that reads as a
|
|
353
|
+
// larger loss than the graph actually took.
|
|
354
|
+
const suppressedKeys = new Set<string>();
|
|
187
355
|
const edges: CodeEdge[] = [];
|
|
188
356
|
const seen = new Set<string>();
|
|
189
357
|
const emit = (e: CodeEdge): void => {
|
|
@@ -198,8 +366,13 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
198
366
|
for (const ex of extracts) {
|
|
199
367
|
// structural inherits edges (same-language resolution)
|
|
200
368
|
for (const inh of ex.inherits) {
|
|
201
|
-
const
|
|
202
|
-
emit({
|
|
369
|
+
const hit = resolveInScope(ex.file, inh.parentName, 'constructible', ex.lang);
|
|
370
|
+
emit({
|
|
371
|
+
source: inh.classId,
|
|
372
|
+
target: hit?.target ?? `symbol:${inh.parentName}`,
|
|
373
|
+
relation: 'inherits',
|
|
374
|
+
confidence: hit?.confidence ?? 'EXTRACTED',
|
|
375
|
+
});
|
|
203
376
|
}
|
|
204
377
|
for (const r of ex.rawEdges) resolveRawEdge(r, ex);
|
|
205
378
|
}
|
|
@@ -216,50 +389,50 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
216
389
|
});
|
|
217
390
|
return;
|
|
218
391
|
}
|
|
219
|
-
if (r.relation === 'imports'
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
// which is barely better than the missing edge it replaced.
|
|
226
|
-
//
|
|
227
|
-
// Same predicate as the `free` call branch below (`#` and no `::`),
|
|
228
|
-
// so a function reference and a function call resolve identically
|
|
229
|
-
// rather than by two rules that can drift.
|
|
230
|
-
const resolved =
|
|
231
|
-
sym.classByName.get(k(ex.lang, r.targetName)) ??
|
|
232
|
-
(sym.byName.get(k(ex.lang, r.targetName)) ?? []).find(
|
|
233
|
-
(id) => id.includes('#') && !id.includes('::'),
|
|
234
|
-
);
|
|
392
|
+
if (r.relation === 'imports') {
|
|
393
|
+
// The import's own binding, computed once per file above. A
|
|
394
|
+
// specifier-resolved binding is a syntactic fact — the file says
|
|
395
|
+
// where the name comes from — and a PHP `use` matched by base name
|
|
396
|
+
// is not, and says so.
|
|
397
|
+
const bound = (scopes.get(ex.file) ?? emptyScope).imports.get(r.localName ?? r.targetName);
|
|
235
398
|
emit({
|
|
236
399
|
source: r.sourceId,
|
|
237
|
-
target:
|
|
238
|
-
relation:
|
|
239
|
-
confidence: 'EXTRACTED',
|
|
400
|
+
target: bound?.target ?? `symbol:${r.targetName}`,
|
|
401
|
+
relation: 'imports',
|
|
402
|
+
confidence: bound ? (bound.exact ? 'EXTRACTED' : 'INFERRED') : 'EXTRACTED',
|
|
240
403
|
});
|
|
241
404
|
return;
|
|
242
405
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
406
|
+
if (r.relation === 'uses') {
|
|
407
|
+
// A registry literal (`{ foo: handleFoo }`) or an object shorthand
|
|
408
|
+
// (`{ id, label }`) names something bound IN THIS FILE. It is
|
|
409
|
+
// resolved through the file's scope and NOWHERE else: a shorthand
|
|
410
|
+
// property almost always names a local variable, and matching it
|
|
411
|
+
// against a same-named declaration elsewhere in the repository is
|
|
412
|
+
// the defect this roadmap removes, applied to a second relation.
|
|
413
|
+
const hit = resolveInScope(ex.file, r.targetName, 'any', ex.lang, true);
|
|
246
414
|
emit({
|
|
247
415
|
source: r.sourceId,
|
|
248
|
-
target:
|
|
249
|
-
relation: '
|
|
250
|
-
confidence: 'EXTRACTED',
|
|
416
|
+
target: hit?.target ?? `symbol:${r.targetName}`,
|
|
417
|
+
relation: 'uses',
|
|
418
|
+
confidence: hit?.confidence ?? 'EXTRACTED',
|
|
251
419
|
});
|
|
252
420
|
return;
|
|
253
421
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
422
|
+
// relation === 'calls'
|
|
423
|
+
if (r.callStyle === 'new' || r.callStyle === 'free') {
|
|
424
|
+
const want: Capability = r.callStyle === 'new' ? 'constructible' : 'callable';
|
|
425
|
+
const hit = resolveInScope(ex.file, r.targetName, want, ex.lang);
|
|
258
426
|
emit({
|
|
259
427
|
source: r.sourceId,
|
|
260
|
-
target:
|
|
428
|
+
target: hit?.target ?? `symbol:${r.targetName}`,
|
|
261
429
|
relation: 'calls',
|
|
262
|
-
|
|
430
|
+
// An UNRESOLVED name stays EXTRACTED: `symbol:strlen` claims
|
|
431
|
+
// nothing about a node, it records that a call to a name this
|
|
432
|
+
// repository does not declare happened here. What is no longer
|
|
433
|
+
// EXTRACTED is a claim about a SPECIFIC in-repo node reached by
|
|
434
|
+
// name alone.
|
|
435
|
+
confidence: hit?.confidence ?? 'EXTRACTED',
|
|
263
436
|
});
|
|
264
437
|
return;
|
|
265
438
|
}
|
|
@@ -287,6 +460,27 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
287
460
|
}
|
|
288
461
|
// scoped (Class::m unresolved / facade) or dynamic ($obj->m) → AMBIGUOUS
|
|
289
462
|
const cands = methodCandidates(r.targetName, ex.lang);
|
|
463
|
+
if (r.callStyle === 'dynamic' && cands.length === 0) {
|
|
464
|
+
// `x.push()`, `m.get()`, `s.join()` — a dynamic member call whose
|
|
465
|
+
// method name matches NO method declared in this repository. The
|
|
466
|
+
// only target available is `symbol:push`, which is not a node, so
|
|
467
|
+
// no query verb can reach it: `affected` and `path` resolve seeds
|
|
468
|
+
// against real nodes and a pseudo-node is never one. It was 257 of
|
|
469
|
+
// 660 edges on this engine's own source — 39 % of the graph
|
|
470
|
+
// asserting "a method named push was called somewhere".
|
|
471
|
+
//
|
|
472
|
+
// NOT suppressed: a `scoped` call (`Cache::get()`) and a
|
|
473
|
+
// `hierarchy-unresolved` `$this->missing()` both name a real
|
|
474
|
+
// unresolved intent inside a known receiver, which a later
|
|
475
|
+
// resolution pass could settle. This drops only the class where
|
|
476
|
+
// there is nothing to settle.
|
|
477
|
+
//
|
|
478
|
+
// The count is published in `suppressed_edge_counts` — an engine
|
|
479
|
+
// that silently deletes its worst edges reports a flattering
|
|
480
|
+
// confidence ratio and no evidence.
|
|
481
|
+
suppressedKeys.add(`${r.sourceId}\0calls\0symbol:${r.targetName}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
290
484
|
emit({
|
|
291
485
|
source: r.sourceId,
|
|
292
486
|
target: ambiguousTarget(cands, r.targetName),
|
|
@@ -310,6 +504,7 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
310
504
|
|
|
311
505
|
const counts = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
|
|
312
506
|
for (const e of edges) counts[e.confidence] += 1;
|
|
507
|
+
const suppressed = { dynamic_no_candidate: suppressedKeys.size };
|
|
313
508
|
|
|
314
509
|
const languages = [...new Set(extracts.map((e) => e.lang))].sort() as Lang[];
|
|
315
510
|
|
|
@@ -328,6 +523,7 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
328
523
|
languages,
|
|
329
524
|
grammar_abi: EXPECTED_GRAMMAR_ABI,
|
|
330
525
|
edge_confidence_counts: counts,
|
|
526
|
+
suppressed_edge_counts: suppressed,
|
|
331
527
|
nodes,
|
|
332
528
|
edges,
|
|
333
529
|
};
|
|
@@ -370,6 +566,7 @@ export function serializeGraph(g: CodeGraph): string {
|
|
|
370
566
|
'languages',
|
|
371
567
|
'grammar_abi',
|
|
372
568
|
'edge_confidence_counts',
|
|
569
|
+
'suppressed_edge_counts',
|
|
373
570
|
'nodes',
|
|
374
571
|
'edges',
|
|
375
572
|
];
|
|
@@ -36,6 +36,37 @@ export interface RawEdge {
|
|
|
36
36
|
enclosingClassId?: string;
|
|
37
37
|
/** scope token for `Scope::m()` scoped calls (a class name / self / static). */
|
|
38
38
|
scopeName?: string;
|
|
39
|
+
/**
|
|
40
|
+
* `imports` only: the module specifier as written (`'./types.js'`,
|
|
41
|
+
* `'node:path'`), stripped of its quotes.
|
|
42
|
+
*
|
|
43
|
+
* Before this field the extractor read the import CLAUSE and threw the
|
|
44
|
+
* `from` away, so the build pass had nothing to resolve against but a
|
|
45
|
+
* repo-wide same-name table — which is how `import * as path from
|
|
46
|
+
* 'node:path'` came to bind to this repository's own `path()` function in
|
|
47
|
+
* `query.ts`, at `EXTRACTED`, in four of eleven files. The specifier is the
|
|
48
|
+
* one piece of evidence that says WHERE the name came from, and it is in
|
|
49
|
+
* the source text; discarding it turned a syntactic fact into a guess.
|
|
50
|
+
*
|
|
51
|
+
* Absent for PHP `use`, which names a fully-qualified symbol rather than a
|
|
52
|
+
* module path — those still resolve by name, and the build pass labels them
|
|
53
|
+
* accordingly.
|
|
54
|
+
*/
|
|
55
|
+
moduleSpecifier?: string;
|
|
56
|
+
/**
|
|
57
|
+
* `imports` only: which import form produced this binding. `namespace`
|
|
58
|
+
* (`import * as x`) and `default` bind the MODULE (or its default export),
|
|
59
|
+
* never a named export, so neither may be matched against a same-named
|
|
60
|
+
* symbol inside the target file.
|
|
61
|
+
*/
|
|
62
|
+
importKind?: 'namespace' | 'named' | 'default';
|
|
63
|
+
/**
|
|
64
|
+
* `imports` only: the LOCAL binding name, when it differs from
|
|
65
|
+
* `targetName` (`import { foo as bar }` → targetName `foo`, localName
|
|
66
|
+
* `bar`). The file's scope is keyed on the local name; the target file is
|
|
67
|
+
* searched for the remote one.
|
|
68
|
+
*/
|
|
69
|
+
localName?: string;
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
export interface FileExtract {
|
|
@@ -245,29 +276,101 @@ function extractPhp(root: TsNode, file: string, out: FileExtract): void {
|
|
|
245
276
|
|
|
246
277
|
// ── TypeScript / JavaScript ───────────────────────────────────────────────────
|
|
247
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Node types that open a new function or class scope. A `const` inside one of
|
|
281
|
+
* these is a LOCAL variable, not a module-level declaration, and emitting a
|
|
282
|
+
* node for it would put every loop counter in the symbol table — the
|
|
283
|
+
* count-inflation failure `2.1` is explicitly not allowed to cause.
|
|
284
|
+
*/
|
|
285
|
+
const TS_SCOPE_INTRODUCERS = new Set([
|
|
286
|
+
'arrow_function',
|
|
287
|
+
'function_expression',
|
|
288
|
+
'function_declaration',
|
|
289
|
+
'generator_function',
|
|
290
|
+
'generator_function_declaration',
|
|
291
|
+
'method_definition',
|
|
292
|
+
'class_declaration',
|
|
293
|
+
'class',
|
|
294
|
+
'class_body',
|
|
295
|
+
]);
|
|
296
|
+
|
|
248
297
|
function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
249
298
|
const fileId = file;
|
|
250
299
|
|
|
251
|
-
const walk = (n: TsNode, classId: string | null, scopeId: string): void => {
|
|
300
|
+
const walk = (n: TsNode, classId: string | null, scopeId: string, topLevel: boolean): void => {
|
|
301
|
+
// A declaration is module-level until the walk descends through a
|
|
302
|
+
// function or class scope; from there down it is a local.
|
|
303
|
+
const inner = topLevel && !TS_SCOPE_INTRODUCERS.has(n.type);
|
|
252
304
|
switch (n.type) {
|
|
253
305
|
case 'import_statement': {
|
|
306
|
+
const src = n.childForFieldName('source');
|
|
307
|
+
// The specifier node is a `string`; its text carries the quotes.
|
|
308
|
+
const specifier = src ? src.text.replace(/^['"`]/, '').replace(/['"`]$/, '') : undefined;
|
|
254
309
|
const clause = firstNamed(n, new Set(['import_clause']));
|
|
310
|
+
const emitImport = (
|
|
311
|
+
remote: string,
|
|
312
|
+
local: string,
|
|
313
|
+
importKind: 'namespace' | 'named' | 'default',
|
|
314
|
+
): void => {
|
|
315
|
+
out.rawEdges.push({
|
|
316
|
+
sourceId: fileId,
|
|
317
|
+
relation: 'imports',
|
|
318
|
+
targetName: baseName(remote),
|
|
319
|
+
confidenceHint: 'EXTRACTED',
|
|
320
|
+
...(specifier === undefined ? {} : { moduleSpecifier: specifier }),
|
|
321
|
+
importKind,
|
|
322
|
+
...(local === remote ? {} : { localName: local }),
|
|
323
|
+
});
|
|
324
|
+
};
|
|
255
325
|
if (clause) {
|
|
256
|
-
for (const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
326
|
+
for (const child of namedChildren(clause)) {
|
|
327
|
+
if (child.type === 'namespace_import') {
|
|
328
|
+
// `import * as path from 'node:path'` — the binding
|
|
329
|
+
// is the MODULE. Matching `path` against a local
|
|
330
|
+
// symbol of the same name is the false edge 1.1
|
|
331
|
+
// exists to remove.
|
|
332
|
+
const idn = firstNamed(child, new Set(['identifier']));
|
|
333
|
+
if (idn) emitImport(idn.text, idn.text, 'namespace');
|
|
334
|
+
} else if (child.type === 'named_imports') {
|
|
335
|
+
for (const sp of namedChildren(child)) {
|
|
336
|
+
if (sp.type !== 'import_specifier') continue;
|
|
337
|
+
const nameNode = sp.childForFieldName('name');
|
|
338
|
+
const aliasNode = sp.childForFieldName('alias');
|
|
339
|
+
const remote = nameNode ? nameNode.text : sp.text;
|
|
340
|
+
emitImport(remote, aliasNode ? aliasNode.text : remote, 'named');
|
|
266
341
|
}
|
|
342
|
+
} else if (child.type === 'identifier') {
|
|
343
|
+
emitImport(child.text, child.text, 'default');
|
|
344
|
+
}
|
|
267
345
|
}
|
|
268
346
|
}
|
|
269
347
|
return;
|
|
270
348
|
}
|
|
349
|
+
// `const`, `type`, `interface`, `enum` — the shapes the v2 corpus
|
|
350
|
+
// asks `references` questions about (`EXT_LANG` is a const,
|
|
351
|
+
// `SettingsClass` a type alias). The extractor used to emit none of
|
|
352
|
+
// them, which is the named mechanical cause of `references` recall
|
|
353
|
+
// 0.333 against grep's 1.000.
|
|
354
|
+
//
|
|
355
|
+
// The body is not walked: a type or interface position holds no
|
|
356
|
+
// call sites, and an enum body holds member names rather than
|
|
357
|
+
// symbols this graph answers questions about.
|
|
358
|
+
case 'interface_declaration':
|
|
359
|
+
case 'type_alias_declaration':
|
|
360
|
+
case 'enum_declaration': {
|
|
361
|
+
const nmNode = n.childForFieldName('name') ?? firstNamed(n, NAME_TYPES);
|
|
362
|
+
const nm = nmNode ? nmNode.text : '<anon>';
|
|
363
|
+
const kind =
|
|
364
|
+
n.type === 'interface_declaration'
|
|
365
|
+
? 'interface'
|
|
366
|
+
: n.type === 'type_alias_declaration'
|
|
367
|
+
? 'type'
|
|
368
|
+
: 'enum';
|
|
369
|
+
const id = `${file}#${nm}`;
|
|
370
|
+
out.nodes.push({ id, label: nm, kind, source_file: file, source_location: loc(n) });
|
|
371
|
+
out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
271
374
|
case 'class_declaration': {
|
|
272
375
|
const nm = nameField(n) ?? '<anon>';
|
|
273
376
|
const id = `${file}#${nm}`;
|
|
@@ -280,7 +383,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
280
383
|
if (idn.type === 'identifier' || idn.type === 'type_identifier')
|
|
281
384
|
out.inherits.push({ classId: id, parentName: baseName(idn.text) });
|
|
282
385
|
const body = n.childForFieldName('body');
|
|
283
|
-
if (body) for (const c of namedChildren(body)) walk(c, id, id);
|
|
386
|
+
if (body) for (const c of namedChildren(body)) walk(c, id, id, false);
|
|
284
387
|
return;
|
|
285
388
|
}
|
|
286
389
|
case 'method_definition': {
|
|
@@ -294,7 +397,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
294
397
|
confidenceHint: 'EXTRACTED',
|
|
295
398
|
});
|
|
296
399
|
const body = n.childForFieldName('body');
|
|
297
|
-
if (body) for (const c of namedChildren(body)) walk(c, classId, id);
|
|
400
|
+
if (body) for (const c of namedChildren(body)) walk(c, classId, id, false);
|
|
298
401
|
return;
|
|
299
402
|
}
|
|
300
403
|
case 'function_declaration': {
|
|
@@ -303,7 +406,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
303
406
|
out.nodes.push({ id, label: nm, kind: 'function', source_file: file, source_location: loc(n) });
|
|
304
407
|
out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
305
408
|
const body = n.childForFieldName('body');
|
|
306
|
-
if (body) for (const c of namedChildren(body)) walk(c, classId, id);
|
|
409
|
+
if (body) for (const c of namedChildren(body)) walk(c, classId, id, false);
|
|
307
410
|
return;
|
|
308
411
|
}
|
|
309
412
|
// Modern TS declares most of its functions as bindings, not as
|
|
@@ -313,17 +416,58 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
313
416
|
// 170-vs-13,428 TS/PHP symbol gap: the six handled kinds simply do
|
|
314
417
|
// not cover the dominant declaration form.
|
|
315
418
|
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
419
|
+
// The scoping note that stood here said a `const x = 3` is "data,
|
|
420
|
+
// not a symbol this graph answers questions about", and that
|
|
421
|
+
// emitting a node per constant would raise the count without
|
|
422
|
+
// improving recall. **The v2 benchmark falsified the first half and
|
|
423
|
+
// the second half no longer follows.** Two of its three
|
|
424
|
+
// `references` questions probe exactly this shape — `EXT_LANG` (a
|
|
425
|
+
// const) and `SettingsClass` (a type alias) — and the class scored
|
|
426
|
+
// recall 0.333 against grep's 1.000 because neither had a node to
|
|
427
|
+
// resolve to. The count-inflation worry was real and is answered by
|
|
428
|
+
// SCOPE rather than by refusal: a binding earns a node only at
|
|
429
|
+
// MODULE level (`topLevel`), so a loop counter inside a function
|
|
430
|
+
// body still gets none. The reason is kept rather than deleted,
|
|
431
|
+
// because a reversed decision whose reason disappears gets re-taken.
|
|
321
432
|
case 'lexical_declaration':
|
|
322
433
|
case 'variable_declaration': {
|
|
323
434
|
for (const d of namedChildren(n)) {
|
|
324
435
|
if (d.type !== 'variable_declarator') continue;
|
|
325
436
|
const value = d.childForFieldName('value');
|
|
326
|
-
|
|
437
|
+
const nmNode = d.childForFieldName('name');
|
|
438
|
+
const isFn =
|
|
439
|
+
value !== null &&
|
|
440
|
+
value !== undefined &&
|
|
441
|
+
(value.type === 'arrow_function' || value.type === 'function_expression');
|
|
442
|
+
const isClassExpr = value?.type === 'class';
|
|
443
|
+
if (!isFn) {
|
|
444
|
+
// Only a plain `identifier` name: a destructuring
|
|
445
|
+
// pattern (`const { a, b } = x`) binds several names and
|
|
446
|
+
// none of them is a declaration this graph can id.
|
|
447
|
+
if (topLevel && nmNode?.type === 'identifier') {
|
|
448
|
+
const nm = nmNode.text;
|
|
449
|
+
const id = classId ? `${classId}::${nm}` : `${file}#${nm}`;
|
|
450
|
+
out.nodes.push({
|
|
451
|
+
id,
|
|
452
|
+
label: nm,
|
|
453
|
+
// `const Widget = class {}` IS constructible;
|
|
454
|
+
// calling it a constant would let the build pass
|
|
455
|
+
// reject a `new Widget()` that is perfectly real.
|
|
456
|
+
kind: isClassExpr ? 'class' : 'constant',
|
|
457
|
+
source_file: file,
|
|
458
|
+
source_location: loc(d),
|
|
459
|
+
});
|
|
460
|
+
out.rawEdges.push({
|
|
461
|
+
sourceId: classId ?? fileId,
|
|
462
|
+
relation: 'member',
|
|
463
|
+
targetName: nm,
|
|
464
|
+
confidenceHint: 'EXTRACTED',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
// Non-function declarators still need walking for their
|
|
468
|
+
// calls — returning early on the whole declaration
|
|
469
|
+
// silently drops call edges from initialisers.
|
|
470
|
+
walk(d, classId, scopeId, inner);
|
|
327
471
|
continue;
|
|
328
472
|
}
|
|
329
473
|
const nm = nameField(d) ?? '<anon>';
|
|
@@ -353,16 +497,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
353
497
|
// can iterate children because a `statement_block` is never
|
|
354
498
|
// the interesting node; here it often is.
|
|
355
499
|
const body = value.childForFieldName('body');
|
|
356
|
-
if (body) walk(body, classId, id);
|
|
357
|
-
}
|
|
358
|
-
// Non-function declarators still need walking for their calls.
|
|
359
|
-
for (const d of namedChildren(n)) {
|
|
360
|
-
if (d.type !== 'variable_declarator') continue;
|
|
361
|
-
const value = d.childForFieldName('value');
|
|
362
|
-
if (value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
walk(d, classId, scopeId);
|
|
500
|
+
if (body) walk(body, classId, id, false);
|
|
366
501
|
}
|
|
367
502
|
return;
|
|
368
503
|
}
|
|
@@ -388,7 +523,7 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
388
523
|
confidenceHint: 'EXTRACTED',
|
|
389
524
|
});
|
|
390
525
|
const body = value.childForFieldName('body');
|
|
391
|
-
if (body) walk(body, classId, id);
|
|
526
|
+
if (body) walk(body, classId, id, false);
|
|
392
527
|
return;
|
|
393
528
|
}
|
|
394
529
|
break;
|
|
@@ -481,11 +616,11 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
481
616
|
default:
|
|
482
617
|
break;
|
|
483
618
|
}
|
|
484
|
-
for (const c of namedChildren(n)) walk(c, classId, scopeId);
|
|
619
|
+
for (const c of namedChildren(n)) walk(c, classId, scopeId, inner);
|
|
485
620
|
};
|
|
486
621
|
|
|
487
622
|
out.nodes.push({ id: fileId, label: file, kind: 'file', source_file: file, source_location: [] });
|
|
488
|
-
walk(root, null, fileId);
|
|
623
|
+
walk(root, null, fileId, true);
|
|
489
624
|
}
|
|
490
625
|
|
|
491
626
|
/**
|