@event4u/agent-config 17.1.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.
Files changed (73) 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 +70 -0
  5. package/README.md +2 -2
  6. package/dist/agent-src/commands/project/analyze.md +9 -6
  7. package/dist/agent-src/commands/project/map.md +58 -0
  8. package/dist/agent-src/commands/project.md +5 -3
  9. package/dist/agent-src/skills/code-intelligence/SKILL.md +6 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/cli/registry.js +1 -1
  12. package/dist/cli-delegate/cmd_conformance.js +23 -39
  13. package/dist/discovery/deprecation-report.md +1 -1
  14. package/dist/discovery/discovery-manifest.json +39 -13
  15. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  16. package/dist/discovery/discovery-manifest.summary.md +3 -3
  17. package/dist/discovery/dormancy-report.md +1 -1
  18. package/dist/discovery/orphan-report.md +1 -1
  19. package/dist/discovery/packs.json +6 -5
  20. package/dist/discovery/trust-report.md +2 -2
  21. package/dist/discovery/workspaces.json +5 -4
  22. package/dist/hooks/dispatch.js +65 -74
  23. package/dist/hooks/dispatch.sha256 +1 -1
  24. package/dist/hooks/instructions_observer.chunk.cjs +301 -0
  25. package/dist/hooks/instructions_observer.chunk.cjs.sha256 +1 -0
  26. package/dist/mcp/registry-manifest.json +2 -2
  27. package/dist/mcp/server.json +2 -2
  28. package/dist/mcp/server.mjs +33058 -22930
  29. package/package.json +4 -1
  30. package/src/config/continuity-surface.json +7 -0
  31. package/src/config/evaluator-budgets.json +4 -4
  32. package/src/config/gate-violation-baselines.json +3 -3
  33. package/src/config/gitignore-block.txt +6 -0
  34. package/src/config/hook-bundle-budget.json +17 -2
  35. package/src/config/hook-token-budget.json +1 -1
  36. package/src/config/packed-binary-manifest.json +27 -0
  37. package/src/config/skill-body-budget.json +7 -1
  38. package/src/scripts/_lib/bench_ab_complexity.ts +14 -2
  39. package/src/scripts/_lib/collector_record.ts +1 -0
  40. package/src/scripts/_lib/runtime_journal.ts +7 -7
  41. package/src/scripts/code_graph/build.ts +210 -10
  42. package/src/scripts/code_graph/cli.ts +168 -46
  43. package/src/scripts/code_graph/detect.ts +5 -4
  44. package/src/scripts/code_graph/docs_layer.ts +57 -0
  45. package/src/scripts/code_graph/extract.ts +58 -13
  46. package/src/scripts/code_graph/extract_more.ts +314 -0
  47. package/src/scripts/code_graph/laravel_overlay.ts +454 -0
  48. package/src/scripts/code_graph/map.ts +846 -0
  49. package/src/scripts/code_graph/map_model.ts +977 -0
  50. package/src/scripts/code_graph/map_verify.ts +140 -0
  51. package/src/scripts/code_graph/map_viewer/viewer.css +278 -0
  52. package/src/scripts/code_graph/map_viewer/viewer.js +756 -0
  53. package/src/scripts/code_graph/scip_reader.ts +201 -0
  54. package/src/scripts/code_graph/sqlite_store.ts +8 -1
  55. package/src/scripts/code_graph/types.ts +81 -15
  56. package/src/scripts/code_graph/validate.ts +16 -26
  57. package/src/scripts/code_graph/verbs.ts +7 -0
  58. package/src/scripts/hook_chunks.mjs +7 -0
  59. package/src/scripts/hook_manifest.json +1 -1
  60. package/src/scripts/hook_manifest.yaml +20 -0
  61. package/src/scripts/hooks/dispatch_hook.ts +2 -2
  62. package/src/scripts/hooks/instructions_observer_hook.ts +25 -6
  63. package/src/scripts/hooks/lazy_concern.ts +4 -0
  64. package/src/scripts/install-hooks.sh +5 -0
  65. package/src/scripts/lint_agents_layout.ts +2 -0
  66. package/src/scripts/lint_hook_manifest.ts +1 -0
  67. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -6
  68. package/src/scripts/mcp_server/graph_tools.ts +73 -1
  69. package/src/scripts/mcp_server/tools.ts +19 -36
  70. package/src/vendor/grammars/README.md +10 -5
  71. package/src/vendor/grammars/tree-sitter-go.wasm +0 -0
  72. package/src/vendor/grammars/tree-sitter-java.wasm +0 -0
  73. package/src/vendor/grammars/tree-sitter-python.wasm +0 -0
@@ -13,9 +13,12 @@
13
13
  import { createHash } from 'node:crypto';
14
14
  import * as fs from 'node:fs';
15
15
  import * as path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
16
17
 
17
18
  import { write_atomic } from '../_lib/fs_atomic.js';
18
19
  import { extractFile, type FileExtract, type RawEdge } from './extract.js';
20
+ import { buildLaravelOverlay } from './laravel_overlay.js';
21
+ import { grammarWasmPath } from './loader.js';
19
22
  import {
20
23
  ALIAS_VIA,
21
24
  aliasRulesFrom,
@@ -27,6 +30,8 @@ import {
27
30
  resolveAlias,
28
31
  resolvePsr4,
29
32
  } from './resolution_tiers.js';
33
+ import { DOC_EXTS, docsLayer } from './docs_layer.js';
34
+ import { readScipIndex, type ScipDocument, scipEdges, supersededByScip } from './scip_reader.js';
30
35
  import { emitSqliteTwin } from './sqlite_store.js';
31
36
  import {
32
37
  type CodeEdge,
@@ -34,6 +39,7 @@ import {
34
39
  type EdgeConfidence,
35
40
  EXPECTED_GRAMMAR_ABI,
36
41
  EXT_LANG,
42
+ GRAMMAR_WASM,
37
43
  isStatedResolution,
38
44
  type Lang,
39
45
  type ResolvedVia,
@@ -51,6 +57,10 @@ const IGNORE_DIRS = new Set([
51
57
  'coverage',
52
58
  '.cache',
53
59
  'storage',
60
+ '__pycache__',
61
+ 'venv',
62
+ 'site-packages',
63
+ 'target',
54
64
  ]);
55
65
 
56
66
  export interface SourceFile {
@@ -62,7 +72,12 @@ export interface SourceFile {
62
72
  // ── deterministic file discovery (path-confined) ─────────────────────────────
63
73
 
64
74
  /** Recursively list launch-set source files under `root`, path-confined. */
65
- export function discoverFiles(root: string): string[] {
75
+ const CODE_EXTS: ReadonlySet<string> = new Set(Object.keys(EXT_LANG));
76
+
77
+ /** Generated output under the project root: the engine never reads its own artefacts back. */
78
+ const GENERATED_DIRS: ReadonlySet<string> = new Set(['agents/map', 'agents/runtime']);
79
+
80
+ export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXTS): string[] {
66
81
  const abs = path.resolve(root);
67
82
  // Resolve the root itself first: a repo checked out under a symlinked
68
83
  // path (e.g. macOS /tmp → /private/tmp) must still confine correctly —
@@ -85,12 +100,14 @@ export function discoverFiles(root: string): string[] {
85
100
  const full = path.join(dir, e.name);
86
101
  if (e.isSymbolicLink()) continue; // never follow symlinks
87
102
  if (e.isDirectory()) {
88
- if (!IGNORE_DIRS.has(e.name) && !e.name.startsWith('.')) walk(full);
103
+ if (IGNORE_DIRS.has(e.name) || e.name.startsWith('.')) continue;
104
+ if (GENERATED_DIRS.has(path.relative(abs, full).split(path.sep).join('/'))) continue;
105
+ walk(full);
89
106
  continue;
90
107
  }
91
108
  if (!e.isFile()) continue;
92
109
  const ext = path.extname(e.name).toLowerCase();
93
- if (!(ext in EXT_LANG)) continue;
110
+ if (!exts.has(ext)) continue;
94
111
  let real: string;
95
112
  try {
96
113
  real = fs.realpathSync(full);
@@ -207,6 +224,49 @@ function resolveSpecifier(fromFile: string, spec: string, fileIds: ReadonlySet<s
207
224
  return null;
208
225
  }
209
226
 
227
+ /**
228
+ * The file(s) a Python, Java or Go import names, or `[]` when the tree does not
229
+ * hold it (a standard-library or third-party module).
230
+ *
231
+ * python `pkg.mod` → `pkg/mod.py` or `pkg/mod/__init__.py` (any source root,
232
+ * unique suffix match); `.mod` / `..mod` relative to the importer.
233
+ * java `a.b.C` → `a/b/C.java` (unique suffix match).
234
+ * go `example.com/x/mod` → every non-test `.go` file of the most specific
235
+ * directory whose path ends the import path — a Go import names a
236
+ * package, and a package is all of its files.
237
+ *
238
+ * A suffix that matches two files is refused rather than guessed.
239
+ */
240
+ export function resolveLangModule(lang: Lang, fromFile: string, spec: string, fileIds: readonly string[]): string[] {
241
+ const unique = (suffixes: string[]): string[] => {
242
+ const hits = fileIds.filter((f) => suffixes.some((s) => f === s || f.endsWith(`/${s}`)));
243
+ return hits.length === 1 ? hits : [];
244
+ };
245
+ if (lang === 'python') {
246
+ const rel = /^(\.+)(.*)$/.exec(spec);
247
+ if (rel) {
248
+ let dir = idDirname(fromFile);
249
+ for (let i = 1; i < (rel[1] as string).length; i++) dir = idDirname(dir);
250
+ const sub = (rel[2] as string).split('.').filter(Boolean).join('/');
251
+ const base = sub ? (dir && dir !== '.' ? `${dir}/${sub}` : sub) : dir;
252
+ return [`${base}.py`, `${base}/__init__.py`].filter((c) => fileIds.includes(c)).slice(0, 1);
253
+ }
254
+ const p = spec.split('.').join('/');
255
+ return unique([`${p}.py`, `${p}/__init__.py`]);
256
+ }
257
+ if (lang === 'java') return unique([`${spec.split('.').join('/')}.java`]);
258
+ if (lang === 'go') {
259
+ const goFiles = fileIds.filter((f) => f.endsWith('.go') && !f.endsWith('_test.go'));
260
+ let best = '';
261
+ for (const f of goFiles) {
262
+ const d = idDirname(f);
263
+ if ((spec === d || spec.endsWith(`/${d}`)) && d.length > best.length) best = d;
264
+ }
265
+ return best ? goFiles.filter((f) => idDirname(f) === best).sort() : [];
266
+ }
267
+ return [];
268
+ }
269
+
210
270
  function push(map: Map<string, string[]>, k: string, v: string): void {
211
271
  const arr = map.get(k);
212
272
  if (arr) {
@@ -247,6 +307,8 @@ export function buildGraph(
247
307
  files: readonly SourceFile[],
248
308
  extracts: readonly FileExtract[],
249
309
  resolution: ResolutionConfig = EMPTY_RESOLUTION_CONFIG,
310
+ scip: readonly ScipDocument[] = [],
311
+ docs: readonly SourceFile[] = [],
250
312
  ): CodeGraph {
251
313
  const nodes = extracts.flatMap((e) => e.nodes);
252
314
  const nodeIds = new Set(nodes.map((n) => n.id));
@@ -318,6 +380,9 @@ export function buildGraph(
318
380
  const nodeKind = new Map(nodes.map((n) => [n.id, n.kind as string]));
319
381
  const fileIds = new Set(nodes.filter((n) => n.kind === 'file').map((n) => n.id));
320
382
  const scopes = new Map<string, FileScope>();
383
+ const fileIdList = [...fileIds].sort();
384
+ /** Go imports name a package of several files; the binding takes the first, these carry the rest. */
385
+ const extraImports: Array<{ source: string; target: string }> = [];
321
386
  for (const ex of extracts) {
322
387
  const locals = new Map<string, string[]>();
323
388
  for (const n of ex.nodes) if (n.kind !== 'file') push(locals, n.label, n.id);
@@ -354,6 +419,26 @@ export function buildGraph(
354
419
  if (hit) imports.set(local, { target: hit, exact: false, via: 'name-lookup' });
355
420
  continue;
356
421
  }
422
+ if (ex.lang === 'python' || ex.lang === 'java' || ex.lang === 'go') {
423
+ const hits = resolveLangModule(ex.lang, ex.file, spec, fileIdList);
424
+ const first = hits[0];
425
+ if (first === undefined) {
426
+ imports.set(local, {
427
+ target: r.importKind === 'named' ? `external:${spec}#${r.targetName}` : `external:${spec}`,
428
+ exact: true,
429
+ via: 'import-specifier',
430
+ });
431
+ continue;
432
+ }
433
+ const member = `${first}#${r.targetName}`;
434
+ imports.set(local, {
435
+ target: r.importKind === 'named' && nodeKind.has(member) ? member : first,
436
+ exact: true,
437
+ via: 'import-specifier',
438
+ });
439
+ for (const extra of hits.slice(1)) extraImports.push({ source: ex.file, target: extra });
440
+ continue;
441
+ }
357
442
  // TIER 0: a relative specifier — the file says where it points.
358
443
  // TIER 1 (2.3): tsconfig `paths`. `@shared/x` IS `src/shared/x`
359
444
  // because the project declares it, so this is a fact and not a
@@ -470,6 +555,16 @@ export function buildGraph(
470
555
  }
471
556
  for (const r of ex.rawEdges) resolveRawEdge(r, ex);
472
557
  }
558
+ for (const x of extraImports) {
559
+ emit({
560
+ source: x.source,
561
+ target: x.target,
562
+ relation: 'imports',
563
+ confidence: 'EXTRACTED',
564
+ resolved_via: 'import-specifier',
565
+ provider: 'native',
566
+ });
567
+ }
473
568
 
474
569
  function resolveRawEdge(r: RawEdge, ex: FileExtract): void {
475
570
  if (r.relation === 'member') {
@@ -659,6 +754,40 @@ export function buildGraph(
659
754
  });
660
755
  }
661
756
 
757
+ // Framework links no import states (routes, views, tables); rule-read,
758
+ // emitted only when the target exists. Also drops the base edges it
759
+ // proves false: a fluent `->middleware()` bound by name to some
760
+ // unrelated in-repo `middleware()`.
761
+ const overlay = buildLaravelOverlay(files, nodes, resolution);
762
+ let routeBuilderCalls = 0;
763
+ if (overlay.nodes.length > 0 || overlay.edges.length > 0) {
764
+ const kept = edges.filter((e) => !overlay.dropEdge(e));
765
+ routeBuilderCalls = edges.length - kept.length;
766
+ // Element-wise: a spread of ~200k edges overflows the call stack.
767
+ edges.length = 0;
768
+ for (const e of kept) edges.push(e);
769
+ for (const e of overlay.edges) edges.push(e);
770
+ for (const n of overlay.nodes) nodes.push(n);
771
+ }
772
+
773
+ if (docs.length > 0) {
774
+ const allFiles = new Set([...files.map((f) => f.path), ...docs.map((d) => d.path)]);
775
+ const layer = docsLayer(docs, allFiles);
776
+ for (const n of layer.nodes) nodes.push(n);
777
+ for (const e of layer.edges) edges.push(e);
778
+ }
779
+
780
+ // A project's own SCIP index states references the compiler resolved;
781
+ // each one supersedes the native name guess it duplicates.
782
+ if (scip.length > 0) {
783
+ const stated = scipEdges(scip, nodes);
784
+ const dropped = supersededByScip(stated, nodes);
785
+ const kept = edges.filter((e) => !dropped(e));
786
+ edges.length = 0;
787
+ for (const e of kept) edges.push(e);
788
+ for (const e of stated) edges.push(e);
789
+ }
790
+
662
791
  // deterministic ordering
663
792
  nodes.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
664
793
  edges.sort((a, b) => {
@@ -669,7 +798,10 @@ export function buildGraph(
669
798
 
670
799
  const counts = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
671
800
  for (const e of edges) counts[e.confidence] += 1;
672
- const suppressed = { dynamic_no_candidate: suppressedKeys.size };
801
+ const suppressed: CodeGraph['suppressed_edge_counts'] = {
802
+ dynamic_no_candidate: suppressedKeys.size,
803
+ ...(routeBuilderCalls > 0 ? { route_builder_call: routeBuilderCalls } : {}),
804
+ };
673
805
 
674
806
  const languages = [...new Set(extracts.map((e) => e.lang))].sort() as Lang[];
675
807
 
@@ -682,6 +814,18 @@ export function buildGraph(
682
814
  h.update('\n');
683
815
  }
684
816
 
817
+ for (const d of [...docs].sort((a, b) => (a.path < b.path ? -1 : 1))) {
818
+ h.update(`doc\0${d.path}\0`);
819
+ h.update(createHash('sha256').update(d.source, 'utf-8').digest('hex'));
820
+ h.update('\n');
821
+ }
822
+ // The SCIP index is an input like any source file: a new index must not
823
+ // read as the same graph.
824
+ for (const d of [...scip].sort((a, b) => (a.path < b.path ? -1 : 1))) {
825
+ h.update(`scip\0${d.path}\0`);
826
+ for (const o of d.occurrences) h.update(`${o.line}:${o.definition ? 1 : 0}:${o.symbol}\n`);
827
+ }
828
+
685
829
  return {
686
830
  schema_version: SCHEMA_VERSION,
687
831
  source_checksum: h.digest('hex'),
@@ -820,19 +964,57 @@ function contentHash(s: string): string {
820
964
  * source of truth: a full build reproduces the identical graph. */
821
965
  interface ExtractSidecar {
822
966
  version: number;
967
+ /** `extractorFingerprint()` at write time — see there. */
968
+ fingerprint?: string;
823
969
  files: Record<string, { hash: string; extract: FileExtract }>;
824
970
  }
825
971
 
972
+ const _BUILD_DIR = path.dirname(fileURLToPath(import.meta.url));
973
+ let _fingerprint: string | null = null;
974
+
975
+ /**
976
+ * What a cached per-file extract was produced BY: schema × package version ×
977
+ * extractor source × every grammar's bytes. Keyed on the schema alone, an
978
+ * upgrade that changed how a file is extracted kept serving the old facts on
979
+ * `--update` for every file whose content had not moved. Hashing the extractor
980
+ * source rather than trusting a hand-bumped constant means a change nobody
981
+ * remembered to version still invalidates the cache.
982
+ */
983
+ export function extractorFingerprint(): string {
984
+ if (_fingerprint !== null) return _fingerprint;
985
+ const h = createHash('sha256');
986
+ h.update(`schema:${SCHEMA_VERSION}\n`);
987
+ try {
988
+ const pkg = JSON.parse(fs.readFileSync(path.join(_BUILD_DIR, '..', '..', '..', 'package.json'), 'utf-8')) as {
989
+ version?: string;
990
+ };
991
+ h.update(`package:${pkg.version ?? '?'}\n`);
992
+ } catch {
993
+ h.update('package:?\n');
994
+ }
995
+ for (const f of ['extract.ts', 'extract.js']) {
996
+ const p = path.join(_BUILD_DIR, f);
997
+ if (fs.existsSync(p)) h.update(fs.readFileSync(p));
998
+ }
999
+ for (const lang of Object.keys(GRAMMAR_WASM).sort() as Lang[]) {
1000
+ const p = grammarWasmPath(lang);
1001
+ h.update(`grammar:${lang}\n`);
1002
+ if (fs.existsSync(p)) h.update(fs.readFileSync(p));
1003
+ }
1004
+ _fingerprint = h.digest('hex').slice(0, 16);
1005
+ return _fingerprint;
1006
+ }
1007
+
826
1008
  /** Exported so `refresh` (cli.ts) can locate/seed/promote the sidecar next to
827
1009
  * a temp `--out` path without duplicating the naming convention. */
828
1010
  export function sidecarPath(cachePath: string): string {
829
1011
  return cachePath.replace(/\.json$/, '.extracts.json');
830
1012
  }
831
1013
 
832
- function readSidecar(p: string): ExtractSidecar | null {
1014
+ function readSidecar(p: string, fingerprint: string): ExtractSidecar | null {
833
1015
  try {
834
1016
  const s = JSON.parse(fs.readFileSync(p, 'utf-8')) as ExtractSidecar;
835
- return s.version === SCHEMA_VERSION ? s : null;
1017
+ return s.version === SCHEMA_VERSION && s.fingerprint === fingerprint ? s : null;
836
1018
  } catch {
837
1019
  return null;
838
1020
  }
@@ -848,7 +1030,7 @@ function readSidecar(p: string): ExtractSidecar | null {
848
1030
  export async function buildFromRepo(
849
1031
  root: string,
850
1032
  cachePath: string | null,
851
- opts: { update?: boolean } = {},
1033
+ opts: { update?: boolean; fingerprint?: string } = {},
852
1034
  ): Promise<BuildResult> {
853
1035
  const abs = path.resolve(root);
854
1036
  const fullPaths = discoverFiles(abs);
@@ -864,9 +1046,10 @@ export async function buildFromRepo(
864
1046
  files.push({ path: rel, source });
865
1047
  }
866
1048
 
867
- const prior = opts.update && cachePath ? readSidecar(sidecarPath(cachePath)) : null;
1049
+ const fingerprint = opts.fingerprint ?? extractorFingerprint();
1050
+ const prior = opts.update && cachePath ? readSidecar(sidecarPath(cachePath), fingerprint) : null;
868
1051
  const priorFiles = prior?.files ?? {};
869
- const nextSidecar: ExtractSidecar = { version: SCHEMA_VERSION, files: {} };
1052
+ const nextSidecar: ExtractSidecar = { version: SCHEMA_VERSION, fingerprint, files: {} };
870
1053
  const extracts: FileExtract[] = [];
871
1054
  let reExtracted = 0;
872
1055
  let reused = 0;
@@ -886,7 +1069,24 @@ export async function buildFromRepo(
886
1069
  nextSidecar.files[f.path] = { hash, extract };
887
1070
  }
888
1071
 
889
- const graph = buildGraph(files, extracts, readResolutionConfig(root));
1072
+ const scipPath = path.join(abs, 'index.scip');
1073
+ let scip: ScipDocument[] = [];
1074
+ if (fs.existsSync(scipPath)) {
1075
+ try {
1076
+ scip = readScipIndex(fs.readFileSync(scipPath));
1077
+ } catch (e) {
1078
+ process.stderr.write(`code-graph: index.scip not read — ${(e as Error).message}\n`);
1079
+ }
1080
+ }
1081
+ const docs: SourceFile[] = [];
1082
+ for (const full of discoverFiles(abs, DOC_EXTS)) {
1083
+ try {
1084
+ docs.push({ path: toPosixRel(abs, full), source: fs.readFileSync(full, 'utf-8') });
1085
+ } catch {
1086
+ continue;
1087
+ }
1088
+ }
1089
+ const graph = buildGraph(files, extracts, readResolutionConfig(root), scip, docs);
890
1090
  const json = serializeGraph(graph);
891
1091
  if (cachePath) {
892
1092
  write_atomic(cachePath, json);