@event4u/agent-config 9.6.0 → 9.8.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 (204) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +207 -942
  3. package/README.md +21 -21
  4. package/dist/agent-src/README.md +2 -2
  5. package/dist/agent-src/commands/commit/in-chunks.md +8 -0
  6. package/dist/agent-src/commands/commit.md +17 -0
  7. package/dist/agent-src/commands/pr/create.md +4 -0
  8. package/dist/agent-src/contexts/override-system.md +75 -5
  9. package/dist/agent-src/rules/context-hygiene.md +2 -0
  10. package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
  11. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  12. package/dist/agent-src/rules/downstream-changes.md +31 -1
  13. package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
  14. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  15. package/dist/agent-src/rules/git-history-discipline.md +2 -0
  16. package/dist/agent-src/rules/language-and-tone.md +2 -0
  17. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  18. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  19. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +3 -0
  21. package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
  22. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  23. package/dist/agent-src/rules/output-discipline.md +2 -0
  24. package/dist/agent-src/rules/persona-governance.md +2 -0
  25. package/dist/agent-src/rules/preservation-guard.md +3 -0
  26. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  27. package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
  28. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  29. package/dist/agent-src/rules/skill-quality.md +2 -0
  30. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  31. package/dist/agent-src/rules/source-of-truth.md +2 -0
  32. package/dist/agent-src/rules/telegraph-speak.md +2 -0
  33. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  34. package/dist/agent-src/rules/tool-safety.md +1 -0
  35. package/dist/agent-src/rules/verify-before-complete.md +2 -0
  36. package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
  37. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +34 -16
  38. package/dist/agent-src/skills/agents-md-thin-root/SKILL.md +9 -9
  39. package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
  40. package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
  41. package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
  42. package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
  43. package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
  44. package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
  45. package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
  46. package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
  47. package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
  48. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
  49. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
  50. package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
  51. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
  52. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
  53. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
  54. package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
  55. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
  56. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  57. package/dist/agent-src/templates/github-workflows/doc-impact.yml +101 -0
  58. package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
  59. package/dist/cli/configRoot.js +112 -0
  60. package/dist/cli/configRoot.js.map +1 -0
  61. package/dist/cli/configRoot.test.js +97 -0
  62. package/dist/cli/configRoot.test.js.map +1 -0
  63. package/dist/cli/help.js +4 -1
  64. package/dist/cli/help.js.map +1 -1
  65. package/dist/cli/main.js +19 -2
  66. package/dist/cli/main.js.map +1 -1
  67. package/dist/cli/registry.js +1 -0
  68. package/dist/cli/registry.js.map +1 -1
  69. package/dist/discovery/deprecation-report.md +1 -1
  70. package/dist/discovery/discovery-manifest.json +164 -47
  71. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  72. package/dist/discovery/discovery-manifest.summary.md +4 -4
  73. package/dist/discovery/orphan-report.md +1 -1
  74. package/dist/discovery/packs.json +13 -8
  75. package/dist/discovery/trust-report.md +11 -11
  76. package/dist/discovery/workspaces.json +11 -6
  77. package/dist/install/install.mjs +16 -0
  78. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  79. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  80. package/dist/mcp/registry-manifest.json +4 -4
  81. package/dist/router.json +1 -1
  82. package/dist/server/app.js +10 -0
  83. package/dist/server/app.js.map +1 -1
  84. package/dist/server/routes/ping.js +18 -0
  85. package/dist/server/routes/ping.js.map +1 -1
  86. package/dist/server/schemas/settings.js +8 -0
  87. package/dist/server/schemas/settings.js.map +1 -1
  88. package/dist/server/serverInfo.js +6 -2
  89. package/dist/server/serverInfo.js.map +1 -1
  90. package/dist/server/token.js +11 -4
  91. package/dist/server/token.js.map +1 -1
  92. package/dist/server/writeRoot.js +15 -1
  93. package/dist/server/writeRoot.js.map +1 -1
  94. package/dist/shared/capabilities.js +33 -0
  95. package/dist/shared/capabilities.js.map +1 -0
  96. package/dist/shared/capabilities.test.js +39 -0
  97. package/dist/shared/capabilities.test.js.map +1 -0
  98. package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
  99. package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
  100. package/dist/ui/index.html +14 -5
  101. package/docs/CLAIMS.md +64 -38
  102. package/docs/architecture.md +2 -2
  103. package/docs/archive/CHANGELOG-6.0.0-overview.md +953 -0
  104. package/docs/benchmark.md +170 -9
  105. package/docs/catalog.md +8 -3
  106. package/docs/comparison.yaml +44 -2
  107. package/docs/contracts/local-server-ports.md +53 -0
  108. package/docs/contracts/no-runtime-boundary.md +1 -1
  109. package/docs/contracts/subagent-boundary.md +15 -0
  110. package/docs/customization.md +14 -3
  111. package/docs/decisions/ADR-016-installer-architecture.md +2 -0
  112. package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
  113. package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
  114. package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
  115. package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
  116. package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
  117. package/docs/decisions/ADR-122-adversarial-verification-council.md +30 -0
  118. package/docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md +31 -0
  119. package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
  120. package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
  121. package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
  122. package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
  123. package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
  124. package/docs/decisions/INDEX.md +21 -0
  125. package/docs/decisions/engine-reclassification-2026-07.md +104 -0
  126. package/docs/distribution/awesome-list-submission.md +1 -1
  127. package/docs/distribution/registries.md +36 -27
  128. package/docs/enforcement-by-host.md +7 -0
  129. package/docs/featured-skills.md +2 -2
  130. package/docs/getting-started-by-role.md +1 -1
  131. package/docs/getting-started.md +1 -1
  132. package/docs/governance-advantage.md +1 -1
  133. package/docs/guides/gated-platform-reads.md +221 -0
  134. package/docs/positioning-evidence.md +1 -1
  135. package/docs/proof.md +92 -16
  136. package/docs/spawn-site-policy.md +72 -0
  137. package/docs/threat-model.md +2 -1
  138. package/package.json +6 -3
  139. package/src/agent-src/README.md +2 -2
  140. package/src/config/agent-settings.template.yml +43 -0
  141. package/src/config/reach-channels.yml +217 -0
  142. package/src/config/reach-prescriptions-intake.yml +200 -0
  143. package/src/scripts/_dispatch.bash +15 -0
  144. package/src/scripts/_lib/adversarial_bench_score.ts +106 -0
  145. package/src/scripts/_lib/exec_evidence.ts +251 -0
  146. package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
  147. package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
  148. package/src/scripts/_lib/kernel_rules.ts +46 -0
  149. package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
  150. package/src/scripts/_lib/risk_paths.ts +160 -0
  151. package/src/scripts/_lib/secret_detector.test.ts +103 -0
  152. package/src/scripts/_lib/secret_detector.ts +319 -0
  153. package/src/scripts/_lib/spawn_env.ts +24 -4
  154. package/src/scripts/_lib/tool_probe.ts +359 -0
  155. package/src/scripts/adr/regenerate_index.ts +81 -6
  156. package/src/scripts/audit_adr_coverage.ts +33 -0
  157. package/src/scripts/bench_adversarial_council.ts +292 -0
  158. package/src/scripts/build_proof.ts +162 -0
  159. package/src/scripts/check_adr_frontmatter.ts +195 -0
  160. package/src/scripts/check_artefact_count_messaging.ts +4 -0
  161. package/src/scripts/check_backstop_debt.ts +200 -0
  162. package/src/scripts/check_claims.ts +188 -8
  163. package/src/scripts/check_comparison.ts +12 -0
  164. package/src/scripts/check_enforcement_coverage.ts +592 -0
  165. package/src/scripts/check_kernel_rule_bundle.ts +16 -12
  166. package/src/scripts/check_no_new_legacy_path.ts +2 -0
  167. package/src/scripts/check_reach_channels.ts +407 -0
  168. package/src/scripts/check_reach_staleness.ts +364 -0
  169. package/src/scripts/check_secret_leak.test.ts +47 -0
  170. package/src/scripts/check_secret_leak.ts +290 -0
  171. package/src/scripts/check_source_pointer_freshness.ts +165 -0
  172. package/src/scripts/code_graph/build.ts +0 -0
  173. package/src/scripts/code_graph/cli.ts +207 -0
  174. package/src/scripts/code_graph/detect.ts +134 -0
  175. package/src/scripts/code_graph/extract.ts +386 -0
  176. package/src/scripts/code_graph/loader.ts +123 -0
  177. package/src/scripts/code_graph/query.ts +176 -0
  178. package/src/scripts/code_graph/sanitize.ts +30 -0
  179. package/src/scripts/code_graph/types.ts +83 -0
  180. package/src/scripts/code_graph/validate.ts +63 -0
  181. package/src/scripts/external_sources_denylist.json +3 -1
  182. package/src/scripts/generate_subagent_floor.ts +194 -0
  183. package/src/scripts/hook_manifest.yaml +13 -3
  184. package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
  185. package/src/scripts/hooks/dispatch_hook.ts +5 -1
  186. package/src/scripts/iron_law_sha.ts +3 -12
  187. package/src/scripts/lint_override_kernel_guard.ts +219 -0
  188. package/src/scripts/lint_readme_serial_comma.ts +169 -0
  189. package/src/scripts/measure_rule_budget.ts +3 -12
  190. package/src/scripts/postinstall_gui.ts +129 -0
  191. package/src/scripts/reach_doctor.ts +1651 -0
  192. package/src/scripts/release.ts +24 -3
  193. package/src/scripts/risk_escalation_shadow.ts +160 -0
  194. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  195. package/src/scripts/schemas/reach-channels.schema.json +134 -0
  196. package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
  197. package/src/scripts/schemas/rule.schema.json +10 -0
  198. package/src/scripts/skill_linter.ts +55 -0
  199. package/src/scripts/validate_reach_prescriptions.ts +751 -0
  200. package/src/server/schemas/settings.ts +16 -0
  201. package/src/shared/capabilities.test.ts +43 -0
  202. package/src/shared/capabilities.ts +75 -0
  203. package/src/templates/minimal/agents-overrides-readme.md +20 -0
  204. package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Source detection + freshness. Answers "what code-graph source should a
3
+ * query use, and is it fresh?" across three sources:
4
+ *
5
+ * consumer — a `graph.json`-shaped artifact the consumer repo ships
6
+ * scip — an `index.scip` / `*.scip` (presence only; peer tooling
7
+ * required — an owned SCIP reader is YAGNI-gated until a
8
+ * consumer actually ships one)
9
+ * native — the suite's own gitignored cache
10
+ *
11
+ * Precedence (ADR-124 § 2): a fresh consumer index wins (interop courtesy);
12
+ * the native engine covers stale-or-absent. Freshness: `head_at_build` when
13
+ * the artifact embeds a SHA (→ commits_behind), else artifact mtime vs the
14
+ * repo's last commit time. No guessing.
15
+ */
16
+ import { execFileSync } from 'node:child_process';
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+
20
+ import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
21
+ import { validateGraph } from './validate.js';
22
+
23
+ export type SourceKind = 'consumer' | 'scip' | 'native';
24
+
25
+ export interface SourceVerdict {
26
+ kind: SourceKind;
27
+ path: string;
28
+ present: boolean;
29
+ stale?: boolean;
30
+ commits_behind?: number;
31
+ note?: string;
32
+ }
33
+
34
+ const CONSUMER_CANDIDATES = ['graph.json', 'code-graph.json', '.code-graph/graph.json'];
35
+
36
+ function gitLastCommitEpoch(root: string): number | null {
37
+ try {
38
+ const out = execFileSync('git', ['-C', root, 'log', '-1', '--format=%ct'], {
39
+ env: hardenedSpawnEnv(),
40
+ encoding: 'utf-8',
41
+ stdio: ['ignore', 'pipe', 'ignore'],
42
+ }).trim();
43
+ const n = Number(out);
44
+ return Number.isFinite(n) ? n : null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function commitsBehind(root: string, sha: string): number | null {
51
+ try {
52
+ const out = execFileSync('git', ['-C', root, 'rev-list', '--count', `${sha}..HEAD`], {
53
+ env: hardenedSpawnEnv(),
54
+ encoding: 'utf-8',
55
+ stdio: ['ignore', 'pipe', 'ignore'],
56
+ }).trim();
57
+ const n = Number(out);
58
+ return Number.isFinite(n) ? n : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function freshness(root: string, file: string, embeddedSha: string | null): { stale?: boolean; commits_behind?: number } {
65
+ if (embeddedSha) {
66
+ const behind = commitsBehind(root, embeddedSha);
67
+ if (behind !== null) return { stale: behind > 0, commits_behind: behind };
68
+ }
69
+ const last = gitLastCommitEpoch(root);
70
+ if (last === null) return {};
71
+ try {
72
+ const mtime = Math.floor(fs.statSync(file).mtimeMs / 1000);
73
+ return { stale: mtime < last };
74
+ } catch {
75
+ return {};
76
+ }
77
+ }
78
+
79
+ /** Shape-validate a candidate consumer graph.json (native or foreign shape). */
80
+ function looksLikeGraph(p: string): { ok: boolean; sha: string | null } {
81
+ try {
82
+ const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, unknown>;
83
+ // native shape passes validateGraph; a foreign graph.json just needs
84
+ // nodes[] + edges|links[] with source/target/relation.
85
+ if (validateGraph(raw).ok) return { ok: true, sha: (raw['head_at_build'] as string) ?? null };
86
+ const nodes = raw['nodes'];
87
+ const edges = (raw['edges'] ?? raw['links']) as unknown;
88
+ const okShape =
89
+ Array.isArray(nodes) &&
90
+ Array.isArray(edges) &&
91
+ (edges.length === 0 ||
92
+ (typeof edges[0] === 'object' && edges[0] !== null && 'source' in (edges[0] as object) && 'target' in (edges[0] as object)));
93
+ return { ok: okShape, sha: (raw['head_at_build'] as string) ?? null };
94
+ } catch {
95
+ return { ok: false, sha: null };
96
+ }
97
+ }
98
+
99
+ export function detectSources(root: string, nativeCache: string): SourceVerdict[] {
100
+ const abs = path.resolve(root);
101
+ const out: SourceVerdict[] = [];
102
+
103
+ for (const rel of CONSUMER_CANDIDATES) {
104
+ const p = path.join(abs, rel);
105
+ if (fs.existsSync(p)) {
106
+ const { ok, sha } = looksLikeGraph(p);
107
+ if (ok) out.push({ kind: 'consumer', path: p, present: true, ...freshness(abs, p, sha) });
108
+ }
109
+ }
110
+
111
+ // SCIP — presence only
112
+ const scipCandidates = [path.join(abs, 'index.scip')];
113
+ try {
114
+ for (const e of fs.readdirSync(abs)) if (e.endsWith('.scip')) scipCandidates.push(path.join(abs, e));
115
+ } catch {
116
+ /* ignore */
117
+ }
118
+ const scip = scipCandidates.find((p) => fs.existsSync(p));
119
+ if (scip) out.push({ kind: 'scip', path: scip, present: true, note: 'SCIP detected — peer tooling required (no owned reader)' });
120
+
121
+ // native cache
122
+ if (fs.existsSync(nativeCache)) out.push({ kind: 'native', path: nativeCache, present: true, ...freshness(abs, nativeCache, null) });
123
+
124
+ return out;
125
+ }
126
+
127
+ /** Pick the source a query should use: fresh consumer > native > stale consumer. */
128
+ export function pickSource(verdicts: SourceVerdict[]): SourceVerdict | null {
129
+ const consumer = verdicts.find((v) => v.kind === 'consumer' && v.stale !== true);
130
+ if (consumer) return consumer;
131
+ const native = verdicts.find((v) => v.kind === 'native');
132
+ if (native) return native;
133
+ return verdicts.find((v) => v.kind === 'consumer') ?? null;
134
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Per-file extraction — WASM tree-sitter, LLM-free, deterministic.
3
+ *
4
+ * Emits definition nodes and *raw* edges; the second pass in `build.ts`
5
+ * resolves raw-edge targets against the whole-repo symbol table and finalises
6
+ * the honest confidence taxonomy:
7
+ *
8
+ * EXTRACTED — definitions, `use`/`import`, extends/implements, trait use,
9
+ * `new X()`, direct free-function calls.
10
+ * INFERRED — `$this->`/`self::`/`static::`/`parent::` and TS `this.`
11
+ * receivers resolved against the parsed class hierarchy.
12
+ * AMBIGUOUS — dynamic method calls, facades, scoped calls to an unresolved
13
+ * name: kept with candidates, never dropped. This is the
14
+ * expected majority for dynamic-dispatch code (Laravel etc.).
15
+ */
16
+ import {
17
+ type CodeNode,
18
+ type EdgeConfidence,
19
+ type Lang,
20
+ MAX_FILE_BYTES,
21
+ type Relation,
22
+ } from './types.js';
23
+ import { getParser, nodeHasError, type TsNode } from './loader.js';
24
+
25
+ export type CallStyle = 'new' | 'free' | 'this' | 'self' | 'static' | 'parent' | 'scoped' | 'dynamic';
26
+
27
+ export interface RawEdge {
28
+ /** caller / container scope node id (method, function, class, or file). */
29
+ sourceId: string;
30
+ relation: Relation;
31
+ /** bare symbol or method name to resolve in the second pass. */
32
+ targetName: string;
33
+ confidenceHint: EdgeConfidence;
34
+ callStyle?: CallStyle;
35
+ /** enclosing class id, for this/self/static/parent resolution. */
36
+ enclosingClassId?: string;
37
+ /** scope token for `Scope::m()` scoped calls (a class name / self / static). */
38
+ scopeName?: string;
39
+ }
40
+
41
+ export interface FileExtract {
42
+ file: string;
43
+ lang: Lang;
44
+ nodes: CodeNode[];
45
+ rawEdges: RawEdge[];
46
+ /** local class id → its declared parent/interface names (2nd-pass inherits). */
47
+ inherits: Array<{ classId: string; parentName: string }>;
48
+ parseError: boolean;
49
+ }
50
+
51
+ function loc(n: TsNode): number[] {
52
+ return [
53
+ n.startPosition.row + 1,
54
+ n.startPosition.column + 1,
55
+ n.endPosition.row + 1,
56
+ n.endPosition.column + 1,
57
+ ];
58
+ }
59
+
60
+ function namedChildren(n: TsNode): TsNode[] {
61
+ const out: TsNode[] = [];
62
+ for (let i = 0; i < n.namedChildCount; i += 1) {
63
+ const c = n.namedChild(i);
64
+ if (c) out.push(c);
65
+ }
66
+ return out;
67
+ }
68
+
69
+ /** First descendant (DFS, named) whose type is in `types`. */
70
+ function firstNamed(n: TsNode, types: ReadonlySet<string>): TsNode | null {
71
+ for (const c of namedChildren(n)) {
72
+ if (types.has(c.type)) return c;
73
+ const deep = firstNamed(c, types);
74
+ if (deep) return deep;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function nameField(n: TsNode): string | null {
80
+ const nm = n.childForFieldName('name');
81
+ if (nm) return nm.text;
82
+ return null;
83
+ }
84
+
85
+ const NAME_TYPES = new Set(['name', 'identifier', 'qualified_name', 'type_identifier']);
86
+ /** last `\`- or `.`-separated segment of a qualified name. */
87
+ function baseName(raw: string): string {
88
+ const s = raw.trim().replace(/^\\+/, '');
89
+ const parts = s.split(/[\\.]/);
90
+ return parts[parts.length - 1] ?? s;
91
+ }
92
+
93
+ // ── PHP ──────────────────────────────────────────────────────────────────────
94
+
95
+ function extractPhp(root: TsNode, file: string, out: FileExtract): void {
96
+ const fileId = file;
97
+
98
+ const walk = (n: TsNode, classId: string | null, scopeId: string): void => {
99
+ switch (n.type) {
100
+ case 'namespace_use_declaration': {
101
+ for (const clause of namedChildren(n)) {
102
+ const nameNode = firstNamed(clause, new Set(['qualified_name', 'name']));
103
+ if (nameNode) {
104
+ out.rawEdges.push({
105
+ sourceId: fileId,
106
+ relation: 'imports',
107
+ targetName: baseName(nameNode.text),
108
+ confidenceHint: 'EXTRACTED',
109
+ });
110
+ }
111
+ }
112
+ return;
113
+ }
114
+ case 'class_declaration':
115
+ case 'interface_declaration':
116
+ case 'trait_declaration': {
117
+ const nm = nameField(n) ?? '<anon>';
118
+ const kind =
119
+ n.type === 'class_declaration'
120
+ ? 'class'
121
+ : n.type === 'interface_declaration'
122
+ ? 'interface'
123
+ : 'trait';
124
+ const id = `${file}#${nm}`;
125
+ out.nodes.push({ id, label: nm, kind, source_file: file, source_location: loc(n) });
126
+ out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
127
+ // extends / implements
128
+ for (const c of namedChildren(n)) {
129
+ if (c.type === 'base_clause' || c.type === 'class_interface_clause') {
130
+ for (const nameNode of namedChildren(c)) {
131
+ if (NAME_TYPES.has(nameNode.type)) {
132
+ out.inherits.push({ classId: id, parentName: baseName(nameNode.text) });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ for (const c of namedChildren(n)) walk(c, id, id);
138
+ return;
139
+ }
140
+ case 'use_declaration': {
141
+ // trait use inside a class body
142
+ if (classId) {
143
+ for (const nameNode of namedChildren(n)) {
144
+ if (NAME_TYPES.has(nameNode.type)) {
145
+ out.rawEdges.push({
146
+ sourceId: classId,
147
+ relation: 'uses',
148
+ targetName: baseName(nameNode.text),
149
+ confidenceHint: 'EXTRACTED',
150
+ });
151
+ }
152
+ }
153
+ }
154
+ return;
155
+ }
156
+ case 'method_declaration':
157
+ case 'function_definition': {
158
+ const nm = nameField(n) ?? '<anon>';
159
+ const id = classId ? `${classId}::${nm}` : `${file}#${nm}`;
160
+ out.nodes.push({
161
+ id,
162
+ label: nm,
163
+ kind: n.type === 'method_declaration' ? 'method' : 'function',
164
+ source_file: file,
165
+ source_location: loc(n),
166
+ });
167
+ out.rawEdges.push({
168
+ sourceId: classId ?? fileId,
169
+ relation: 'member',
170
+ targetName: nm,
171
+ confidenceHint: 'EXTRACTED',
172
+ });
173
+ const body = n.childForFieldName('body');
174
+ if (body) for (const c of namedChildren(body)) walk(c, classId, id);
175
+ return;
176
+ }
177
+ case 'object_creation_expression': {
178
+ const nameNode = firstNamed(n, new Set(['qualified_name', 'name']));
179
+ if (nameNode)
180
+ out.rawEdges.push({
181
+ sourceId: scopeId,
182
+ relation: 'calls',
183
+ targetName: baseName(nameNode.text),
184
+ confidenceHint: 'EXTRACTED',
185
+ callStyle: 'new',
186
+ });
187
+ break;
188
+ }
189
+ case 'function_call_expression': {
190
+ const fn = n.childForFieldName('function');
191
+ if (fn && NAME_TYPES.has(fn.type))
192
+ out.rawEdges.push({
193
+ sourceId: scopeId,
194
+ relation: 'calls',
195
+ targetName: baseName(fn.text),
196
+ confidenceHint: 'EXTRACTED',
197
+ callStyle: 'free',
198
+ });
199
+ break;
200
+ }
201
+ case 'member_call_expression': {
202
+ const obj = n.childForFieldName('object');
203
+ const nm = n.childForFieldName('name');
204
+ if (nm) {
205
+ const objText = obj?.text ?? '';
206
+ const isThis = objText === '$this';
207
+ out.rawEdges.push({
208
+ sourceId: scopeId,
209
+ relation: 'calls',
210
+ targetName: nm.text,
211
+ confidenceHint: isThis && classId ? 'INFERRED' : 'AMBIGUOUS',
212
+ callStyle: isThis ? 'this' : 'dynamic',
213
+ ...(classId ? { enclosingClassId: classId } : {}),
214
+ });
215
+ }
216
+ break;
217
+ }
218
+ case 'scoped_call_expression': {
219
+ const scope = n.childForFieldName('scope');
220
+ const nm = n.childForFieldName('name');
221
+ if (nm) {
222
+ const scopeText = scope?.text ?? '';
223
+ const isSelf = /^(self|static|parent)$/.test(scopeText);
224
+ out.rawEdges.push({
225
+ sourceId: scopeId,
226
+ relation: 'calls',
227
+ targetName: nm.text,
228
+ confidenceHint: isSelf && classId ? 'INFERRED' : 'AMBIGUOUS',
229
+ callStyle: isSelf ? (scopeText as CallStyle) : 'scoped',
230
+ ...(classId ? { enclosingClassId: classId } : {}),
231
+ ...(isSelf ? {} : { scopeName: baseName(scopeText) }),
232
+ });
233
+ }
234
+ break;
235
+ }
236
+ default:
237
+ break;
238
+ }
239
+ for (const c of namedChildren(n)) walk(c, classId, scopeId);
240
+ };
241
+
242
+ out.nodes.push({ id: fileId, label: file, kind: 'file', source_file: file, source_location: [] });
243
+ walk(root, null, fileId);
244
+ }
245
+
246
+ // ── TypeScript / JavaScript ───────────────────────────────────────────────────
247
+
248
+ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
249
+ const fileId = file;
250
+
251
+ const walk = (n: TsNode, classId: string | null, scopeId: string): void => {
252
+ switch (n.type) {
253
+ case 'import_statement': {
254
+ const clause = firstNamed(n, new Set(['import_clause']));
255
+ if (clause) {
256
+ for (const spec of namedChildren(clause)) {
257
+ for (const idn of [spec, ...namedChildren(spec)])
258
+ if (idn.type === 'identifier' || idn.type === 'import_specifier') {
259
+ const nm = idn.type === 'import_specifier' ? nameField(idn) ?? idn.text : idn.text;
260
+ out.rawEdges.push({
261
+ sourceId: fileId,
262
+ relation: 'imports',
263
+ targetName: baseName(nm),
264
+ confidenceHint: 'EXTRACTED',
265
+ });
266
+ }
267
+ }
268
+ }
269
+ return;
270
+ }
271
+ case 'class_declaration': {
272
+ const nm = nameField(n) ?? '<anon>';
273
+ const id = `${file}#${nm}`;
274
+ out.nodes.push({ id, label: nm, kind: 'class', source_file: file, source_location: loc(n) });
275
+ out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
276
+ const heritage = firstNamed(n, new Set(['class_heritage']));
277
+ if (heritage)
278
+ for (const cl of namedChildren(heritage))
279
+ for (const idn of namedChildren(cl))
280
+ if (idn.type === 'identifier' || idn.type === 'type_identifier')
281
+ out.inherits.push({ classId: id, parentName: baseName(idn.text) });
282
+ const body = n.childForFieldName('body');
283
+ if (body) for (const c of namedChildren(body)) walk(c, id, id);
284
+ return;
285
+ }
286
+ case 'method_definition': {
287
+ const nm = nameField(n) ?? '<anon>';
288
+ const id = classId ? `${classId}::${nm}` : `${file}#${nm}`;
289
+ out.nodes.push({ id, label: nm, kind: 'method', source_file: file, source_location: loc(n) });
290
+ out.rawEdges.push({
291
+ sourceId: classId ?? fileId,
292
+ relation: 'member',
293
+ targetName: nm,
294
+ confidenceHint: 'EXTRACTED',
295
+ });
296
+ const body = n.childForFieldName('body');
297
+ if (body) for (const c of namedChildren(body)) walk(c, classId, id);
298
+ return;
299
+ }
300
+ case 'function_declaration': {
301
+ const nm = nameField(n) ?? '<anon>';
302
+ const id = `${file}#${nm}`;
303
+ out.nodes.push({ id, label: nm, kind: 'function', source_file: file, source_location: loc(n) });
304
+ out.rawEdges.push({ sourceId: fileId, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
305
+ const body = n.childForFieldName('body');
306
+ if (body) for (const c of namedChildren(body)) walk(c, classId, id);
307
+ return;
308
+ }
309
+ case 'new_expression': {
310
+ const ctor = n.childForFieldName('constructor');
311
+ if (ctor && (ctor.type === 'identifier' || ctor.type === 'type_identifier' || ctor.type === 'member_expression'))
312
+ out.rawEdges.push({
313
+ sourceId: scopeId,
314
+ relation: 'calls',
315
+ targetName: baseName(ctor.text.split('.').pop() ?? ctor.text),
316
+ confidenceHint: 'EXTRACTED',
317
+ callStyle: 'new',
318
+ });
319
+ break;
320
+ }
321
+ case 'call_expression': {
322
+ const fn = n.childForFieldName('function');
323
+ if (fn) {
324
+ if (fn.type === 'identifier') {
325
+ out.rawEdges.push({
326
+ sourceId: scopeId,
327
+ relation: 'calls',
328
+ targetName: fn.text,
329
+ confidenceHint: 'EXTRACTED',
330
+ callStyle: 'free',
331
+ });
332
+ } else if (fn.type === 'member_expression') {
333
+ const obj = fn.childForFieldName('object');
334
+ const prop = fn.childForFieldName('property');
335
+ if (prop) {
336
+ const isThis = obj?.type === 'this';
337
+ out.rawEdges.push({
338
+ sourceId: scopeId,
339
+ relation: 'calls',
340
+ targetName: prop.text,
341
+ confidenceHint: isThis && classId ? 'INFERRED' : 'AMBIGUOUS',
342
+ callStyle: isThis ? 'this' : 'dynamic',
343
+ ...(classId ? { enclosingClassId: classId } : {}),
344
+ });
345
+ }
346
+ }
347
+ }
348
+ break;
349
+ }
350
+ default:
351
+ break;
352
+ }
353
+ for (const c of namedChildren(n)) walk(c, classId, scopeId);
354
+ };
355
+
356
+ out.nodes.push({ id: fileId, label: file, kind: 'file', source_file: file, source_location: [] });
357
+ walk(root, null, fileId);
358
+ }
359
+
360
+ /**
361
+ * Extract one file. `source` is the file's UTF-8 content; `relPath` is the
362
+ * repo-relative POSIX path used to build collision-free node ids.
363
+ */
364
+ export async function extractFile(relPath: string, source: string, lang: Lang): Promise<FileExtract> {
365
+ const out: FileExtract = { file: relPath, lang, nodes: [], rawEdges: [], inherits: [], parseError: false };
366
+ if (Buffer.byteLength(source, 'utf-8') > MAX_FILE_BYTES) {
367
+ out.nodes.push({
368
+ id: relPath,
369
+ label: relPath,
370
+ kind: 'skipped',
371
+ source_file: relPath,
372
+ source_location: [],
373
+ });
374
+ return out;
375
+ }
376
+ const parser = await getParser(lang);
377
+ const tree = parser.parse(source);
378
+ try {
379
+ out.parseError = nodeHasError(tree.rootNode);
380
+ if (lang === 'php') extractPhp(tree.rootNode, relPath, out);
381
+ else extractTsJs(tree.rootNode, relPath, out);
382
+ } finally {
383
+ tree.delete(); // delete the tree, never the cached parser (loader.ts)
384
+ }
385
+ return out;
386
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * WASM tree-sitter loader for the code-graph engine.
3
+ *
4
+ * Pinned pair (ABI-verified 2026-07-23): `web-tree-sitter@0.24.7` +
5
+ * `tree-sitter-wasms@0.1.13` → grammar ABI 14. The ABI smoke test
6
+ * (`tests/scripts/code_graph_abi.test.ts`) asserts this pairing loads and
7
+ * parses before merge; a bump to either dependency re-runs it. No network,
8
+ * no node-gyp — grammars load from `node_modules` via the resolved path.
9
+ *
10
+ * web-tree-sitter 0.24.x is CJS with a single default export (the Parser
11
+ * class) and the classic `Parser.Language.load()` API; we reach it through
12
+ * `createRequire` so this ESM module stays portable under the repo's tsx
13
+ * runner.
14
+ */
15
+ import { createRequire } from 'node:module';
16
+ import * as path from 'node:path';
17
+
18
+ import { EXPECTED_GRAMMAR_ABI, GRAMMAR_WASM, type Lang } from './types.js';
19
+
20
+ const require = createRequire(import.meta.url);
21
+
22
+ // Minimal structural types for the 0.24.x surface we use (the package ships
23
+ // its own .d.ts, but the CJS-default shape is simplest to pin locally).
24
+ export interface TsNode {
25
+ type: string;
26
+ text: string;
27
+ startPosition: { row: number; column: number };
28
+ endPosition: { row: number; column: number };
29
+ namedChildCount: number;
30
+ childCount: number;
31
+ namedChild(i: number): TsNode | null;
32
+ child(i: number): TsNode | null;
33
+ childForFieldName(field: string): TsNode | null;
34
+ hasError: boolean | (() => boolean);
35
+ }
36
+ export interface TsTree {
37
+ rootNode: TsNode;
38
+ delete(): void;
39
+ }
40
+ export interface TsLanguage {
41
+ version: number;
42
+ }
43
+ export interface TsParser {
44
+ setLanguage(l: TsLanguage): void;
45
+ parse(src: string): TsTree;
46
+ delete(): void;
47
+ }
48
+ interface ParserStatic {
49
+ init(): Promise<void>;
50
+ new (): TsParser;
51
+ Language: { load(wasmPath: string): Promise<TsLanguage> };
52
+ }
53
+
54
+ let _wasmDir: string | null = null;
55
+ function wasmDir(): string {
56
+ if (_wasmDir === null) {
57
+ _wasmDir = path.join(path.dirname(require.resolve('tree-sitter-wasms/package.json')), 'out');
58
+ }
59
+ return _wasmDir;
60
+ }
61
+
62
+ let _initialized = false;
63
+ const _langCache = new Map<Lang, TsLanguage>();
64
+ const _parserCache = new Map<Lang, TsParser>();
65
+
66
+ export function grammarWasmPath(lang: Lang): string {
67
+ return path.join(wasmDir(), GRAMMAR_WASM[lang]);
68
+ }
69
+
70
+ let _mod: ParserStatic | null = null;
71
+ function _Parser(): ParserStatic {
72
+ // Require exactly once and cache the class reference — repeated
73
+ // createRequire calls under tsx can hand back a module view whose static
74
+ // `Language` is not yet populated, which breaks the second grammar load.
75
+ if (_mod === null) _mod = require('web-tree-sitter') as ParserStatic;
76
+ return _mod;
77
+ }
78
+
79
+ async function _init(): Promise<void> {
80
+ if (!_initialized) {
81
+ await _Parser().init();
82
+ _initialized = true;
83
+ }
84
+ }
85
+
86
+ /** Load (and cache) a grammar; asserts the ABI matches the pinned expectation. */
87
+ export async function loadLanguage(lang: Lang): Promise<TsLanguage> {
88
+ const cached = _langCache.get(lang);
89
+ if (cached) return cached;
90
+ await _init();
91
+ const language = await _Parser().Language.load(grammarWasmPath(lang));
92
+ if (language.version !== EXPECTED_GRAMMAR_ABI) {
93
+ throw new Error(
94
+ `code-graph: grammar ABI drift for ${lang} — got ${language.version}, ` +
95
+ `pinned ${EXPECTED_GRAMMAR_ABI}. Re-pin web-tree-sitter/tree-sitter-wasms ` +
96
+ `and re-run the ABI smoke test.`,
97
+ );
98
+ }
99
+ _langCache.set(lang, language);
100
+ return language;
101
+ }
102
+
103
+ /**
104
+ * A parser bound to `lang`, cached per language for the process lifetime.
105
+ * Callers MUST NOT `.delete()` it — emscripten's runtime is torn down on
106
+ * parser deletion, breaking every subsequent `new Parser()`. Delete only the
107
+ * per-parse `tree` (extract.ts does), so the emscripten heap does not balloon.
108
+ */
109
+ export async function getParser(lang: Lang): Promise<TsParser> {
110
+ const cached = _parserCache.get(lang);
111
+ if (cached) return cached;
112
+ await _init();
113
+ const language = await loadLanguage(lang);
114
+ const parser = new (_Parser())();
115
+ parser.setLanguage(language);
116
+ _parserCache.set(lang, parser);
117
+ return parser;
118
+ }
119
+
120
+ /** Normalise the 0.24.x `hasError` (property) vs newer (method). */
121
+ export function nodeHasError(n: TsNode): boolean {
122
+ return typeof n.hasError === 'function' ? n.hasError() : n.hasError;
123
+ }