ts-knowledge-graph 0.1.1 → 0.1.2
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/README.md +34 -13
- package/contribs/web_visualisation/README.md +55 -0
- package/contribs/web_visualisation/web/css/style.css +115 -0
- package/contribs/web_visualisation/web/data/.gitignore +2 -0
- package/contribs/web_visualisation/web/index.html +58 -0
- package/contribs/web_visualisation/web/js/app.js +364 -0
- package/dist/agent/agent_tools.d.ts +13 -0
- package/dist/agent/agent_tools.d.ts.map +1 -0
- package/dist/agent/agent_tools.js +153 -0
- package/dist/agent/agent_tools.js.map +1 -0
- package/dist/agent/code_editor.d.ts +18 -0
- package/dist/agent/code_editor.d.ts.map +1 -0
- package/dist/agent/code_editor.js +43 -0
- package/dist/agent/code_editor.js.map +1 -0
- package/dist/agent/optimizer_agent.d.ts +30 -0
- package/dist/agent/optimizer_agent.d.ts.map +1 -0
- package/dist/agent/optimizer_agent.js +97 -0
- package/dist/agent/optimizer_agent.js.map +1 -0
- package/dist/cli.d.ts +0 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -208
- package/dist/cli.js.map +1 -1
- package/dist/commands/blast-radius.d.ts +5 -0
- package/dist/commands/blast-radius.d.ts.map +1 -0
- package/dist/commands/blast-radius.js +18 -0
- package/dist/commands/blast-radius.js.map +1 -0
- package/dist/commands/blast_radius.d.ts +5 -0
- package/dist/commands/blast_radius.d.ts.map +1 -0
- package/dist/commands/blast_radius.js +18 -0
- package/dist/commands/blast_radius.js.map +1 -0
- package/dist/commands/blast_radius_command.d.ts +5 -0
- package/dist/commands/blast_radius_command.d.ts.map +1 -0
- package/dist/commands/blast_radius_command.js +18 -0
- package/dist/commands/blast_radius_command.js.map +1 -0
- package/dist/commands/calls.d.ts +5 -0
- package/dist/commands/calls.d.ts.map +1 -0
- package/dist/commands/calls.js +7 -0
- package/dist/commands/calls.js.map +1 -0
- package/dist/commands/calls_command.d.ts +5 -0
- package/dist/commands/calls_command.d.ts.map +1 -0
- package/dist/commands/calls_command.js +7 -0
- package/dist/commands/calls_command.js.map +1 -0
- package/dist/commands/command-helpers.d.ts +15 -0
- package/dist/commands/command-helpers.d.ts.map +1 -0
- package/dist/commands/command-helpers.js +61 -0
- package/dist/commands/command-helpers.js.map +1 -0
- package/dist/commands/command_helpers.d.ts +15 -0
- package/dist/commands/command_helpers.d.ts.map +1 -0
- package/dist/commands/command_helpers.js +61 -0
- package/dist/commands/command_helpers.js.map +1 -0
- package/dist/commands/dead-exports.d.ts +5 -0
- package/dist/commands/dead-exports.d.ts.map +1 -0
- package/dist/commands/dead-exports.js +7 -0
- package/dist/commands/dead-exports.js.map +1 -0
- package/dist/commands/dead_exports.d.ts +5 -0
- package/dist/commands/dead_exports.d.ts.map +1 -0
- package/dist/commands/dead_exports.js +7 -0
- package/dist/commands/dead_exports.js.map +1 -0
- package/dist/commands/dead_exports_command.d.ts +5 -0
- package/dist/commands/dead_exports_command.d.ts.map +1 -0
- package/dist/commands/dead_exports_command.js +7 -0
- package/dist/commands/dead_exports_command.js.map +1 -0
- package/dist/commands/extract.d.ts +8 -0
- package/dist/commands/extract.d.ts.map +1 -0
- package/dist/commands/extract.js +49 -0
- package/dist/commands/extract.js.map +1 -0
- package/dist/commands/extract_command.d.ts +8 -0
- package/dist/commands/extract_command.d.ts.map +1 -0
- package/dist/commands/extract_command.js +49 -0
- package/dist/commands/extract_command.js.map +1 -0
- package/dist/commands/find.d.ts +5 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +7 -0
- package/dist/commands/find.js.map +1 -0
- package/dist/commands/find_command.d.ts +5 -0
- package/dist/commands/find_command.d.ts.map +1 -0
- package/dist/commands/find_command.js +7 -0
- package/dist/commands/find_command.js.map +1 -0
- package/dist/commands/install_command.d.ts +16 -0
- package/dist/commands/install_command.d.ts.map +1 -0
- package/dist/commands/install_command.js +42 -0
- package/dist/commands/install_command.js.map +1 -0
- package/dist/commands/load.d.ts +6 -0
- package/dist/commands/load.d.ts.map +1 -0
- package/dist/commands/load.js +28 -0
- package/dist/commands/load.js.map +1 -0
- package/dist/commands/load_command.d.ts +6 -0
- package/dist/commands/load_command.d.ts.map +1 -0
- package/dist/commands/load_command.js +28 -0
- package/dist/commands/load_command.js.map +1 -0
- package/dist/commands/neighbors.d.ts +5 -0
- package/dist/commands/neighbors.d.ts.map +1 -0
- package/dist/commands/neighbors.js +17 -0
- package/dist/commands/neighbors.js.map +1 -0
- package/dist/commands/neighbors_command.d.ts +5 -0
- package/dist/commands/neighbors_command.d.ts.map +1 -0
- package/dist/commands/neighbors_command.js +17 -0
- package/dist/commands/neighbors_command.js.map +1 -0
- package/dist/commands/optimize.d.ts +6 -0
- package/dist/commands/optimize.d.ts.map +1 -0
- package/dist/commands/optimize.js +59 -0
- package/dist/commands/optimize.js.map +1 -0
- package/dist/commands/optimize_command.d.ts +6 -0
- package/dist/commands/optimize_command.d.ts.map +1 -0
- package/dist/commands/optimize_command.js +59 -0
- package/dist/commands/optimize_command.js.map +1 -0
- package/dist/commands/references.d.ts +5 -0
- package/dist/commands/references.d.ts.map +1 -0
- package/dist/commands/references.js +17 -0
- package/dist/commands/references.js.map +1 -0
- package/dist/commands/references_command.d.ts +5 -0
- package/dist/commands/references_command.d.ts.map +1 -0
- package/dist/commands/references_command.js +17 -0
- package/dist/commands/references_command.js.map +1 -0
- package/dist/commands/web.d.ts +19 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +120 -0
- package/dist/commands/web.js.map +1 -0
- package/dist/commands/web_command.d.ts +19 -0
- package/dist/commands/web_command.d.ts.map +1 -0
- package/dist/commands/web_command.js +120 -0
- package/dist/commands/web_command.js.map +1 -0
- package/dist/commands/who-calls.d.ts +5 -0
- package/dist/commands/who-calls.d.ts.map +1 -0
- package/dist/commands/who-calls.js +7 -0
- package/dist/commands/who-calls.js.map +1 -0
- package/dist/commands/who_calls.d.ts +5 -0
- package/dist/commands/who_calls.d.ts.map +1 -0
- package/dist/commands/who_calls.js +7 -0
- package/dist/commands/who_calls.js.map +1 -0
- package/dist/commands/who_calls_command.d.ts +5 -0
- package/dist/commands/who_calls_command.d.ts.map +1 -0
- package/dist/commands/who_calls_command.js +7 -0
- package/dist/commands/who_calls_command.js.map +1 -0
- package/dist/extract/graph_builder.d.ts +16 -0
- package/dist/extract/graph_builder.d.ts.map +1 -0
- package/dist/extract/graph_builder.js +39 -0
- package/dist/extract/graph_builder.js.map +1 -0
- package/dist/extract/node_id.d.ts +8 -0
- package/dist/extract/node_id.d.ts.map +1 -0
- package/dist/extract/node_id.js +22 -0
- package/dist/extract/node_id.js.map +1 -0
- package/dist/extract/project_loader.d.ts +5 -0
- package/dist/extract/project_loader.d.ts.map +1 -0
- package/dist/extract/project_loader.js +19 -0
- package/dist/extract/project_loader.js.map +1 -0
- package/dist/extract/semantic_extractor.d.ts +22 -0
- package/dist/extract/semantic_extractor.d.ts.map +1 -0
- package/dist/extract/semantic_extractor.js +254 -0
- package/dist/extract/semantic_extractor.js.map +1 -0
- package/dist/extract/structural_extractor.d.ts +18 -0
- package/dist/extract/structural_extractor.d.ts.map +1 -0
- package/dist/extract/structural_extractor.js +97 -0
- package/dist/extract/structural_extractor.js.map +1 -0
- package/dist/query/graph_query.d.ts +28 -0
- package/dist/query/graph_query.d.ts.map +1 -0
- package/dist/query/graph_query.js +93 -0
- package/dist/query/graph_query.js.map +1 -0
- package/dist/store/jsonl_reader.d.ts +11 -0
- package/dist/store/jsonl_reader.d.ts.map +1 -0
- package/dist/store/jsonl_reader.js +19 -0
- package/dist/store/jsonl_reader.js.map +1 -0
- package/dist/store/jsonl_store.d.ts +7 -0
- package/dist/store/jsonl_store.d.ts.map +1 -0
- package/dist/store/jsonl_store.js +13 -0
- package/dist/store/jsonl_store.js.map +1 -0
- package/dist/store/kuzu_store.d.ts +20 -0
- package/dist/store/kuzu_store.d.ts.map +1 -0
- package/dist/store/kuzu_store.js +66 -0
- package/dist/store/kuzu_store.js.map +1 -0
- package/package.json +6 -2
- package/skills/ts-knowledge-graph/SKILL.md +91 -0
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ts_knowledge_graph
|
|
2
2
|
|
|
3
3
|
Parse TypeScript source code into a **knowledge graph**, then use that graph as
|
|
4
4
|
the substrate for an autonomous AI agent that finds and applies code
|
|
5
5
|
optimizations.
|
|
6
6
|
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
Full documentation lives in [`./docs`](docs/INDEX.md). The
|
|
10
|
+
[documentation index](docs/INDEX.md) describes every guide and command — start
|
|
11
|
+
there, or jump straight to [Getting Started](docs/GETTING_STARTED.md).
|
|
12
|
+
|
|
7
13
|
## Why a graph
|
|
8
14
|
|
|
9
15
|
An optimization agent constantly needs to reason about *blast radius*:
|
|
@@ -73,12 +79,27 @@ The query methods on `GraphQuery` (`whoCalls`, `blastRadius`, `deadExports`,
|
|
|
73
79
|
`neighborhood`, …) are designed to map one-to-one onto agent tools: JSON in,
|
|
74
80
|
JSON out.
|
|
75
81
|
|
|
82
|
+
For a task-oriented walk-through of these commands — using them by hand to
|
|
83
|
+
answer impact, dead-code, and dependency questions — see the
|
|
84
|
+
[Static Analysis guide](docs/STATIC_ANALYSIS.md).
|
|
85
|
+
|
|
76
86
|
> **`dead-exports` accuracy:** it is member-aware (a class/interface counts as
|
|
77
87
|
> live when any contained member is referenced) and considers `CALLS`,
|
|
78
88
|
> `EXTENDS`, `IMPLEMENTS`, `USES_TYPE`, `RETURNS`, `PARAM_TYPE`, `INSTANTIATES`,
|
|
79
89
|
> and `READS` (value-identifier) edges. On this repository it reports exactly the
|
|
80
90
|
> two genuinely-unused type aliases — no false positives.
|
|
81
91
|
|
|
92
|
+
### Web visualisation
|
|
93
|
+
|
|
94
|
+
Serve the database as an interactive graph — pan/zoom, kind filters, symbol
|
|
95
|
+
search, per-node edge listing (see
|
|
96
|
+
[contribs/web_visualisation](contribs/web_visualisation)):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm run web # reads ./outputs/graph.kuzu, serves http://localhost:4173
|
|
100
|
+
npm run web -- --db ./outputs/graph.kuzu --port 8080
|
|
101
|
+
```
|
|
102
|
+
|
|
82
103
|
### The optimization agent
|
|
83
104
|
|
|
84
105
|
The end goal: an agent that uses the graph to find and apply optimizations,
|
|
@@ -109,22 +130,22 @@ find/replace with in-memory backups; run on a clean git tree so you can review
|
|
|
109
130
|
src/
|
|
110
131
|
schema/ Zod schemas for nodes and edges (the wire format)
|
|
111
132
|
extract/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
project_loader.ts load a ts-morph Project from tsconfig
|
|
134
|
+
node_id.ts deterministic, position-stable node ids
|
|
135
|
+
structural_extractor.ts modules, declarations, imports, containment
|
|
136
|
+
semantic_extractor.ts heritage, CALLS, INSTANTIATES, type edges
|
|
137
|
+
graph_builder.ts orchestrates extraction, dedupes by id
|
|
117
138
|
store/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
jsonl_store.ts serialize the graph to JSONL
|
|
140
|
+
jsonl_reader.ts read + Zod-validate the JSONL back in
|
|
141
|
+
kuzu_store.ts load the graph into embedded Kùzu, run Cypher
|
|
121
142
|
query/
|
|
122
|
-
|
|
143
|
+
graph_query.ts the agent's query tools (who-calls, blast-radius…)
|
|
123
144
|
agent/
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
agent_tools.ts graph queries + read_file + propose_optimization, as LLM tools
|
|
146
|
+
code_editor.ts unique-match find/replace with in-memory backup + revert
|
|
126
147
|
verifier.ts runs `tsc --noEmit`, returns pass/fail + output
|
|
127
|
-
|
|
148
|
+
optimizer_agent.ts the LLM tool-calling loop (propose → verify → keep/revert)
|
|
128
149
|
cli.ts extract / load / query / optimize commands
|
|
129
150
|
```
|
|
130
151
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Web visualisation
|
|
2
|
+
|
|
3
|
+
Interactive viewer for the knowledge graph — pan/zoom, color-coded node and
|
|
4
|
+
edge kinds with toggleable filters, symbol search, and a per-node detail panel
|
|
5
|
+
showing every incoming/outgoing edge.
|
|
6
|
+
|
|
7
|
+
**No server required.** The page (in [web/](web)) is fully static; only the
|
|
8
|
+
Cytoscape.js library comes from a CDN (so you need internet, or vendor the
|
|
9
|
+
file).
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run build # embed ../../outputs/graph/*.jsonl into web/data/graph_data.js
|
|
15
|
+
npm start # serve web/ on http://localhost:4173 (optional)
|
|
16
|
+
npm run open # open web/index.html directly (file://, macOS)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Loading data — pick one
|
|
20
|
+
|
|
21
|
+
**A. Embed the data, then open the page** (works from `file://`, no server):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# from the repo root, after `npm run extract -- . --semantic`
|
|
25
|
+
cd contribs/web_visualisation
|
|
26
|
+
npm run build
|
|
27
|
+
npm run open
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`scripts/build-data.ts` reads `../../outputs/graph/{nodes,edges}.jsonl` by
|
|
31
|
+
default; pass a different graph directory as the first argument
|
|
32
|
+
(`npx tsx scripts/build-data.ts /path/to/graph`).
|
|
33
|
+
|
|
34
|
+
**B. Drag & drop** — open `web/index.html` any way at all and drop
|
|
35
|
+
`outputs/graph/nodes.jsonl` + `outputs/graph/edges.jsonl` onto the page.
|
|
36
|
+
|
|
37
|
+
**C. Serve the repo root** — the page then auto-fetches
|
|
38
|
+
`../../../outputs/graph/*.jsonl`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# from the repo root
|
|
42
|
+
npx serve # or: python3 -m http.server
|
|
43
|
+
# open http://localhost:3000/contribs/web_visualisation/web/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Reading the graph
|
|
47
|
+
|
|
48
|
+
- **Node size** scales with degree (how connected a symbol is).
|
|
49
|
+
- **Edge colors**: red `CALLS`, green/teal type edges (`USES_TYPE`, `RETURNS`,
|
|
50
|
+
`PARAM_TYPE`), yellow `READS`, violet heritage, gray structure
|
|
51
|
+
(`CONTAINS`, `IMPORTS`).
|
|
52
|
+
- Uncheck noisy kinds (`CONTAINS`, `IMPORTS`) to see the behavioral core;
|
|
53
|
+
enable **hide isolated nodes** to drop whatever the filter disconnected.
|
|
54
|
+
- Click a node to fade everything outside its neighborhood and list its edges
|
|
55
|
+
in the sidebar — the links navigate the graph.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
* { box-sizing: border-box; }
|
|
2
|
+
|
|
3
|
+
html, body {
|
|
4
|
+
margin: 0;
|
|
5
|
+
height: 100%;
|
|
6
|
+
font: 13px/1.45 -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
7
|
+
background: #0f172a;
|
|
8
|
+
color: #cbd5e1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body { display: flex; }
|
|
12
|
+
|
|
13
|
+
#sidebar {
|
|
14
|
+
width: 300px;
|
|
15
|
+
flex: none;
|
|
16
|
+
height: 100%;
|
|
17
|
+
overflow-y: auto;
|
|
18
|
+
padding: 14px;
|
|
19
|
+
background: #111c33;
|
|
20
|
+
border-right: 1px solid #1e293b;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#sidebar h1 { font-size: 15px; margin: 0 0 4px; color: #f1f5f9; }
|
|
24
|
+
#sidebar h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #64748b; margin: 16px 0 6px; }
|
|
25
|
+
#sidebar section { margin-top: 8px; }
|
|
26
|
+
|
|
27
|
+
#status { font-size: 11px; color: #64748b; margin-bottom: 10px; }
|
|
28
|
+
|
|
29
|
+
#search {
|
|
30
|
+
width: 100%;
|
|
31
|
+
padding: 6px 8px;
|
|
32
|
+
border: 1px solid #334155;
|
|
33
|
+
border-radius: 6px;
|
|
34
|
+
background: #0f172a;
|
|
35
|
+
color: #e2e8f0;
|
|
36
|
+
}
|
|
37
|
+
#search:focus { outline: none; border-color: #4f8cff; }
|
|
38
|
+
|
|
39
|
+
#search-results { margin-top: 4px; }
|
|
40
|
+
#search-results .hit {
|
|
41
|
+
padding: 3px 6px;
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
font-size: 12px;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
text-overflow: ellipsis;
|
|
48
|
+
}
|
|
49
|
+
#search-results .hit:hover { background: #1e293b; }
|
|
50
|
+
#search-results .hit .loc { color: #64748b; font-size: 10px; }
|
|
51
|
+
|
|
52
|
+
.legend label {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
padding: 1px 0;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
user-select: none;
|
|
59
|
+
}
|
|
60
|
+
.legend .swatch {
|
|
61
|
+
width: 10px;
|
|
62
|
+
height: 10px;
|
|
63
|
+
border-radius: 3px;
|
|
64
|
+
flex: none;
|
|
65
|
+
}
|
|
66
|
+
.legend .count { margin-left: auto; color: #64748b; font-size: 11px; }
|
|
67
|
+
|
|
68
|
+
.row { display: flex; align-items: center; gap: 6px; margin: 4px 0; }
|
|
69
|
+
|
|
70
|
+
select, button {
|
|
71
|
+
background: #1e293b;
|
|
72
|
+
color: #e2e8f0;
|
|
73
|
+
border: 1px solid #334155;
|
|
74
|
+
border-radius: 6px;
|
|
75
|
+
padding: 4px 8px;
|
|
76
|
+
font: inherit;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
button:hover { background: #334155; }
|
|
80
|
+
select { flex: 1; }
|
|
81
|
+
|
|
82
|
+
#details-body { font-size: 12px; }
|
|
83
|
+
#details-body .id { color: #64748b; font-size: 10px; word-break: break-all; }
|
|
84
|
+
#details-body .kind-tag {
|
|
85
|
+
display: inline-block;
|
|
86
|
+
padding: 1px 6px;
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
font-size: 10px;
|
|
89
|
+
color: #0f172a;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
}
|
|
92
|
+
#details-body h3 { font-size: 11px; color: #64748b; margin: 10px 0 3px; }
|
|
93
|
+
#details-body .edge-row { padding: 1px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
94
|
+
#details-body .edge-kind { color: #64748b; font-size: 10px; }
|
|
95
|
+
#details-body a { color: #7db2ff; cursor: pointer; text-decoration: none; }
|
|
96
|
+
#details-body a:hover { text-decoration: underline; }
|
|
97
|
+
|
|
98
|
+
#cy { flex: 1; height: 100%; }
|
|
99
|
+
|
|
100
|
+
#dropzone {
|
|
101
|
+
position: fixed;
|
|
102
|
+
inset: 0;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
font-size: 18px;
|
|
107
|
+
color: #e2e8f0;
|
|
108
|
+
background: rgba(15, 23, 42, .85);
|
|
109
|
+
border: 3px dashed #4f8cff;
|
|
110
|
+
pointer-events: none;
|
|
111
|
+
opacity: 0;
|
|
112
|
+
transition: opacity .15s;
|
|
113
|
+
}
|
|
114
|
+
#dropzone.active { opacity: 1; }
|
|
115
|
+
#dropzone code { color: #7db2ff; }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>open_ts_optim_ai — knowledge graph</title>
|
|
7
|
+
<link rel="stylesheet" href="./css/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<aside id="sidebar">
|
|
11
|
+
<h1>Knowledge graph</h1>
|
|
12
|
+
<div id="status">no data loaded</div>
|
|
13
|
+
|
|
14
|
+
<input id="search" type="search" placeholder="Search symbol or file… (Enter)" autocomplete="off">
|
|
15
|
+
<div id="search-results"></div>
|
|
16
|
+
|
|
17
|
+
<section>
|
|
18
|
+
<h2>Node kinds</h2>
|
|
19
|
+
<div id="node-kinds" class="legend"></div>
|
|
20
|
+
</section>
|
|
21
|
+
|
|
22
|
+
<section>
|
|
23
|
+
<h2>Edge kinds</h2>
|
|
24
|
+
<div id="edge-kinds" class="legend"></div>
|
|
25
|
+
</section>
|
|
26
|
+
|
|
27
|
+
<section>
|
|
28
|
+
<h2>Options</h2>
|
|
29
|
+
<label class="row"><input id="hide-isolated" type="checkbox"> hide isolated nodes</label>
|
|
30
|
+
<div class="row">
|
|
31
|
+
<select id="layout-select">
|
|
32
|
+
<option value="cose">cose (force)</option>
|
|
33
|
+
<option value="concentric">concentric (by degree)</option>
|
|
34
|
+
<option value="breadthfirst">breadthfirst</option>
|
|
35
|
+
<option value="circle">circle</option>
|
|
36
|
+
<option value="grid">grid</option>
|
|
37
|
+
</select>
|
|
38
|
+
<button id="relayout">layout</button>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<section id="details" class="empty">
|
|
43
|
+
<h2>Selection</h2>
|
|
44
|
+
<div id="details-body">click a node</div>
|
|
45
|
+
</section>
|
|
46
|
+
</aside>
|
|
47
|
+
|
|
48
|
+
<main id="cy"></main>
|
|
49
|
+
|
|
50
|
+
<div id="dropzone" class="hidden-overlay">
|
|
51
|
+
<div>drop <code>nodes.jsonl</code> + <code>edges.jsonl</code> here</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<script src="./data/graph_data.js"></script>
|
|
55
|
+
<script src="https://unpkg.com/cytoscape@3/dist/cytoscape.min.js"></script>
|
|
56
|
+
<script src="./js/app.js"></script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const NODE_COLORS = {
|
|
4
|
+
Module: '#4f8cff',
|
|
5
|
+
Class: '#f59e0b',
|
|
6
|
+
Interface: '#a78bfa',
|
|
7
|
+
TypeAlias: '#34d399',
|
|
8
|
+
Enum: '#f472b6',
|
|
9
|
+
Function: '#fb923c',
|
|
10
|
+
Method: '#facc15',
|
|
11
|
+
Property: '#94a3b8',
|
|
12
|
+
Parameter: '#64748b',
|
|
13
|
+
Variable: '#2dd4bf',
|
|
14
|
+
ExternalModule: '#6b7280',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const EDGE_COLORS = {
|
|
18
|
+
CONTAINS: '#475569',
|
|
19
|
+
IMPORTS: '#64748b',
|
|
20
|
+
EXPORTS: '#64748b',
|
|
21
|
+
CALLS: '#ef4444',
|
|
22
|
+
INSTANTIATES: '#f97316',
|
|
23
|
+
EXTENDS: '#8b5cf6',
|
|
24
|
+
IMPLEMENTS: '#a78bfa',
|
|
25
|
+
USES_TYPE: '#10b981',
|
|
26
|
+
RETURNS: '#14b8a6',
|
|
27
|
+
PARAM_TYPE: '#06b6d4',
|
|
28
|
+
READS: '#eab308',
|
|
29
|
+
WRITES: '#eab308',
|
|
30
|
+
OVERRIDES: '#94a3b8',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const state = {
|
|
34
|
+
nodes: [],
|
|
35
|
+
edges: [],
|
|
36
|
+
cy: undefined,
|
|
37
|
+
hiddenNodeKinds: new Set(),
|
|
38
|
+
hiddenEdgeKinds: new Set(),
|
|
39
|
+
hideIsolated: false,
|
|
40
|
+
droppedFiles: { nodes: undefined, edges: undefined },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const el = (id) => document.getElementById(id);
|
|
44
|
+
|
|
45
|
+
/* ---------- data loading ---------- */
|
|
46
|
+
|
|
47
|
+
function boot() {
|
|
48
|
+
setupDropzone();
|
|
49
|
+
el('hide-isolated').addEventListener('change', (event) => {
|
|
50
|
+
state.hideIsolated = event.target.checked;
|
|
51
|
+
applyFilters();
|
|
52
|
+
});
|
|
53
|
+
el('relayout').addEventListener('click', () => runLayout());
|
|
54
|
+
el('search').addEventListener('input', () => renderSearchResults());
|
|
55
|
+
el('search').addEventListener('keydown', (event) => {
|
|
56
|
+
if (event.key === 'Enter') {
|
|
57
|
+
const first = document.querySelector('#search-results .hit');
|
|
58
|
+
if (first !== null) {
|
|
59
|
+
first.click();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (window.GRAPH_DATA !== undefined) {
|
|
65
|
+
setData(window.GRAPH_DATA.nodes, window.GRAPH_DATA.edges, 'embedded graph_data.js');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (location.protocol.startsWith('http') === true) {
|
|
69
|
+
tryFetch();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
el('status').textContent = 'no data — run `npm run build`, or drop the JSONL files here';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function tryFetch() {
|
|
76
|
+
try {
|
|
77
|
+
const [nodesText, edgesText] = await Promise.all([
|
|
78
|
+
fetch('../../../outputs/graph/nodes.jsonl').then((r) => r.ok === true ? r.text() : Promise.reject(new Error(String(r.status)))),
|
|
79
|
+
fetch('../../../outputs/graph/edges.jsonl').then((r) => r.ok === true ? r.text() : Promise.reject(new Error(String(r.status)))),
|
|
80
|
+
]);
|
|
81
|
+
setData(parseJsonl(nodesText), parseJsonl(edgesText), 'fetched ../../../outputs/graph/*.jsonl');
|
|
82
|
+
} catch {
|
|
83
|
+
el('status').textContent = 'no data — generate data/graph_data.js or drop the JSONL files here';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseJsonl(text) {
|
|
88
|
+
return text.split('\n').filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setupDropzone() {
|
|
92
|
+
const zone = el('dropzone');
|
|
93
|
+
window.addEventListener('dragover', (event) => {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
zone.classList.add('active');
|
|
96
|
+
});
|
|
97
|
+
window.addEventListener('dragleave', (event) => {
|
|
98
|
+
if (event.relatedTarget === null) {
|
|
99
|
+
zone.classList.remove('active');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
window.addEventListener('drop', async (event) => {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
zone.classList.remove('active');
|
|
105
|
+
for (const file of event.dataTransfer.files) {
|
|
106
|
+
const records = parseJsonl(await file.text());
|
|
107
|
+
if (records.length === 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (records[0].from !== undefined && records[0].to !== undefined) {
|
|
111
|
+
state.droppedFiles.edges = records;
|
|
112
|
+
} else {
|
|
113
|
+
state.droppedFiles.nodes = records;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (state.droppedFiles.nodes !== undefined && state.droppedFiles.edges !== undefined) {
|
|
117
|
+
setData(state.droppedFiles.nodes, state.droppedFiles.edges, 'dropped files');
|
|
118
|
+
} else {
|
|
119
|
+
el('status').textContent = 'got one file — drop the other one too';
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ---------- graph construction ---------- */
|
|
125
|
+
|
|
126
|
+
function setData(nodes, edges, sourceLabel) {
|
|
127
|
+
state.nodes = nodes;
|
|
128
|
+
state.edges = edges;
|
|
129
|
+
|
|
130
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
131
|
+
const degree = new Map();
|
|
132
|
+
for (const edge of edges) {
|
|
133
|
+
degree.set(edge.from, (degree.get(edge.from) ?? 0) + 1);
|
|
134
|
+
degree.set(edge.to, (degree.get(edge.to) ?? 0) + 1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const elements = [
|
|
138
|
+
...nodes.map((node) => ({
|
|
139
|
+
group: 'nodes',
|
|
140
|
+
data: { id: node.id, name: node.name, kind: node.kind, filePath: node.filePath, startLine: node.range === undefined ? 0 : node.range.startLine, exported: node.exported === true, degree: degree.get(node.id) ?? 0 },
|
|
141
|
+
})),
|
|
142
|
+
...edges
|
|
143
|
+
.filter((edge) => nodeIds.has(edge.from) === true && nodeIds.has(edge.to) === true)
|
|
144
|
+
.map((edge) => ({
|
|
145
|
+
group: 'edges',
|
|
146
|
+
data: { id: edge.id, source: edge.from, target: edge.to, kind: edge.kind },
|
|
147
|
+
})),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
if (state.cy !== undefined) {
|
|
151
|
+
state.cy.destroy();
|
|
152
|
+
}
|
|
153
|
+
state.cy = cytoscape({
|
|
154
|
+
container: el('cy'),
|
|
155
|
+
elements,
|
|
156
|
+
style: cyStyle(),
|
|
157
|
+
layout: { name: 'cose', animate: false, padding: 30 },
|
|
158
|
+
});
|
|
159
|
+
state.cy.on('tap', 'node', (event) => select(event.target));
|
|
160
|
+
state.cy.on('tap', (event) => {
|
|
161
|
+
if (event.target === state.cy) {
|
|
162
|
+
clearSelection();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
buildLegends();
|
|
167
|
+
applyFilters();
|
|
168
|
+
el('status').textContent = `${sourceLabel} — ${nodes.length} nodes, ${edges.length} edges`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cyStyle() {
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
selector: 'node',
|
|
175
|
+
style: {
|
|
176
|
+
'background-color': (node) => NODE_COLORS[node.data('kind')] ?? '#9ca3af',
|
|
177
|
+
'width': (node) => 8 + Math.sqrt(node.data('degree')) * 4,
|
|
178
|
+
'height': (node) => 8 + Math.sqrt(node.data('degree')) * 4,
|
|
179
|
+
'label': 'data(name)',
|
|
180
|
+
'color': '#cbd5e1',
|
|
181
|
+
'font-size': 8,
|
|
182
|
+
'min-zoomed-font-size': 7,
|
|
183
|
+
'text-valign': 'bottom',
|
|
184
|
+
'text-margin-y': 3,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
selector: 'edge',
|
|
189
|
+
style: {
|
|
190
|
+
'width': 1,
|
|
191
|
+
'line-color': (edge) => EDGE_COLORS[edge.data('kind')] ?? '#475569',
|
|
192
|
+
'target-arrow-color': (edge) => EDGE_COLORS[edge.data('kind')] ?? '#475569',
|
|
193
|
+
'target-arrow-shape': 'triangle',
|
|
194
|
+
'arrow-scale': 0.6,
|
|
195
|
+
'curve-style': 'bezier',
|
|
196
|
+
'opacity': 0.65,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{ selector: '.hidden', style: { display: 'none' } },
|
|
200
|
+
{ selector: '.faded', style: { opacity: 0.08, 'text-opacity': 0 } },
|
|
201
|
+
{ selector: 'node.sel', style: { 'border-width': 3, 'border-color': '#ffffff' } },
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function runLayout() {
|
|
206
|
+
const name = el('layout-select').value;
|
|
207
|
+
const options = name === 'concentric'
|
|
208
|
+
? { name, concentric: (node) => node.degree(), levelWidth: () => 2, animate: false, padding: 30 }
|
|
209
|
+
: { name, animate: false, padding: 30 };
|
|
210
|
+
state.cy.elements(':visible').layout(options).run();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ---------- legends & filtering ---------- */
|
|
214
|
+
|
|
215
|
+
function buildLegends() {
|
|
216
|
+
const nodeCounts = countBy(state.nodes.map((node) => node.kind));
|
|
217
|
+
const edgeCounts = countBy(state.edges.map((edge) => edge.kind));
|
|
218
|
+
renderLegend(el('node-kinds'), nodeCounts, NODE_COLORS, state.hiddenNodeKinds);
|
|
219
|
+
renderLegend(el('edge-kinds'), edgeCounts, EDGE_COLORS, state.hiddenEdgeKinds);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderLegend(container, counts, colors, hiddenSet) {
|
|
223
|
+
container.innerHTML = '';
|
|
224
|
+
for (const [kind, count] of counts) {
|
|
225
|
+
const label = document.createElement('label');
|
|
226
|
+
const checkbox = document.createElement('input');
|
|
227
|
+
checkbox.type = 'checkbox';
|
|
228
|
+
checkbox.checked = hiddenSet.has(kind) === false;
|
|
229
|
+
checkbox.addEventListener('change', () => {
|
|
230
|
+
if (checkbox.checked === true) {
|
|
231
|
+
hiddenSet.delete(kind);
|
|
232
|
+
} else {
|
|
233
|
+
hiddenSet.add(kind);
|
|
234
|
+
}
|
|
235
|
+
applyFilters();
|
|
236
|
+
});
|
|
237
|
+
const swatch = document.createElement('span');
|
|
238
|
+
swatch.className = 'swatch';
|
|
239
|
+
swatch.style.background = colors[kind] ?? '#9ca3af';
|
|
240
|
+
const text = document.createElement('span');
|
|
241
|
+
text.textContent = kind;
|
|
242
|
+
const countSpan = document.createElement('span');
|
|
243
|
+
countSpan.className = 'count';
|
|
244
|
+
countSpan.textContent = String(count);
|
|
245
|
+
label.append(checkbox, swatch, text, countSpan);
|
|
246
|
+
container.appendChild(label);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function applyFilters() {
|
|
251
|
+
const cy = state.cy;
|
|
252
|
+
if (cy === undefined) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
cy.batch(() => {
|
|
256
|
+
cy.nodes().forEach((node) => {
|
|
257
|
+
node.toggleClass('hidden', state.hiddenNodeKinds.has(node.data('kind')) === true);
|
|
258
|
+
});
|
|
259
|
+
cy.edges().forEach((edge) => {
|
|
260
|
+
edge.toggleClass('hidden', state.hiddenEdgeKinds.has(edge.data('kind')) === true);
|
|
261
|
+
});
|
|
262
|
+
if (state.hideIsolated === true) {
|
|
263
|
+
cy.nodes().not('.hidden').forEach((node) => {
|
|
264
|
+
const hasVisibleEdge = node.connectedEdges().some((edge) =>
|
|
265
|
+
edge.hasClass('hidden') === false
|
|
266
|
+
&& edge.source().hasClass('hidden') === false
|
|
267
|
+
&& edge.target().hasClass('hidden') === false);
|
|
268
|
+
if (hasVisibleEdge === false) {
|
|
269
|
+
node.addClass('hidden');
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function countBy(values) {
|
|
277
|
+
const counts = new Map();
|
|
278
|
+
for (const value of values) {
|
|
279
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
280
|
+
}
|
|
281
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* ---------- search ---------- */
|
|
285
|
+
|
|
286
|
+
function renderSearchResults() {
|
|
287
|
+
const query = el('search').value.trim().toLowerCase();
|
|
288
|
+
const container = el('search-results');
|
|
289
|
+
container.innerHTML = '';
|
|
290
|
+
if (query.length < 2) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const hits = state.nodes
|
|
294
|
+
.filter((node) => node.name.toLowerCase().includes(query) === true || node.filePath.toLowerCase().includes(query) === true)
|
|
295
|
+
.slice(0, 15);
|
|
296
|
+
for (const hit of hits) {
|
|
297
|
+
const row = document.createElement('div');
|
|
298
|
+
row.className = 'hit';
|
|
299
|
+
row.innerHTML = `${escapeHtml(hit.name)} <span class="loc">${escapeHtml(hit.kind)} · ${escapeHtml(hit.filePath)}</span>`;
|
|
300
|
+
row.addEventListener('click', () => {
|
|
301
|
+
const node = state.cy.getElementById(hit.id);
|
|
302
|
+
if (node.length === 1) {
|
|
303
|
+
select(node);
|
|
304
|
+
state.cy.animate({ center: { eles: node }, zoom: 2 }, { duration: 350 });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
container.appendChild(row);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* ---------- selection & details ---------- */
|
|
312
|
+
|
|
313
|
+
function select(node) {
|
|
314
|
+
const cy = state.cy;
|
|
315
|
+
cy.elements().addClass('faded').removeClass('sel');
|
|
316
|
+
const hood = node.closedNeighborhood();
|
|
317
|
+
hood.removeClass('faded');
|
|
318
|
+
node.addClass('sel');
|
|
319
|
+
renderDetails(node);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function clearSelection() {
|
|
323
|
+
state.cy.elements().removeClass('faded sel');
|
|
324
|
+
el('details-body').textContent = 'click a node';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderDetails(node) {
|
|
328
|
+
const id = node.id();
|
|
329
|
+
const color = NODE_COLORS[node.data('kind')] ?? '#9ca3af';
|
|
330
|
+
const outgoing = state.edges.filter((edge) => edge.from === id);
|
|
331
|
+
const incoming = state.edges.filter((edge) => edge.to === id);
|
|
332
|
+
const nodeById = new Map(state.nodes.map((entry) => [entry.id, entry]));
|
|
333
|
+
|
|
334
|
+
const renderEdgeRows = (edges, direction) => edges.map((edge) => {
|
|
335
|
+
const otherId = direction === 'out' ? edge.to : edge.from;
|
|
336
|
+
const other = nodeById.get(otherId);
|
|
337
|
+
const name = other === undefined ? otherId : other.name;
|
|
338
|
+
const arrow = direction === 'out' ? '→' : '←';
|
|
339
|
+
return `<div class="edge-row"><span class="edge-kind">${escapeHtml(edge.kind)}</span> ${arrow} <a data-target="${escapeHtml(otherId)}">${escapeHtml(name)}</a></div>`;
|
|
340
|
+
}).join('');
|
|
341
|
+
|
|
342
|
+
el('details-body').innerHTML = `
|
|
343
|
+
<div><span class="kind-tag" style="background:${color}">${escapeHtml(node.data('kind'))}</span> <strong>${escapeHtml(node.data('name'))}</strong></div>
|
|
344
|
+
<div>${escapeHtml(node.data('filePath'))}${node.data('startLine') > 0 ? ':' + node.data('startLine') : ''}</div>
|
|
345
|
+
<div class="id">${escapeHtml(id)}</div>
|
|
346
|
+
<h3>outgoing (${outgoing.length})</h3>${renderEdgeRows(outgoing, 'out')}
|
|
347
|
+
<h3>incoming (${incoming.length})</h3>${renderEdgeRows(incoming, 'in')}
|
|
348
|
+
`;
|
|
349
|
+
el('details-body').querySelectorAll('a[data-target]').forEach((link) => {
|
|
350
|
+
link.addEventListener('click', () => {
|
|
351
|
+
const target = state.cy.getElementById(link.dataset.target);
|
|
352
|
+
if (target.length === 1) {
|
|
353
|
+
select(target);
|
|
354
|
+
state.cy.animate({ center: { eles: target } }, { duration: 300 });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function escapeHtml(value) {
|
|
361
|
+
return String(value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
boot();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { GraphQuery } from '../query/graph_query.js';
|
|
3
|
+
export declare const PROPOSE_TOOL_NAME = "propose_optimization";
|
|
4
|
+
export declare const AGENT_TOOLS: OpenAI.Chat.Completions.ChatCompletionTool[];
|
|
5
|
+
export declare class AgentTools {
|
|
6
|
+
private readonly query;
|
|
7
|
+
private readonly rootPath;
|
|
8
|
+
constructor(query: GraphQuery, rootPath: string);
|
|
9
|
+
dispatch(name: string, input: Record<string, unknown>): Promise<string>;
|
|
10
|
+
private readFile;
|
|
11
|
+
private static stringify;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=agent_tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent_tools.d.ts","sourceRoot":"","sources":["../../src/agent/agent_tools.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,eAAO,MAAM,iBAAiB,yBAAyB,CAAC;AAExD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,kBAAkB,EAyGnE,CAAC;AAEF,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM;IAKzC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;YAqB/D,QAAQ;IAetB,OAAO,CAAC,MAAM,CAAC,SAAS;CAGxB"}
|