kibi-mcp 0.2.4 → 0.3.1

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.
@@ -1,311 +0,0 @@
1
- /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- */
18
- import { parseAtomList, parsePairList } from "./prolog-list.js";
19
- const RULES = [
20
- "transitively_implements",
21
- "transitively_depends",
22
- "impacted_by_change",
23
- "affected_symbols",
24
- "coverage_gap",
25
- "untested_symbols",
26
- "stale",
27
- "orphaned",
28
- "conflicting",
29
- "deprecated_still_used",
30
- "current_adr",
31
- "adr_chain",
32
- "superseded_by",
33
- "domain_contradictions",
34
- ];
35
- export async function handleKbDerive(prolog, args) {
36
- const params = args.params ?? {};
37
- const { rule } = args;
38
- if (!RULES.includes(rule)) {
39
- throw new Error(`Unsupported rule '${rule}'`);
40
- }
41
- let rows = [];
42
- switch (rule) {
43
- case "transitively_implements":
44
- rows = await deriveTransitivelyImplements(prolog, params);
45
- break;
46
- case "transitively_depends":
47
- rows = await deriveTransitivelyDepends(prolog, params);
48
- break;
49
- case "impacted_by_change":
50
- rows = await deriveImpactedByChange(prolog, params);
51
- break;
52
- case "affected_symbols":
53
- rows = await deriveAffectedSymbols(prolog, params);
54
- break;
55
- case "coverage_gap":
56
- rows = await deriveCoverageGap(prolog, params);
57
- break;
58
- case "untested_symbols":
59
- rows = await deriveUntestedSymbols(prolog);
60
- break;
61
- case "stale":
62
- rows = await deriveStale(prolog, params);
63
- break;
64
- case "orphaned":
65
- rows = await deriveOrphaned(prolog, params);
66
- break;
67
- case "conflicting":
68
- rows = await deriveConflicting(prolog, params);
69
- break;
70
- case "deprecated_still_used":
71
- rows = await deriveDeprecatedStillUsed(prolog, params);
72
- break;
73
- case "current_adr":
74
- rows = await deriveCurrentAdr(prolog);
75
- break;
76
- case "adr_chain":
77
- rows = await deriveAdrChain(prolog, params);
78
- break;
79
- case "superseded_by":
80
- rows = await deriveSupersededBy(prolog, params);
81
- break;
82
- case "domain_contradictions":
83
- rows = await deriveDomainContradictions(prolog);
84
- break;
85
- }
86
- return {
87
- content: [
88
- {
89
- type: "text",
90
- text: `Derived ${rows.length} row(s) for rule '${rule}'.`,
91
- },
92
- ],
93
- structuredContent: {
94
- rule,
95
- params,
96
- count: rows.length,
97
- rows,
98
- provenance: {
99
- predicate: rule,
100
- deterministic: true,
101
- },
102
- },
103
- };
104
- }
105
- async function deriveTransitivelyImplements(prolog, params) {
106
- const symbolFilter = asOptionalString(params.symbol);
107
- const reqFilter = asOptionalString(params.req);
108
- const cond = makeConjunction([
109
- symbolFilter ? `Symbol='${escapeAtom(symbolFilter)}'` : "",
110
- reqFilter ? `Req='${escapeAtom(reqFilter)}'` : "",
111
- ]);
112
- const goal = `setof([Symbol,Req], (transitively_implements(Symbol, Req)${cond}), Rows)`;
113
- const pairs = await queryPairRows(prolog, goal, "Rows");
114
- return pairs.map(([symbol, req]) => ({ symbol, req }));
115
- }
116
- async function deriveTransitivelyDepends(prolog, params) {
117
- const req1Filter = asOptionalString(params.req1);
118
- const req2Filter = asOptionalString(params.req2);
119
- const cond = makeConjunction([
120
- req1Filter ? `Req1='${escapeAtom(req1Filter)}'` : "",
121
- req2Filter ? `Req2='${escapeAtom(req2Filter)}'` : "",
122
- ]);
123
- const goal = `setof([Req1,Req2], (transitively_depends(Req1, Req2)${cond}), Rows)`;
124
- const pairs = await queryPairRows(prolog, goal, "Rows");
125
- return pairs.map(([req1, req2]) => ({ req1, req2 }));
126
- }
127
- async function deriveImpactedByChange(prolog, params) {
128
- const changed = asRequiredString(params.changed, "params.changed is required");
129
- const goal = `setof(Entity, impacted_by_change(Entity, '${escapeAtom(changed)}'), Rows)`;
130
- const entities = await queryAtomRows(prolog, goal, "Rows");
131
- return entities.map((entity) => ({ changed, entity }));
132
- }
133
- async function deriveAffectedSymbols(prolog, params) {
134
- const req = asRequiredString(params.req, "params.req is required");
135
- const goal = `affected_symbols('${escapeAtom(req)}', Symbols)`;
136
- const result = await prolog.query(goal);
137
- if (!result.success || !result.bindings.Symbols) {
138
- return [];
139
- }
140
- const symbols = parseAtomList(result.bindings.Symbols);
141
- return symbols.map((symbol) => ({ req, symbol }));
142
- }
143
- async function deriveCoverageGap(prolog, params) {
144
- const reqFilter = asOptionalString(params.req);
145
- const cond = reqFilter ? `Req='${escapeAtom(reqFilter)}'` : "";
146
- const goal = `setof([Req,Reason], (coverage_gap(Req, Reason)${makeConjunction([cond])}), Rows)`;
147
- const pairs = await queryPairRows(prolog, goal, "Rows");
148
- return pairs.map(([req, reason]) => ({ req, reason }));
149
- }
150
- async function deriveUntestedSymbols(prolog) {
151
- const result = await prolog.query("untested_symbols(Symbols)");
152
- if (!result.success || !result.bindings.Symbols) {
153
- return [];
154
- }
155
- const symbols = parseAtomList(result.bindings.Symbols);
156
- return symbols.map((symbol) => ({ symbol }));
157
- }
158
- async function deriveStale(prolog, params) {
159
- const maxAgeDays = Number(params.max_age_days ?? params.maxAgeDays);
160
- if (!Number.isFinite(maxAgeDays)) {
161
- throw new Error("params.max_age_days is required and must be numeric");
162
- }
163
- const entityFilter = asOptionalString(params.entity);
164
- const cond = entityFilter ? `Entity='${escapeAtom(entityFilter)}'` : "";
165
- const goal = `setof(Entity, (stale(Entity, ${maxAgeDays})${makeConjunction([cond])}), Rows)`;
166
- const entities = await queryAtomRows(prolog, goal, "Rows");
167
- return entities.map((entity) => ({ entity, max_age_days: maxAgeDays }));
168
- }
169
- async function deriveOrphaned(prolog, params) {
170
- const symbolFilter = asOptionalString(params.symbol);
171
- const cond = symbolFilter ? `Symbol='${escapeAtom(symbolFilter)}'` : "";
172
- const goal = `setof(Symbol, (orphaned(Symbol)${makeConjunction([cond])}), Rows)`;
173
- const symbols = await queryAtomRows(prolog, goal, "Rows");
174
- return symbols.map((symbol) => ({ symbol }));
175
- }
176
- async function deriveConflicting(prolog, params) {
177
- const adr1Filter = asOptionalString(params.adr1);
178
- const adr2Filter = asOptionalString(params.adr2);
179
- const cond = makeConjunction([
180
- adr1Filter ? `Adr1='${escapeAtom(adr1Filter)}'` : "",
181
- adr2Filter ? `Adr2='${escapeAtom(adr2Filter)}'` : "",
182
- ]);
183
- const goal = `setof([Adr1,Adr2], (conflicting(Adr1, Adr2)${cond}), Rows)`;
184
- const pairs = await queryPairRows(prolog, goal, "Rows");
185
- return pairs.map(([adr1, adr2]) => ({ adr1, adr2 }));
186
- }
187
- async function deriveDeprecatedStillUsed(prolog, params) {
188
- const adrFilter = asOptionalString(params.adr);
189
- const goal = adrFilter
190
- ? `deprecated_still_used('${escapeAtom(adrFilter)}', Symbols)`
191
- : "setof([Adr,Symbols], deprecated_still_used(Adr, Symbols), Rows)";
192
- if (adrFilter) {
193
- const result = await prolog.query(goal);
194
- if (!result.success || !result.bindings.Symbols) {
195
- return [];
196
- }
197
- return [
198
- { adr: adrFilter, symbols: parseAtomList(result.bindings.Symbols) },
199
- ];
200
- }
201
- const pairs = await queryPairRows(prolog, goal, "Rows");
202
- return pairs.map(([adr, symbolsRaw]) => ({
203
- adr,
204
- symbols: parseAtomList(symbolsRaw),
205
- }));
206
- }
207
- async function queryAtomRows(prolog, goal, bindingName) {
208
- const result = await prolog.query(goal);
209
- if (!result.success || !result.bindings[bindingName]) {
210
- return [];
211
- }
212
- return parseAtomList(result.bindings[bindingName]);
213
- }
214
- async function queryPairRows(prolog, goal, bindingName) {
215
- const result = await prolog.query(goal);
216
- if (!result.success || !result.bindings[bindingName]) {
217
- return [];
218
- }
219
- return parsePairList(result.bindings[bindingName]);
220
- }
221
- function asOptionalString(value) {
222
- return typeof value === "string" && value.length > 0 ? value : undefined;
223
- }
224
- function asRequiredString(value, message) {
225
- if (typeof value !== "string" || value.length === 0) {
226
- throw new Error(message);
227
- }
228
- return value;
229
- }
230
- function makeConjunction(parts) {
231
- const filtered = parts.filter((part) => part.length > 0);
232
- if (filtered.length === 0) {
233
- return "";
234
- }
235
- return `, ${filtered.join(", ")}`;
236
- }
237
- function escapeAtom(value) {
238
- return value.replace(/'/g, "\\'");
239
- }
240
- async function deriveCurrentAdr(prolog) {
241
- // Query for all current ADRs and their titles
242
- const result = await prolog.query("setof([Id,TitleAtom], (kb_entity(Id, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom), current_adr(Id)), Rows)");
243
- if (!result.success || !result.bindings.Rows) {
244
- return [];
245
- }
246
- const pairs = parsePairList(result.bindings.Rows);
247
- return pairs.map(([id, title]) => ({ id, title }));
248
- }
249
- async function deriveAdrChain(prolog, params) {
250
- const adr = asRequiredString(params.adr, "params.adr is required");
251
- // Query for the full chain including status
252
- const result = await prolog.query(`findall([Id,TitleAtom,StatusAtom], (kb_entity(Id, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom), memberchk(status=Status, Props), normalize_term_atom(Status, StatusAtom), adr_chain('${escapeAtom(adr)}', Chain), member(Id, Chain)), Rows)`);
253
- if (!result.success || !result.bindings.Rows) {
254
- return [];
255
- }
256
- // Parse triplets and include status
257
- const triplets = parseTripleList(result.bindings.Rows);
258
- return triplets.map(([id, title, status]) => ({ id, title, status }));
259
- }
260
- async function deriveSupersededBy(prolog, params) {
261
- const adr = asRequiredString(params.adr, "params.adr is required");
262
- // Query for direct supersession
263
- const result = await prolog.query(`superseded_by('${escapeAtom(adr)}', NewAdr), kb_entity(NewAdr, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom)`);
264
- if (!result.success ||
265
- !result.bindings.NewAdr ||
266
- !result.bindings.TitleAtom) {
267
- return [];
268
- }
269
- const newAdr = String(result.bindings.NewAdr).replace(/^'|'$/g, "");
270
- const newAdrTitle = String(result.bindings.TitleAtom).replace(/^'|'$/g, "");
271
- return [
272
- {
273
- adr,
274
- successor_id: newAdr,
275
- successor_title: newAdrTitle,
276
- },
277
- ];
278
- }
279
- function parseTripleList(raw) {
280
- const match = raw.match(/\[(.*)\]/);
281
- if (!match) {
282
- return [];
283
- }
284
- const content = match[1].trim();
285
- if (!content) {
286
- return [];
287
- }
288
- // Parse triplets: [[a,b,c],[x,y,z],...]
289
- const triplets = [];
290
- const tripletRegex = /\[([^,]+),([^,]+),([^\]]+)\]/g;
291
- let tripletMatch;
292
- do {
293
- tripletMatch = tripletRegex.exec(content);
294
- if (tripletMatch !== null) {
295
- triplets.push([
296
- tripletMatch[1].trim().replace(/^'|'$/g, ""),
297
- tripletMatch[2].trim().replace(/^'|'$/g, ""),
298
- tripletMatch[3].trim().replace(/^'|'$/g, ""),
299
- ]);
300
- }
301
- } while (tripletMatch !== null);
302
- return triplets;
303
- }
304
- async function deriveDomainContradictions(prolog) {
305
- const result = await prolog.query("setof([ReqA,ReqB,Reason], contradicting_reqs(ReqA, ReqB, Reason), Rows)");
306
- if (!result.success || !result.bindings.Rows) {
307
- return [];
308
- }
309
- const rows = parseTripleList(result.bindings.Rows);
310
- return rows.map(([reqA, reqB, reason]) => ({ reqA, reqB, reason }));
311
- }
@@ -1,70 +0,0 @@
1
- /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- */
18
- import { parseAtomList } from "./prolog-list.js";
19
- export async function handleKbImpact(prolog, args) {
20
- if (!args.entity || typeof args.entity !== "string") {
21
- throw new Error("'entity' is required");
22
- }
23
- const goal = `setof(Id, (impacted_by_change(Id, '${escapeAtom(args.entity)}'), Id \\= '${escapeAtom(args.entity)}'), Impacted)`;
24
- const impactedIds = await queryAtoms(prolog, goal, "Impacted");
25
- const impacted = [];
26
- for (const id of impactedIds) {
27
- const type = await getEntityType(prolog, id);
28
- impacted.push({ id, type: type ?? "unknown" });
29
- }
30
- impacted.sort((a, b) => {
31
- if (a.type === b.type) {
32
- return a.id.localeCompare(b.id);
33
- }
34
- return a.type.localeCompare(b.type);
35
- });
36
- return {
37
- content: [
38
- {
39
- type: "text",
40
- text: `Impact analysis for '${args.entity}': ${impacted.length} impacted entity(s).`,
41
- },
42
- ],
43
- structuredContent: {
44
- entity: args.entity,
45
- impacted,
46
- count: impacted.length,
47
- provenance: {
48
- predicate: "impacted_by_change",
49
- deterministic: true,
50
- },
51
- },
52
- };
53
- }
54
- async function queryAtoms(prolog, goal, bindingName) {
55
- const result = await prolog.query(goal);
56
- if (!result.success || !result.bindings[bindingName]) {
57
- return [];
58
- }
59
- return parseAtomList(result.bindings[bindingName]);
60
- }
61
- async function getEntityType(prolog, id) {
62
- const result = await prolog.query(`kb_entity('${escapeAtom(id)}', Type, _)`);
63
- if (!result.success || !result.bindings.Type) {
64
- return null;
65
- }
66
- return result.bindings.Type;
67
- }
68
- function escapeAtom(value) {
69
- return value.replace(/'/g, "\\'");
70
- }
@@ -1,75 +0,0 @@
1
- /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- */
18
- /**
19
- * Handle kb_list_entity_types tool calls
20
- * Returns the static list of supported KB entity type names (req, scenario, test, adr, flag, event, symbol, fact).
21
- */
22
- export async function handleKbListEntityTypes() {
23
- return {
24
- content: [
25
- {
26
- type: "text",
27
- text: "Available entity types: req, scenario, test, adr, flag, event, symbol, fact",
28
- },
29
- ],
30
- structuredContent: {
31
- types: [
32
- "req",
33
- "scenario",
34
- "test",
35
- "adr",
36
- "flag",
37
- "event",
38
- "symbol",
39
- "fact",
40
- ],
41
- },
42
- };
43
- }
44
- /**
45
- * Handle kb_list_relationship_types tool calls
46
- * Returns the static list of supported KB relationship type names (depends_on, specified_by, verified_by, etc.).
47
- */
48
- export async function handleKbListRelationshipTypes() {
49
- return {
50
- content: [
51
- {
52
- type: "text",
53
- text: "Available relationship types: depends_on, specified_by, verified_by, validates, implements, covered_by, constrained_by, constrains, requires_property, guards, publishes, consumes, supersedes, relates_to",
54
- },
55
- ],
56
- structuredContent: {
57
- types: [
58
- "depends_on",
59
- "specified_by",
60
- "verified_by",
61
- "validates",
62
- "implements",
63
- "covered_by",
64
- "constrained_by",
65
- "constrains",
66
- "requires_property",
67
- "guards",
68
- "publishes",
69
- "consumes",
70
- "supersedes",
71
- "relates_to",
72
- ],
73
- },
74
- };
75
- }
@@ -1,176 +0,0 @@
1
- /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
17
- */
18
- const VALID_REL_TYPES = [
19
- "depends_on",
20
- "specified_by",
21
- "verified_by",
22
- "validates",
23
- "implements",
24
- "covered_by",
25
- "constrained_by",
26
- "constrains",
27
- "requires_property",
28
- "guards",
29
- "publishes",
30
- "consumes",
31
- "supersedes",
32
- "relates_to",
33
- ];
34
- /**
35
- * Handle kb_query_relationships tool calls.
36
- * Queries the kb_relationship/3 predicate which has arity (Type, From, To).
37
- *
38
- * Note: kb_relationship/3 requires RelType to be bound (atom_concat/3 in Prolog
39
- * does not work with an unbound first argument). When no type filter is given,
40
- * we iterate over all known type values.
41
- */
42
- export async function handleKbQueryRelationships(prolog, args) {
43
- const { from, to, type } = args;
44
- if (type && !VALID_REL_TYPES.includes(type)) {
45
- throw new Error(`Invalid relationship type '${type}'. Valid types: ${VALID_REL_TYPES.join(", ")}`);
46
- }
47
- // When type is specified we run one query; otherwise iterate all known types
48
- // (kb_relationship/3 requires the type to be bound due to atom_concat/3 in Prolog).
49
- const typesToQuery = type ? [type] : VALID_REL_TYPES;
50
- const allRelationships = [];
51
- for (const relType of typesToQuery) {
52
- // We collect what we actually need based on which args are bound.
53
- // When both from and to are specified, we just need to check existence.
54
- // Otherwise collect the unbound sides.
55
- let goal;
56
- if (from && to) {
57
- // Check if the specific triple exists
58
- goal = `(kb_relationship('${relType}', '${from}', '${to}') -> Results = [['${from}','${to}']] ; Results = [])`;
59
- }
60
- else if (from) {
61
- goal = `findall(To, kb_relationship('${relType}', '${from}', To), Results)`;
62
- }
63
- else if (to) {
64
- goal = `findall(From, kb_relationship('${relType}', From, '${to}'), Results)`;
65
- }
66
- else {
67
- goal = `findall([From,To], kb_relationship('${relType}', From, To), Results)`;
68
- }
69
- const queryResult = await prolog.query(goal);
70
- if (!queryResult.success) {
71
- throw new Error(queryResult.error || "Relationship query failed");
72
- }
73
- if (queryResult.bindings.Results) {
74
- const raw = queryResult.bindings.Results;
75
- if (from && to) {
76
- // Results is either [[from,to]] or []
77
- const pairs = parsePairResults(raw);
78
- for (const [pairFrom, pairTo] of pairs) {
79
- allRelationships.push({ relType, from: pairFrom, to: pairTo });
80
- }
81
- }
82
- else if (from) {
83
- // Results is [To, To, ...]
84
- const ids = parseIdList(raw);
85
- for (const toId of ids) {
86
- allRelationships.push({ relType, from, to: toId });
87
- }
88
- }
89
- else if (to) {
90
- // Results is [From, From, ...]
91
- const ids = parseIdList(raw);
92
- for (const fromId of ids) {
93
- allRelationships.push({ relType, from: fromId, to });
94
- }
95
- }
96
- else {
97
- // Results is [[From,To], ...]
98
- const pairs = parsePairResults(raw);
99
- for (const [pairFrom, pairTo] of pairs) {
100
- allRelationships.push({ relType, from: pairFrom, to: pairTo });
101
- }
102
- }
103
- }
104
- }
105
- const text = allRelationships.length === 0
106
- ? "No relationships found."
107
- : `Found ${allRelationships.length} relationship(s): ${allRelationships
108
- .map((r) => `${r.from} -[${r.relType}]-> ${r.to}`)
109
- .join(", ")}`;
110
- return {
111
- content: [{ type: "text", text }],
112
- structuredContent: {
113
- relationships: allRelationships,
114
- count: allRelationships.length,
115
- },
116
- };
117
- }
118
- /**
119
- * Parse a flat Prolog list of atoms "[A,B,C]" into a string array.
120
- */
121
- function parseIdList(raw) {
122
- const cleaned = raw.trim();
123
- if (cleaned === "[]" || cleaned === "")
124
- return [];
125
- const inner = cleaned.replace(/^\[/, "").replace(/\]$/, "");
126
- return inner
127
- .split(",")
128
- .map((s) => s.trim().replace(/^'|'$/g, "").replace(/^"|"$/g, ""))
129
- .filter(Boolean);
130
- }
131
- /**
132
- * Parse Prolog findall result "[[From,To],...]" into [from, to] pairs.
133
- */
134
- function parsePairResults(raw) {
135
- const cleaned = raw.trim();
136
- if (cleaned === "[]" || cleaned === "")
137
- return [];
138
- const inner = cleaned.replace(/^\[/, "").replace(/\]$/, "");
139
- const pairs = [];
140
- let depth = 0;
141
- let current = "";
142
- for (let i = 0; i < inner.length; i++) {
143
- const ch = inner[i];
144
- if (ch === "[") {
145
- depth++;
146
- current += ch;
147
- }
148
- else if (ch === "]") {
149
- depth--;
150
- current += ch;
151
- if (depth === 0) {
152
- const pair = parsePair(current.trim());
153
- if (pair)
154
- pairs.push(pair);
155
- current = "";
156
- }
157
- }
158
- else if (ch === "," && depth === 0) {
159
- // top-level separator between pairs — skip
160
- }
161
- else {
162
- current += ch;
163
- }
164
- }
165
- return pairs;
166
- }
167
- function parsePair(pairStr) {
168
- // expect "[From,To]"
169
- const inner = pairStr.replace(/^\[/, "").replace(/\]$/, "").trim();
170
- const parts = inner
171
- .split(",")
172
- .map((s) => s.trim().replace(/^'|'$/g, "").replace(/^"|"$/g, ""));
173
- if (parts.length < 2)
174
- return null;
175
- return [parts[0], parts[1]];
176
- }