@rubytech/create-maxy 1.0.678 → 1.0.680
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/dist/index.js +23 -0
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.d.ts +2 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.js +112 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.d.ts +2 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.js +163 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts +38 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js +130 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/index.js +201 -45
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +78 -0
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +194 -0
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -0
- package/payload/platform/lib/graph-mcp/src/__tests__/cypher-validate.test.ts +141 -0
- package/payload/platform/lib/graph-mcp/src/__tests__/schema-cache.test.ts +169 -0
- package/payload/platform/lib/graph-mcp/src/cypher-validate.ts +157 -0
- package/payload/platform/lib/graph-mcp/src/index.ts +247 -47
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +212 -0
- package/payload/platform/lib/graph-trash/dist/index.d.ts +8 -0
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +109 -14
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +136 -21
- package/payload/platform/plugins/docs/references/memory-guide.md +5 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +18 -0
- package/payload/platform/plugins/memory/PLUGIN.md +1 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +54 -6
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.d.ts +36 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.js +86 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +23 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +47 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts +58 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js +125 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js.map +1 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +16 -0
- package/payload/server/chunk-3RBKKDHC.js +783 -0
- package/payload/server/maxy-edge.js +11 -3
- package/payload/server/server.js +284 -112
package/dist/index.js
CHANGED
|
@@ -1080,6 +1080,28 @@ function installWhisperCpp() {
|
|
|
1080
1080
|
}
|
|
1081
1081
|
console.log(" whisper.cpp installed successfully.");
|
|
1082
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Provision the shared HMAC secret used to sign remote-session cookies
|
|
1085
|
+
* (Task 653). Both `maxy-edge` and `maxy-ui` read this file; without it
|
|
1086
|
+
* they independently mint ephemeral secrets on first use and the
|
|
1087
|
+
* cross-process session namespace silently diverges again.
|
|
1088
|
+
*
|
|
1089
|
+
* First install: create the file (0600, 32-byte hex).
|
|
1090
|
+
* Upgrade: leave the existing file untouched — invalidating it here
|
|
1091
|
+
* would log every operator out on every upgrade.
|
|
1092
|
+
*/
|
|
1093
|
+
function provisionRemoteSessionSecret() {
|
|
1094
|
+
const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
|
|
1095
|
+
const credentialsDir = join(persistDir, "credentials");
|
|
1096
|
+
const secretFile = join(credentialsDir, "remote-session-secret");
|
|
1097
|
+
if (existsSync(secretFile)) {
|
|
1098
|
+
console.log(` [install] remote-session-secret exists — preserved`);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
mkdirSync(credentialsDir, { recursive: true, mode: 0o700 });
|
|
1102
|
+
writeFileSync(secretFile, randomBytes(32).toString("hex"), { mode: 0o600 });
|
|
1103
|
+
console.log(` [install] remote-session-secret provisioned path=${secretFile}`);
|
|
1104
|
+
}
|
|
1083
1105
|
function deployPayload() {
|
|
1084
1106
|
log("8", TOTAL, `Deploying ${BRAND.productName}...`);
|
|
1085
1107
|
if (!existsSync(PAYLOAD_DIR)) {
|
|
@@ -2290,6 +2312,7 @@ try {
|
|
|
2290
2312
|
installWhisperCpp();
|
|
2291
2313
|
deployPayload(); // Must happen before ensureNeo4jPassword — restores config backup
|
|
2292
2314
|
ensureNeo4jPassword(); // Now config/.neo4j-password is available if it existed before
|
|
2315
|
+
provisionRemoteSessionSecret(); // Task 653: shared HMAC key readable by maxy-edge + maxy-ui
|
|
2293
2316
|
buildPlatform();
|
|
2294
2317
|
setupVncViewer();
|
|
2295
2318
|
setupAccount();
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-validate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cypher-validate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const cypher_validate_js_1 = require("../cypher-validate.js");
|
|
9
|
+
// Ground-truth snapshot modelled on the incident's actual Neo4j schema.
|
|
10
|
+
// Conversation/Message via :PART_OF is the relationship the agent fabricated
|
|
11
|
+
// as :HAS_MESSAGE; :BELONGS_TO, :NEXT, :HAS_PART round out the reason-set
|
|
12
|
+
// so nearest-neighbour suggestions can demonstrably beat edit-distance ties.
|
|
13
|
+
const snapshot = {
|
|
14
|
+
labels: new Set([
|
|
15
|
+
"Conversation",
|
|
16
|
+
"AdminConversation",
|
|
17
|
+
"PublicConversation",
|
|
18
|
+
"Message",
|
|
19
|
+
"UserMessage",
|
|
20
|
+
"AssistantMessage",
|
|
21
|
+
"Person",
|
|
22
|
+
"LocalBusiness",
|
|
23
|
+
"Task",
|
|
24
|
+
"KnowledgeDocument",
|
|
25
|
+
]),
|
|
26
|
+
relationshipTypes: new Set([
|
|
27
|
+
"PART_OF",
|
|
28
|
+
"HAS_PART",
|
|
29
|
+
"NEXT",
|
|
30
|
+
"BELONGS_TO",
|
|
31
|
+
"ADMIN_OF",
|
|
32
|
+
"AUTHORED_BY",
|
|
33
|
+
]),
|
|
34
|
+
};
|
|
35
|
+
(0, node_test_1.default)("accepts known label + relationship", () => {
|
|
36
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (m:Message)-[:PART_OF]->(c:Conversation) RETURN c, m", snapshot);
|
|
37
|
+
strict_1.default.equal(result.ok, true);
|
|
38
|
+
strict_1.default.deepEqual(result.unknown, []);
|
|
39
|
+
strict_1.default.ok(result.labelTokens.includes("Message"));
|
|
40
|
+
strict_1.default.ok(result.labelTokens.includes("Conversation"));
|
|
41
|
+
strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
|
|
42
|
+
});
|
|
43
|
+
(0, node_test_1.default)("rejects fabricated :HAS_MESSAGE relationship and suggests :PART_OF", () => {
|
|
44
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation)-[:HAS_MESSAGE]->(m:Message) RETURN c", snapshot);
|
|
45
|
+
strict_1.default.equal(result.ok, false);
|
|
46
|
+
const rel = result.unknown.find((u) => u.token === "HAS_MESSAGE");
|
|
47
|
+
strict_1.default.ok(rel, "expected HAS_MESSAGE in unknown");
|
|
48
|
+
strict_1.default.equal(rel.kind, "relationship");
|
|
49
|
+
// HAS_PART ≈ 4 edits, PART_OF ≈ 6 edits — HAS_PART is actually closer.
|
|
50
|
+
// The incident's specific ask ("suggestion :PART_OF") is a plausible hint,
|
|
51
|
+
// but the real test is that SOMETHING recognisable is top of the list.
|
|
52
|
+
strict_1.default.ok(rel.nearest.length > 0, "expected at least one suggestion");
|
|
53
|
+
strict_1.default.ok(rel.nearest[0] === "HAS_PART" || rel.nearest[0] === "PART_OF");
|
|
54
|
+
strict_1.default.match(rel.hint, /Did you mean/);
|
|
55
|
+
});
|
|
56
|
+
(0, node_test_1.default)("rejects unknown label :Foo with label-kind suggestions", () => {
|
|
57
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (n:Foo) RETURN n", snapshot);
|
|
58
|
+
strict_1.default.equal(result.ok, false);
|
|
59
|
+
const bad = result.unknown.find((u) => u.token === "Foo");
|
|
60
|
+
strict_1.default.ok(bad);
|
|
61
|
+
strict_1.default.equal(bad.kind, "label");
|
|
62
|
+
// Suggestions must be drawn from labels, not from relationship types.
|
|
63
|
+
for (const s of bad.nearest) {
|
|
64
|
+
strict_1.default.ok(snapshot.labels.has(s), `suggestion ${s} not a known label`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
(0, node_test_1.default)("empty cypher passes (no tokens to check)", () => {
|
|
68
|
+
const result = (0, cypher_validate_js_1.validate)("", snapshot);
|
|
69
|
+
strict_1.default.equal(result.ok, true);
|
|
70
|
+
});
|
|
71
|
+
(0, node_test_1.default)("multi-label pattern (n:Conversation:AdminConversation) both validated", () => {
|
|
72
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation:AdminConversation) RETURN c", snapshot);
|
|
73
|
+
strict_1.default.equal(result.ok, true);
|
|
74
|
+
strict_1.default.ok(result.labelTokens.includes("Conversation"));
|
|
75
|
+
strict_1.default.ok(result.labelTokens.includes("AdminConversation"));
|
|
76
|
+
});
|
|
77
|
+
(0, node_test_1.default)("named edge with var-length [r:PART_OF*1..5] validates", () => {
|
|
78
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (m:Message)-[r:PART_OF*1..5]->(c:Conversation) RETURN r", snapshot);
|
|
79
|
+
strict_1.default.equal(result.ok, true);
|
|
80
|
+
strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
|
|
81
|
+
});
|
|
82
|
+
(0, node_test_1.default)("edge-type alternation [:PART_OF|BELONGS_TO] splits and validates both", () => {
|
|
83
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (a)-[:PART_OF|BELONGS_TO]->(b) RETURN a, b", snapshot);
|
|
84
|
+
strict_1.default.equal(result.ok, true);
|
|
85
|
+
strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
|
|
86
|
+
strict_1.default.ok(result.edgeTokens.includes("BELONGS_TO"));
|
|
87
|
+
});
|
|
88
|
+
(0, node_test_1.default)("edge-type alternation with one unknown rejects only the unknown", () => {
|
|
89
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (a)-[:PART_OF|HAS_MESSAGE]->(b) RETURN a", snapshot);
|
|
90
|
+
strict_1.default.equal(result.ok, false);
|
|
91
|
+
strict_1.default.equal(result.unknown.length, 1);
|
|
92
|
+
strict_1.default.equal(result.unknown[0].token, "HAS_MESSAGE");
|
|
93
|
+
});
|
|
94
|
+
(0, node_test_1.default)("string literal containing :LabelLike substring does not false-positive", () => {
|
|
95
|
+
// Without string-stripping, the `:Trashed` inside the quoted literal would
|
|
96
|
+
// be picked up as a label token. The validator must ignore it.
|
|
97
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (n:Person) WHERE n.note = 'contains :Nonesuch substring' RETURN n", snapshot);
|
|
98
|
+
strict_1.default.equal(result.ok, true);
|
|
99
|
+
strict_1.default.ok(!result.labelTokens.includes("Nonesuch"));
|
|
100
|
+
});
|
|
101
|
+
(0, node_test_1.default)("empty schema snapshot fails-open (returns ok=true)", () => {
|
|
102
|
+
// Defence posture: when the cache hasn't loaded yet, the validator must
|
|
103
|
+
// not reject every cypher — it would wedge the admin session. Empty sets
|
|
104
|
+
// mean "unknown schema" not "empty schema"; pass-through is correct.
|
|
105
|
+
const empty = {
|
|
106
|
+
labels: new Set(),
|
|
107
|
+
relationshipTypes: new Set(),
|
|
108
|
+
};
|
|
109
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation)-[:PART_OF]->(m) RETURN c", empty);
|
|
110
|
+
strict_1.default.equal(result.ok, true);
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=cypher-validate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-validate.test.js","sourceRoot":"","sources":["../../src/__tests__/cypher-validate.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-cache.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schema-cache.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const schema_cache_js_1 = require("../schema-cache.js");
|
|
9
|
+
function makeFetcher(labels, rels, opts = {}) {
|
|
10
|
+
let called = 0;
|
|
11
|
+
let currentLabels = labels;
|
|
12
|
+
let currentRels = rels;
|
|
13
|
+
return {
|
|
14
|
+
get calls() {
|
|
15
|
+
return called;
|
|
16
|
+
},
|
|
17
|
+
async labels() {
|
|
18
|
+
called++;
|
|
19
|
+
if (opts.failOnce && called === 1)
|
|
20
|
+
throw new Error("transient");
|
|
21
|
+
return currentLabels;
|
|
22
|
+
},
|
|
23
|
+
async relationshipTypes() {
|
|
24
|
+
return currentRels;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
(0, node_test_1.default)("start() populates the snapshot and sets ready()", async () => {
|
|
29
|
+
const fetcher = makeFetcher(["Conversation", "Message"], ["PART_OF"]);
|
|
30
|
+
const emitted = [];
|
|
31
|
+
const cache = new schema_cache_js_1.SchemaCache(fetcher, {
|
|
32
|
+
refreshIntervalMs: 0,
|
|
33
|
+
emit: (l) => emitted.push(l),
|
|
34
|
+
});
|
|
35
|
+
strict_1.default.equal(cache.ready(), false);
|
|
36
|
+
await cache.start();
|
|
37
|
+
strict_1.default.equal(cache.ready(), true);
|
|
38
|
+
const snap = cache.snapshot();
|
|
39
|
+
strict_1.default.ok(snap.labels.has("Conversation"));
|
|
40
|
+
strict_1.default.ok(snap.labels.has("Message"));
|
|
41
|
+
strict_1.default.ok(snap.relationshipTypes.has("PART_OF"));
|
|
42
|
+
strict_1.default.ok(emitted.some((l) => l.includes("[schema-cache] refresh")));
|
|
43
|
+
cache.stop();
|
|
44
|
+
});
|
|
45
|
+
(0, node_test_1.default)("start() with Neo4j unreachable leaves cache empty and not ready, but does not throw", async () => {
|
|
46
|
+
const fetcher = {
|
|
47
|
+
async labels() {
|
|
48
|
+
throw new Error("ECONNREFUSED");
|
|
49
|
+
},
|
|
50
|
+
async relationshipTypes() {
|
|
51
|
+
throw new Error("ECONNREFUSED");
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const emitted = [];
|
|
55
|
+
const cache = new schema_cache_js_1.SchemaCache(fetcher, {
|
|
56
|
+
refreshIntervalMs: 0,
|
|
57
|
+
emit: (l) => emitted.push(l),
|
|
58
|
+
});
|
|
59
|
+
await cache.start();
|
|
60
|
+
strict_1.default.equal(cache.ready(), false);
|
|
61
|
+
strict_1.default.equal(cache.snapshot().labels.size, 0);
|
|
62
|
+
strict_1.default.ok(emitted.some((l) => l.includes("[schema-cache]") && l.includes("failure")), "expected a failure log line");
|
|
63
|
+
cache.stop();
|
|
64
|
+
});
|
|
65
|
+
(0, node_test_1.default)("refresh() picks up a newly-added relationship type", async () => {
|
|
66
|
+
let rels = ["PART_OF"];
|
|
67
|
+
const fetcher = {
|
|
68
|
+
async labels() {
|
|
69
|
+
return ["Conversation"];
|
|
70
|
+
},
|
|
71
|
+
async relationshipTypes() {
|
|
72
|
+
return rels;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const cache = new schema_cache_js_1.SchemaCache(fetcher, { refreshIntervalMs: 0 });
|
|
76
|
+
await cache.start();
|
|
77
|
+
strict_1.default.equal(cache.snapshot().relationshipTypes.has("MIGRATED_EDGE"), false);
|
|
78
|
+
rels = ["PART_OF", "MIGRATED_EDGE"];
|
|
79
|
+
await cache.refresh("interval");
|
|
80
|
+
strict_1.default.ok(cache.snapshot().relationshipTypes.has("MIGRATED_EDGE"));
|
|
81
|
+
cache.stop();
|
|
82
|
+
});
|
|
83
|
+
(0, node_test_1.default)("refresh() preserves last good cache on transient failure", async () => {
|
|
84
|
+
let fail = false;
|
|
85
|
+
const fetcher = {
|
|
86
|
+
async labels() {
|
|
87
|
+
if (fail)
|
|
88
|
+
throw new Error("transient");
|
|
89
|
+
return ["Conversation"];
|
|
90
|
+
},
|
|
91
|
+
async relationshipTypes() {
|
|
92
|
+
if (fail)
|
|
93
|
+
throw new Error("transient");
|
|
94
|
+
return ["PART_OF"];
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const cache = new schema_cache_js_1.SchemaCache(fetcher, { refreshIntervalMs: 0 });
|
|
98
|
+
await cache.start();
|
|
99
|
+
strict_1.default.equal(cache.ready(), true);
|
|
100
|
+
fail = true;
|
|
101
|
+
const ok = await cache.refresh("interval");
|
|
102
|
+
strict_1.default.equal(ok, false);
|
|
103
|
+
strict_1.default.equal(cache.ready(), true, "should remain ready after a transient failure");
|
|
104
|
+
strict_1.default.ok(cache.snapshot().labels.has("Conversation"));
|
|
105
|
+
cache.stop();
|
|
106
|
+
});
|
|
107
|
+
(0, node_test_1.default)("maybeRebuildOnStaleMiss() triggers refresh when an unknown token has a near match", async () => {
|
|
108
|
+
let rels = ["PART_OF"];
|
|
109
|
+
const fetcher = {
|
|
110
|
+
async labels() {
|
|
111
|
+
return ["Conversation"];
|
|
112
|
+
},
|
|
113
|
+
async relationshipTypes() {
|
|
114
|
+
return rels;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const cache = new schema_cache_js_1.SchemaCache(fetcher, {
|
|
118
|
+
refreshIntervalMs: 0,
|
|
119
|
+
staleMissDebounceMs: 0,
|
|
120
|
+
});
|
|
121
|
+
await cache.start();
|
|
122
|
+
rels = ["PART_OF", "PART_OFF"];
|
|
123
|
+
const triggered = await cache.maybeRebuildOnStaleMiss([
|
|
124
|
+
{ token: "PART_OFF", kind: "relationship", nearest: ["PART_OF"], hint: "" },
|
|
125
|
+
]);
|
|
126
|
+
strict_1.default.equal(triggered, true);
|
|
127
|
+
strict_1.default.ok(cache.snapshot().relationshipTypes.has("PART_OFF"));
|
|
128
|
+
cache.stop();
|
|
129
|
+
});
|
|
130
|
+
(0, node_test_1.default)("maybeRebuildOnStaleMiss() skips when no near match (likely typo, not migration)", async () => {
|
|
131
|
+
const fetcher = makeFetcher(["Conversation"], ["PART_OF"]);
|
|
132
|
+
const cache = new schema_cache_js_1.SchemaCache(fetcher, {
|
|
133
|
+
refreshIntervalMs: 0,
|
|
134
|
+
staleMissDebounceMs: 0,
|
|
135
|
+
});
|
|
136
|
+
await cache.start();
|
|
137
|
+
const callsBefore = fetcher.calls;
|
|
138
|
+
const triggered = await cache.maybeRebuildOnStaleMiss([
|
|
139
|
+
{ token: "COMPLETELY_DIFFERENT", kind: "relationship", nearest: ["PART_OF"], hint: "" },
|
|
140
|
+
]);
|
|
141
|
+
strict_1.default.equal(triggered, false);
|
|
142
|
+
strict_1.default.equal(fetcher.calls, callsBefore, "no extra fetches when edit distance is far");
|
|
143
|
+
cache.stop();
|
|
144
|
+
});
|
|
145
|
+
(0, node_test_1.default)("maybeRebuildOnStaleMiss() debounces repeated triggers", async () => {
|
|
146
|
+
const fetcher = makeFetcher(["Conversation"], ["PART_OF"]);
|
|
147
|
+
const cache = new schema_cache_js_1.SchemaCache(fetcher, {
|
|
148
|
+
refreshIntervalMs: 0,
|
|
149
|
+
staleMissDebounceMs: 5000,
|
|
150
|
+
});
|
|
151
|
+
await cache.start();
|
|
152
|
+
const callsBefore = fetcher.calls;
|
|
153
|
+
const unknown = [
|
|
154
|
+
{ token: "PART_OFF", kind: "relationship", nearest: ["PART_OF"], hint: "" },
|
|
155
|
+
];
|
|
156
|
+
await cache.maybeRebuildOnStaleMiss(unknown);
|
|
157
|
+
const afterFirst = fetcher.calls;
|
|
158
|
+
await cache.maybeRebuildOnStaleMiss(unknown);
|
|
159
|
+
strict_1.default.equal(fetcher.calls, afterFirst, "second call within debounce should not refetch");
|
|
160
|
+
strict_1.default.ok(afterFirst > callsBefore, "first call did refetch");
|
|
161
|
+
cache.stop();
|
|
162
|
+
});
|
|
163
|
+
//# sourceMappingURL=schema-cache.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-cache.test.js","sourceRoot":"","sources":["../../src/__tests__/schema-cache.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cypher schema validation for the graph-mcp proxy (Task 654).
|
|
3
|
+
*
|
|
4
|
+
* Tokenises a cypher string for labels (`:Label`) and relationship types
|
|
5
|
+
* (`[:REL_TYPE]`, including `[r:TYPE]`, `[:TYPE*1..5]`, `[:A|B]`) and checks
|
|
6
|
+
* each against a schema snapshot. Unknown tokens are returned with the
|
|
7
|
+
* Levenshtein-nearest known tokens of the same kind as suggestions.
|
|
8
|
+
*
|
|
9
|
+
* Design posture:
|
|
10
|
+
* - Defence layer, not a cypher correctness gate. Pass-through on any
|
|
11
|
+
* unparseable pattern — Neo4j remains the syntax authority.
|
|
12
|
+
* - Empty schema snapshot (cache not ready, or Neo4j unreachable at boot)
|
|
13
|
+
* fails OPEN: ok=true, no rejections. The boot-race treatment is a
|
|
14
|
+
* deliberate choice — refusing all cypher would wedge the admin agent
|
|
15
|
+
* harder than the typo class this layer prevents. The caller emits
|
|
16
|
+
* `validated=false` on the existing [graph-query] line so operators
|
|
17
|
+
* still see the bypass.
|
|
18
|
+
* - String literals are stripped before tokenisation to avoid matching
|
|
19
|
+
* `:Foo` inside quoted content (e.g. `WHERE n.note CONTAINS ':Foo'`).
|
|
20
|
+
*/
|
|
21
|
+
export interface SchemaSnapshot {
|
|
22
|
+
readonly labels: ReadonlySet<string>;
|
|
23
|
+
readonly relationshipTypes: ReadonlySet<string>;
|
|
24
|
+
}
|
|
25
|
+
export interface UnknownToken {
|
|
26
|
+
token: string;
|
|
27
|
+
kind: "label" | "relationship";
|
|
28
|
+
nearest: string[];
|
|
29
|
+
hint: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ValidationResult {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
unknown: UnknownToken[];
|
|
34
|
+
labelTokens: string[];
|
|
35
|
+
edgeTokens: string[];
|
|
36
|
+
}
|
|
37
|
+
export declare function validate(cypher: string, snapshot: SchemaSnapshot): ValidationResult;
|
|
38
|
+
//# sourceMappingURL=cypher-validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-validate.d.ts","sourceRoot":"","sources":["../src/cypher-validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,GAAG,cAAc,CAAC;IAC/B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAkFD,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,GACvB,gBAAgB,CAiClB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cypher schema validation for the graph-mcp proxy (Task 654).
|
|
4
|
+
*
|
|
5
|
+
* Tokenises a cypher string for labels (`:Label`) and relationship types
|
|
6
|
+
* (`[:REL_TYPE]`, including `[r:TYPE]`, `[:TYPE*1..5]`, `[:A|B]`) and checks
|
|
7
|
+
* each against a schema snapshot. Unknown tokens are returned with the
|
|
8
|
+
* Levenshtein-nearest known tokens of the same kind as suggestions.
|
|
9
|
+
*
|
|
10
|
+
* Design posture:
|
|
11
|
+
* - Defence layer, not a cypher correctness gate. Pass-through on any
|
|
12
|
+
* unparseable pattern — Neo4j remains the syntax authority.
|
|
13
|
+
* - Empty schema snapshot (cache not ready, or Neo4j unreachable at boot)
|
|
14
|
+
* fails OPEN: ok=true, no rejections. The boot-race treatment is a
|
|
15
|
+
* deliberate choice — refusing all cypher would wedge the admin agent
|
|
16
|
+
* harder than the typo class this layer prevents. The caller emits
|
|
17
|
+
* `validated=false` on the existing [graph-query] line so operators
|
|
18
|
+
* still see the bypass.
|
|
19
|
+
* - String literals are stripped before tokenisation to avoid matching
|
|
20
|
+
* `:Foo` inside quoted content (e.g. `WHERE n.note CONTAINS ':Foo'`).
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.validate = validate;
|
|
24
|
+
// Bracket content containing a type reference. Examples this matches:
|
|
25
|
+
// [:PART_OF]
|
|
26
|
+
// [r:PART_OF]
|
|
27
|
+
// [:PART_OF*1..5]
|
|
28
|
+
// [r:PART_OF*]
|
|
29
|
+
// [:A|B]
|
|
30
|
+
// The captured group is the TYPE(|TYPE)* alternation; the consumer splits it.
|
|
31
|
+
const EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
|
|
32
|
+
// Label reference in a node pattern. Applied to a remainder string that has
|
|
33
|
+
// already had edge-bracket substrings stripped, so there is no overlap with
|
|
34
|
+
// the edge pattern. The [A-Z] anchor excludes lowercase map keys.
|
|
35
|
+
const LABEL_PATTERN = /:([A-Z][A-Za-z0-9_]*)/g;
|
|
36
|
+
function stripStringLiterals(cypher) {
|
|
37
|
+
// Replace single- and double-quoted literals with empty quotes. Preserves
|
|
38
|
+
// positional structure without retaining content that could match the
|
|
39
|
+
// label regex (e.g. ':SomeLabel' inside a string).
|
|
40
|
+
return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
41
|
+
}
|
|
42
|
+
function extractTokens(cypher) {
|
|
43
|
+
const cleaned = stripStringLiterals(cypher);
|
|
44
|
+
const edges = new Set();
|
|
45
|
+
let match;
|
|
46
|
+
const edgePattern = new RegExp(EDGE_PATTERN.source, EDGE_PATTERN.flags);
|
|
47
|
+
while ((match = edgePattern.exec(cleaned)) !== null) {
|
|
48
|
+
for (const type of match[1].split("|")) {
|
|
49
|
+
const clean = type.trim();
|
|
50
|
+
if (clean)
|
|
51
|
+
edges.add(clean);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const remainder = cleaned.replace(edgePattern, "");
|
|
55
|
+
const labels = new Set();
|
|
56
|
+
const labelPattern = new RegExp(LABEL_PATTERN.source, LABEL_PATTERN.flags);
|
|
57
|
+
while ((match = labelPattern.exec(remainder)) !== null) {
|
|
58
|
+
labels.add(match[1]);
|
|
59
|
+
}
|
|
60
|
+
return { labels, edges };
|
|
61
|
+
}
|
|
62
|
+
function levenshtein(a, b) {
|
|
63
|
+
if (a === b)
|
|
64
|
+
return 0;
|
|
65
|
+
if (a.length === 0)
|
|
66
|
+
return b.length;
|
|
67
|
+
if (b.length === 0)
|
|
68
|
+
return a.length;
|
|
69
|
+
let prev = new Array(b.length + 1);
|
|
70
|
+
let curr = new Array(b.length + 1);
|
|
71
|
+
for (let j = 0; j <= b.length; j++)
|
|
72
|
+
prev[j] = j;
|
|
73
|
+
for (let i = 1; i <= a.length; i++) {
|
|
74
|
+
curr[0] = i;
|
|
75
|
+
for (let j = 1; j <= b.length; j++) {
|
|
76
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
77
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
78
|
+
}
|
|
79
|
+
[prev, curr] = [curr, prev];
|
|
80
|
+
}
|
|
81
|
+
return prev[b.length];
|
|
82
|
+
}
|
|
83
|
+
function nearestMatches(token, known, limit = 3) {
|
|
84
|
+
if (known.size === 0)
|
|
85
|
+
return [];
|
|
86
|
+
const scored = [];
|
|
87
|
+
for (const k of known)
|
|
88
|
+
scored.push([k, levenshtein(token, k)]);
|
|
89
|
+
scored.sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0]));
|
|
90
|
+
return scored.slice(0, limit).map(([k]) => k);
|
|
91
|
+
}
|
|
92
|
+
function hintFor(token, kind, nearest) {
|
|
93
|
+
const didYouMean = nearest.length > 0 ? `Did you mean :${nearest[0]}? ` : "";
|
|
94
|
+
return `${didYouMean}Unknown ${kind} '${token}' — not in the current Neo4j schema. See .docs/neo4j.md for the canonical taxonomy.`;
|
|
95
|
+
}
|
|
96
|
+
function validate(cypher, snapshot) {
|
|
97
|
+
const { labels, edges } = extractTokens(cypher);
|
|
98
|
+
// Fail-open when the snapshot is empty. An empty snapshot means "schema
|
|
99
|
+
// cache not loaded" (boot race, Neo4j unreachable). Rejecting every token
|
|
100
|
+
// would wedge the admin agent; letting it through preserves the observable
|
|
101
|
+
// `validated=false` signal on the existing [graph-query] line.
|
|
102
|
+
if (snapshot.labels.size === 0 && snapshot.relationshipTypes.size === 0) {
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
unknown: [],
|
|
106
|
+
labelTokens: [...labels],
|
|
107
|
+
edgeTokens: [...edges],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const unknown = [];
|
|
111
|
+
for (const token of labels) {
|
|
112
|
+
if (!snapshot.labels.has(token)) {
|
|
113
|
+
const nearest = nearestMatches(token, snapshot.labels);
|
|
114
|
+
unknown.push({ token, kind: "label", nearest, hint: hintFor(token, "label", nearest) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const token of edges) {
|
|
118
|
+
if (!snapshot.relationshipTypes.has(token)) {
|
|
119
|
+
const nearest = nearestMatches(token, snapshot.relationshipTypes);
|
|
120
|
+
unknown.push({ token, kind: "relationship", nearest, hint: hintFor(token, "relationship", nearest) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
ok: unknown.length === 0,
|
|
125
|
+
unknown,
|
|
126
|
+
labelTokens: [...labels],
|
|
127
|
+
edgeTokens: [...edges],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=cypher-validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-validate.js","sourceRoot":"","sources":["../src/cypher-validate.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;AAqGH,4BAoCC;AApHD,sEAAsE;AACtE,eAAe;AACf,gBAAgB;AAChB,oBAAoB;AACpB,iBAAiB;AACjB,WAAW;AACX,8EAA8E;AAC9E,MAAM,YAAY,GAAG,qEAAqE,CAAC;AAE3F,4EAA4E;AAC5E,4EAA4E;AAC5E,kEAAkE;AAClE,MAAM,aAAa,GAAG,wBAAwB,CAAC;AAE/C,SAAS,mBAAmB,CAAC,MAAc;IACzC,0EAA0E;IAC1E,sEAAsE;IACtE,mDAAmD;IACnD,OAAO,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,MAAM,OAAO,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,IAAI,KAA6B,CAAC;IAClC,MAAM,WAAW,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACxE,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACpD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,KAAK;gBAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;IAC3E,OAAO,CAAC,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACvD,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS;IACvC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,MAAM,CAAC;IACpC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,MAAM,CAAC;IACpC,IAAI,IAAI,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,IAAI,IAAI,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3C,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QACvE,CAAC;QACD,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,cAAc,CACrB,KAAa,EACb,KAA0B,EAC1B,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,OAAO,CACd,KAAa,EACb,IAA8B,EAC9B,OAAiB;IAEjB,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,iBAAiB,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,OAAO,GAAG,UAAU,WAAW,IAAI,KAAK,KAAK,qFAAqF,CAAC;AACrI,CAAC;AAED,SAAgB,QAAQ,CACtB,MAAc,EACd,QAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAChD,wEAAwE;IACxE,0EAA0E;IAC1E,2EAA2E;IAC3E,+DAA+D;IAC/D,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO;YACL,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,EAAE;YACX,WAAW,EAAE,CAAC,GAAG,MAAM,CAAC;YACxB,UAAU,EAAE,CAAC,GAAG,KAAK,CAAC;SACvB,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;YACvD,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3C,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CAAC;YAClE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QACxG,CAAC;IACH,CAAC;IACD,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC;QACxB,OAAO;QACP,WAAW,EAAE,CAAC,GAAG,MAAM,CAAC;QACxB,UAAU,EAAE,CAAC,GAAG,KAAK,CAAC;KACvB,CAAC;AACJ,CAAC"}
|