@rubytech/create-maxy 1.0.744 → 1.0.746
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/__tests__/peer-brand-detect.test.js +103 -0
- package/dist/index.js +63 -4
- package/dist/peer-brand-detect.js +39 -0
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts +37 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js +333 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts +71 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js +168 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/index.js +146 -3
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +3 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +30 -7
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/cypher-rewrite-stamp.ts +349 -0
- package/payload/platform/lib/graph-mcp/src/cypher-shim-write.ts +240 -0
- package/payload/platform/lib/graph-mcp/src/index.ts +216 -4
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +37 -7
- package/payload/platform/plugins/docs/references/deployment.md +2 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +3 -22
- package/payload/server/chunk-SPTD7L7Z.js +9474 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{graph-DhNy70eS.js → graph-BoSJpLG3.js} +1 -1
- package/payload/server/public/graph.html +1 -1
- package/payload/server/server.js +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Task 800 — acceptance grid for findPeerBrandOnDefaultNeo4jPort.
|
|
2
|
+
//
|
|
3
|
+
// Each test exercises one branch of the matching rule. The wrapper in
|
|
4
|
+
// index.ts owns fs reads + warning emission; this suite does not touch fs.
|
|
5
|
+
//
|
|
6
|
+
// Runs via Node's built-in test runner (matches apt-resolve.test.ts +
|
|
7
|
+
// port-canonicalisation.test.ts). The brief asked for vitest, but the
|
|
8
|
+
// package has no vitest dependency — node:test is the codebase convention
|
|
9
|
+
// and ships with the toolchain.
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { findPeerBrandOnDefaultNeo4jPort } from "../peer-brand-detect.js";
|
|
13
|
+
const DEFAULT_PORT = 7687;
|
|
14
|
+
const CURRENT = "realagent"; // installer is realagent; peer would be maxy
|
|
15
|
+
test("no peer hostnames → null", () => {
|
|
16
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
17
|
+
currentBrandHostname: CURRENT,
|
|
18
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
19
|
+
peerEnvContents: [],
|
|
20
|
+
});
|
|
21
|
+
assert.equal(result, null);
|
|
22
|
+
});
|
|
23
|
+
test("peer with content=null (no .env on disk) → null", () => {
|
|
24
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
25
|
+
currentBrandHostname: CURRENT,
|
|
26
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
27
|
+
peerEnvContents: [["maxy", null]],
|
|
28
|
+
});
|
|
29
|
+
assert.equal(result, null);
|
|
30
|
+
});
|
|
31
|
+
test("peer pins NEO4J_URI=bolt://localhost:7687 → returns peer hostname", () => {
|
|
32
|
+
const env = `EMBED_MODEL=nomic-embed-text\nNEO4J_URI=bolt://localhost:7687\nACCOUNT_ID=abc\n`;
|
|
33
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
34
|
+
currentBrandHostname: CURRENT,
|
|
35
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
36
|
+
peerEnvContents: [["maxy", env]],
|
|
37
|
+
});
|
|
38
|
+
assert.equal(result, "maxy");
|
|
39
|
+
});
|
|
40
|
+
test("peer pins dedicated port 7688 (not the system unit) → null", () => {
|
|
41
|
+
const env = `NEO4J_URI=bolt://localhost:7688\n`;
|
|
42
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
43
|
+
currentBrandHostname: CURRENT,
|
|
44
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
45
|
+
peerEnvContents: [["maxy", env]],
|
|
46
|
+
});
|
|
47
|
+
assert.equal(result, null);
|
|
48
|
+
});
|
|
49
|
+
test("peer .env has no NEO4J_URI line → null", () => {
|
|
50
|
+
const env = `EMBED_MODEL=nomic-embed-text\nDISPLAY_MODE=hd\n`;
|
|
51
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
52
|
+
currentBrandHostname: CURRENT,
|
|
53
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
54
|
+
peerEnvContents: [["maxy", env]],
|
|
55
|
+
});
|
|
56
|
+
assert.equal(result, null);
|
|
57
|
+
});
|
|
58
|
+
test("peer .env is empty → null", () => {
|
|
59
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
60
|
+
currentBrandHostname: CURRENT,
|
|
61
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
62
|
+
peerEnvContents: [["maxy", ""]],
|
|
63
|
+
});
|
|
64
|
+
assert.equal(result, null);
|
|
65
|
+
});
|
|
66
|
+
test("multiple peers, second one matches → returns the matching hostname", () => {
|
|
67
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
68
|
+
currentBrandHostname: CURRENT,
|
|
69
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
70
|
+
peerEnvContents: [
|
|
71
|
+
["future-brand", `NEO4J_URI=bolt://localhost:7689\n`],
|
|
72
|
+
["maxy", `NEO4J_URI=bolt://localhost:7687\n`],
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
assert.equal(result, "maxy");
|
|
76
|
+
});
|
|
77
|
+
test("only the current brand in iterable → filtered, returns null", () => {
|
|
78
|
+
// Caller may pass the full known-brand list; the helper filters self.
|
|
79
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
80
|
+
currentBrandHostname: CURRENT,
|
|
81
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
82
|
+
peerEnvContents: [[CURRENT, `NEO4J_URI=bolt://localhost:7687\n`]],
|
|
83
|
+
});
|
|
84
|
+
assert.equal(result, null);
|
|
85
|
+
});
|
|
86
|
+
test("URI with non-bolt scheme is not a match", () => {
|
|
87
|
+
const env = `NEO4J_URI=neo4j://localhost:7687\n`;
|
|
88
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
89
|
+
currentBrandHostname: CURRENT,
|
|
90
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
91
|
+
peerEnvContents: [["maxy", env]],
|
|
92
|
+
});
|
|
93
|
+
assert.equal(result, null);
|
|
94
|
+
});
|
|
95
|
+
test("URI with non-localhost host is not a match", () => {
|
|
96
|
+
const env = `NEO4J_URI=bolt://neo4j.example.com:7687\n`;
|
|
97
|
+
const result = findPeerBrandOnDefaultNeo4jPort({
|
|
98
|
+
currentBrandHostname: CURRENT,
|
|
99
|
+
defaultNeo4jPort: DEFAULT_PORT,
|
|
100
|
+
peerEnvContents: [["maxy", env]],
|
|
101
|
+
});
|
|
102
|
+
assert.equal(result, null);
|
|
103
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { resolveInstallPortFromFs, buildMaxyUnitFile } from "./port-resolution.js";
|
|
7
7
|
import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
|
|
8
|
+
import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
|
|
8
9
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
9
10
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
10
11
|
// The bundler stamps brand.json into the payload at build time.
|
|
@@ -841,6 +842,48 @@ function installNeo4j() {
|
|
|
841
842
|
shell("systemctl", ["start", "neo4j"], { sudo: true });
|
|
842
843
|
console.log(" Neo4j started. Password stored securely.");
|
|
843
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Task 800 — does any peer brand on this host pin `NEO4J_URI=bolt://localhost:<default>`
|
|
847
|
+
* in its `.env`? If yes, the apt-installed `neo4j.service` is its database and the
|
|
848
|
+
* dedicated-unit installer must NOT stop+disable it. Returns the first matching
|
|
849
|
+
* peer's hostname, or `null` when no peer pins the default port.
|
|
850
|
+
*
|
|
851
|
+
* Wraps the pure decision in `peer-brand-detect.ts` with the fs reads of
|
|
852
|
+
* `~/.<peer>/.env`. Bias on read errors: if `.env` exists but is unreadable
|
|
853
|
+
* (permissions, transient I/O), the wrapper treats that peer as a *potential*
|
|
854
|
+
* dependency and short-circuits to the kept-active path. Disabling the system
|
|
855
|
+
* unit on faulty evidence would silently kill the peer's database (the exact
|
|
856
|
+
* failure Task 800 prevents); a conservatively-skipped disable is recoverable
|
|
857
|
+
* because the dedicated-unit bind check at the end of `setupDedicatedNeo4j`
|
|
858
|
+
* fails loud if the system unit is actually free.
|
|
859
|
+
*/
|
|
860
|
+
function peerBrandUsingSystemUnit() {
|
|
861
|
+
const home = process.env.HOME ?? "/root";
|
|
862
|
+
const peerEnvContents = [];
|
|
863
|
+
for (const hostname of KNOWN_BRAND_HOSTNAMES) {
|
|
864
|
+
if (hostname === BRAND.hostname) {
|
|
865
|
+
peerEnvContents.push([hostname, null]);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const envPath = resolve(home, `.${hostname}`, ".env");
|
|
869
|
+
if (!existsSync(envPath)) {
|
|
870
|
+
peerEnvContents.push([hostname, null]);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
peerEnvContents.push([hostname, readFileSync(envPath, "utf-8")]);
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
console.error(` WARNING: unable to read peer brand .env at ${envPath} — treating as potential dependency to avoid data loss (Task 800): ${err instanceof Error ? err.message : String(err)}`);
|
|
878
|
+
return hostname;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return findPeerBrandOnDefaultNeo4jPort({
|
|
882
|
+
currentBrandHostname: BRAND.hostname,
|
|
883
|
+
defaultNeo4jPort: DEFAULT_NEO4J_PORT,
|
|
884
|
+
peerEnvContents,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
844
887
|
/**
|
|
845
888
|
* Create a dedicated Neo4j instance for this brand when NEO4J_DEDICATED is true.
|
|
846
889
|
* Produces: separate config dir, data dir, log dir, systemd service, and password.
|
|
@@ -849,6 +892,10 @@ function installNeo4j() {
|
|
|
849
892
|
* dedicated, start, verify) so a half-installed Pi recovers in-place without
|
|
850
893
|
* manual systemctl. ensureNeo4jPassword() handles password verification on the
|
|
851
894
|
* recovery path.
|
|
895
|
+
*
|
|
896
|
+
* Task 800: on multi-brand hosts where a peer brand still depends on the apt
|
|
897
|
+
* `neo4j.service` (port 7687), the stop+disable step is skipped — disabling
|
|
898
|
+
* the system unit would kill the peer's database.
|
|
852
899
|
*/
|
|
853
900
|
function setupDedicatedNeo4j() {
|
|
854
901
|
if (!NEO4J_DEDICATED)
|
|
@@ -952,10 +999,22 @@ WantedBy=multi-user.target
|
|
|
952
999
|
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
|
|
953
1000
|
console.log(" [privileged] systemctl enable");
|
|
954
1001
|
shell("systemctl", ["enable", serviceName], { sudo: true });
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1002
|
+
// Task 800: skip stop+disable when a peer brand on this host still depends
|
|
1003
|
+
// on the apt `neo4j.service` (port 7687). Disabling it would kill the peer's
|
|
1004
|
+
// database — Task 797 reproducer on Neo's laptop. The kept-active path is
|
|
1005
|
+
// mutually exclusive with the disable path: exactly one log line per install.
|
|
1006
|
+
const peerOnSystemUnit = peerBrandUsingSystemUnit();
|
|
1007
|
+
if (peerOnSystemUnit !== null) {
|
|
1008
|
+
const keptActiveMsg = ` [neo4j] system unit kept active — peer brand ${peerOnSystemUnit} depends on port ${DEFAULT_NEO4J_PORT} (Task 800)`;
|
|
1009
|
+
console.log(keptActiveMsg);
|
|
1010
|
+
logFile(keptActiveMsg);
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
console.log(` [neo4j] disabling system unit (brand-dedicated active on port ${NEO4J_PORT})`);
|
|
1014
|
+
logFile(` [neo4j] disabling system unit (brand-dedicated active on port ${NEO4J_PORT})`);
|
|
1015
|
+
shell("systemctl", ["stop", "neo4j"], { sudo: true });
|
|
1016
|
+
shell("systemctl", ["disable", "neo4j"], { sudo: true });
|
|
1017
|
+
}
|
|
959
1018
|
console.log(` [neo4j] reset-failed ${serviceName} before start`);
|
|
960
1019
|
logFile(` [neo4j] reset-failed ${serviceName} before start`);
|
|
961
1020
|
shell("systemctl", ["reset-failed", serviceName], { sudo: true, bestEffort: true });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Task 800 — pure peer-brand detection. Extracted from index.ts so the
|
|
2
|
+
// "should we leave the system neo4j.service alone?" decision can be unit-
|
|
3
|
+
// tested with concrete `.env` fixtures, no fs reads. Mirrors the apt-resolve
|
|
4
|
+
// / port-resolution pattern: inputs in, decision out, no I/O.
|
|
5
|
+
//
|
|
6
|
+
// The installer's `setupDedicatedNeo4j()` wraps this with the fs reads of
|
|
7
|
+
// `~/.<peer>/.env` and the `console.error` warning on read failure. This
|
|
8
|
+
// module owns only the parsing + matching rule.
|
|
9
|
+
/**
|
|
10
|
+
* Decide whether any peer brand on the same host depends on the apt-installed
|
|
11
|
+
* `neo4j.service` (port `defaultNeo4jPort`, conventionally 7687). Returns the
|
|
12
|
+
* first matching peer brand's hostname, or `null` if no peer pins that port.
|
|
13
|
+
*
|
|
14
|
+
* `peerEnvContents` is an iterable of `[hostname, envContent | null]` pairs.
|
|
15
|
+
* `null` content means the peer's `.env` is absent or unreadable — treated
|
|
16
|
+
* the same as "no evidence of dependency". The wrapper logs the read failure;
|
|
17
|
+
* this module stays decision-only.
|
|
18
|
+
*
|
|
19
|
+
* Matching rule: a peer pins the default port iff its `.env` contains a line
|
|
20
|
+
* matching `NEO4J_URI=bolt://localhost:<defaultNeo4jPort>` exactly. Any other
|
|
21
|
+
* scheme, host, or port → not a match. Self-references (peer hostname equal
|
|
22
|
+
* to the current install's hostname) are silently skipped so the caller can
|
|
23
|
+
* pass the full known-brand list without filtering first.
|
|
24
|
+
*/
|
|
25
|
+
export function findPeerBrandOnDefaultNeo4jPort(args) {
|
|
26
|
+
const { currentBrandHostname, defaultNeo4jPort, peerEnvContents } = args;
|
|
27
|
+
const expected = `bolt://localhost:${defaultNeo4jPort}`;
|
|
28
|
+
for (const [hostname, content] of peerEnvContents) {
|
|
29
|
+
if (hostname === currentBrandHostname)
|
|
30
|
+
continue;
|
|
31
|
+
if (content === null)
|
|
32
|
+
continue;
|
|
33
|
+
const match = content.match(/^NEO4J_URI=(.+)$/m);
|
|
34
|
+
if (match && match[1].trim() === expected) {
|
|
35
|
+
return hostname;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cypher rewrite pass that injects provenance stamps into operator-supplied
|
|
3
|
+
* write_neo4j_cypher (Task 797). Sibling to cypher-validate.ts: validate first,
|
|
4
|
+
* rewrite second, run third.
|
|
5
|
+
*
|
|
6
|
+
* Doctrine context: Task 673 wrapped writers stamp every CREATE node
|
|
7
|
+
* structurally via writeNodeWithEdges; raw cypher (Task 796) relied on
|
|
8
|
+
* prompt-internalised discipline. This module closes the asymmetry — the
|
|
9
|
+
* rewriter appends `SET <alias>.createdAt = $__autoStartTimestamp,
|
|
10
|
+
* <alias>.createdByAgent = $__autoAgent, <alias>.createdByTool =
|
|
11
|
+
* 'graph-cypher-write', <alias>.createdBySession = $__autoSession` to every
|
|
12
|
+
* CREATE/MERGE clause that introduces a new labeled node. Caller supplies
|
|
13
|
+
* the three `$__auto*` params at tx.run time.
|
|
14
|
+
*
|
|
15
|
+
* MERGE handling preserves the create-vs-match distinction. CREATE always
|
|
16
|
+
* stamps (every CREATE introduces a new node by definition). MERGE injects
|
|
17
|
+
* `ON CREATE SET` so an existing-node match path keeps the original writer's
|
|
18
|
+
* provenance intact — overwriting it would destroy forensic attribution.
|
|
19
|
+
*
|
|
20
|
+
* Node-only stamping. Edge aliases (`[r:R]`) are not auto-stamped — matches
|
|
21
|
+
* the wrapped writer's writeNodeWithEdges, which stamps the node only.
|
|
22
|
+
*
|
|
23
|
+
* Idempotent: re-applying the rewriter to its own output is a no-op. The
|
|
24
|
+
* marker that proves "already stamped" is `<alias>.createdBySession =
|
|
25
|
+
* $__autoSession` — the unique combination of property name + auto param.
|
|
26
|
+
*
|
|
27
|
+
* String literals and comments are redacted (chars replaced with spaces,
|
|
28
|
+
* offsets preserved) so a `CREATE (fake:Foo)` inside a comment or quoted
|
|
29
|
+
* string cannot mistakenly trip the rewriter.
|
|
30
|
+
*/
|
|
31
|
+
export interface RewriteResult {
|
|
32
|
+
cypher: string;
|
|
33
|
+
/** Number of node aliases that received a fresh stamp injection. */
|
|
34
|
+
stampsAppended: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function rewriteWithProvenanceStamps(cypher: string): RewriteResult;
|
|
37
|
+
//# sourceMappingURL=cypher-rewrite-stamp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-rewrite-stamp.d.ts","sourceRoot":"","sources":["../src/cypher-rewrite-stamp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAC;CACxB;AAsPD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAmEzE"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cypher rewrite pass that injects provenance stamps into operator-supplied
|
|
4
|
+
* write_neo4j_cypher (Task 797). Sibling to cypher-validate.ts: validate first,
|
|
5
|
+
* rewrite second, run third.
|
|
6
|
+
*
|
|
7
|
+
* Doctrine context: Task 673 wrapped writers stamp every CREATE node
|
|
8
|
+
* structurally via writeNodeWithEdges; raw cypher (Task 796) relied on
|
|
9
|
+
* prompt-internalised discipline. This module closes the asymmetry — the
|
|
10
|
+
* rewriter appends `SET <alias>.createdAt = $__autoStartTimestamp,
|
|
11
|
+
* <alias>.createdByAgent = $__autoAgent, <alias>.createdByTool =
|
|
12
|
+
* 'graph-cypher-write', <alias>.createdBySession = $__autoSession` to every
|
|
13
|
+
* CREATE/MERGE clause that introduces a new labeled node. Caller supplies
|
|
14
|
+
* the three `$__auto*` params at tx.run time.
|
|
15
|
+
*
|
|
16
|
+
* MERGE handling preserves the create-vs-match distinction. CREATE always
|
|
17
|
+
* stamps (every CREATE introduces a new node by definition). MERGE injects
|
|
18
|
+
* `ON CREATE SET` so an existing-node match path keeps the original writer's
|
|
19
|
+
* provenance intact — overwriting it would destroy forensic attribution.
|
|
20
|
+
*
|
|
21
|
+
* Node-only stamping. Edge aliases (`[r:R]`) are not auto-stamped — matches
|
|
22
|
+
* the wrapped writer's writeNodeWithEdges, which stamps the node only.
|
|
23
|
+
*
|
|
24
|
+
* Idempotent: re-applying the rewriter to its own output is a no-op. The
|
|
25
|
+
* marker that proves "already stamped" is `<alias>.createdBySession =
|
|
26
|
+
* $__autoSession` — the unique combination of property name + auto param.
|
|
27
|
+
*
|
|
28
|
+
* String literals and comments are redacted (chars replaced with spaces,
|
|
29
|
+
* offsets preserved) so a `CREATE (fake:Foo)` inside a comment or quoted
|
|
30
|
+
* string cannot mistakenly trip the rewriter.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.rewriteWithProvenanceStamps = rewriteWithProvenanceStamps;
|
|
34
|
+
const AUTO_PARAM_AGENT = "$__autoAgent";
|
|
35
|
+
const AUTO_PARAM_SESSION = "$__autoSession";
|
|
36
|
+
const AUTO_PARAM_TIMESTAMP = "$__autoStartTimestamp";
|
|
37
|
+
const AUTO_TOOL_LITERAL = "'graph-cypher-write'";
|
|
38
|
+
const KEYWORD_TERMINATORS = new Set([
|
|
39
|
+
"SET",
|
|
40
|
+
"ON",
|
|
41
|
+
"MATCH",
|
|
42
|
+
"MERGE",
|
|
43
|
+
"CREATE",
|
|
44
|
+
"RETURN",
|
|
45
|
+
"WITH",
|
|
46
|
+
"WHERE",
|
|
47
|
+
"REMOVE",
|
|
48
|
+
"DELETE",
|
|
49
|
+
"DETACH",
|
|
50
|
+
"UNION",
|
|
51
|
+
"ORDER",
|
|
52
|
+
"LIMIT",
|
|
53
|
+
"SKIP",
|
|
54
|
+
"FOREACH",
|
|
55
|
+
"UNWIND",
|
|
56
|
+
"CALL",
|
|
57
|
+
"USE",
|
|
58
|
+
"LOAD",
|
|
59
|
+
"YIELD",
|
|
60
|
+
]);
|
|
61
|
+
function buildRedacted(cypher) {
|
|
62
|
+
let out = "";
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < cypher.length) {
|
|
65
|
+
const c = cypher[i];
|
|
66
|
+
if (c === "/" && cypher[i + 1] === "/") {
|
|
67
|
+
while (i < cypher.length && cypher[i] !== "\n") {
|
|
68
|
+
out += " ";
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === "/" && cypher[i + 1] === "*") {
|
|
74
|
+
out += " ";
|
|
75
|
+
i += 2;
|
|
76
|
+
while (i < cypher.length && !(cypher[i] === "*" && cypher[i + 1] === "/")) {
|
|
77
|
+
out += " ";
|
|
78
|
+
i++;
|
|
79
|
+
}
|
|
80
|
+
if (i < cypher.length) {
|
|
81
|
+
out += " ";
|
|
82
|
+
i += 2;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (c === "'" || c === '"') {
|
|
87
|
+
const quote = c;
|
|
88
|
+
out += c;
|
|
89
|
+
i++;
|
|
90
|
+
while (i < cypher.length && cypher[i] !== quote) {
|
|
91
|
+
if (cypher[i] === "\\" && i + 1 < cypher.length) {
|
|
92
|
+
out += " ";
|
|
93
|
+
i += 2;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
out += " ";
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
if (i < cypher.length) {
|
|
100
|
+
out += cypher[i];
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
out += c;
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function findKeywordHits(redacted) {
|
|
111
|
+
const re = /\b(CREATE|MERGE)\b/gi;
|
|
112
|
+
const results = [];
|
|
113
|
+
for (const m of redacted.matchAll(re)) {
|
|
114
|
+
if (m.index === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
const upper = m[1].toUpperCase();
|
|
117
|
+
// Skip `CREATE` that's part of `ON CREATE SET` (a MERGE sub-clause, not
|
|
118
|
+
// an introducing CREATE). Without this, the auto-stamp's own injected
|
|
119
|
+
// `ON CREATE SET` would register as a separate CREATE hit on the next
|
|
120
|
+
// rewriter pass, breaking idempotency by truncating the stamp window.
|
|
121
|
+
if (upper === "CREATE") {
|
|
122
|
+
const prefix = redacted.slice(Math.max(0, m.index - 8), m.index);
|
|
123
|
+
if (/\bON\s+$/i.test(prefix))
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
results.push({
|
|
127
|
+
kw: upper,
|
|
128
|
+
afterKw: m.index + m[0].length,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
const TERMINATOR_PROBE = /^(\s*)(?:;|([A-Za-z]+))/;
|
|
134
|
+
/**
|
|
135
|
+
* Returns true iff position `i` in `redacted` is at a word boundary — i.e.,
|
|
136
|
+
* the immediately preceding character is not part of an identifier (letters,
|
|
137
|
+
* digits, underscore). Without this guard, TERMINATOR_PROBE matches the
|
|
138
|
+
* trailing letters of a multi-char identifier as a keyword (e.g. `Session`
|
|
139
|
+
* ending in `on` would match the `ON` keyword and truncate the scan
|
|
140
|
+
* mid-identifier).
|
|
141
|
+
*/
|
|
142
|
+
function atWordBoundary(redacted, i) {
|
|
143
|
+
if (i === 0)
|
|
144
|
+
return true;
|
|
145
|
+
return !/[A-Za-z0-9_]/.test(redacted[i - 1]);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns the index just past the closing `)` of the CREATE/MERGE pattern,
|
|
149
|
+
* or null if no node pattern follows the keyword (e.g. CREATE INDEX, CREATE
|
|
150
|
+
* CONSTRAINT — neither would have reached the rewriter, but defensive).
|
|
151
|
+
*/
|
|
152
|
+
function findPatternEnd(redacted, afterKw) {
|
|
153
|
+
let i = afterKw;
|
|
154
|
+
while (i < redacted.length && /\s/.test(redacted[i]))
|
|
155
|
+
i++;
|
|
156
|
+
if (i >= redacted.length || redacted[i] !== "(")
|
|
157
|
+
return null;
|
|
158
|
+
let parenDepth = 0;
|
|
159
|
+
let bracketDepth = 0;
|
|
160
|
+
while (i < redacted.length) {
|
|
161
|
+
if (parenDepth === 0 && bracketDepth === 0 && i > afterKw) {
|
|
162
|
+
const tmatch = redacted.slice(i).match(TERMINATOR_PROBE);
|
|
163
|
+
if (tmatch) {
|
|
164
|
+
const wsLen = tmatch[1].length;
|
|
165
|
+
const ch = redacted[i + wsLen];
|
|
166
|
+
if (ch === ";")
|
|
167
|
+
return i + wsLen;
|
|
168
|
+
const word = tmatch[2];
|
|
169
|
+
if (word &&
|
|
170
|
+
KEYWORD_TERMINATORS.has(word.toUpperCase()) &&
|
|
171
|
+
atWordBoundary(redacted, i + wsLen)) {
|
|
172
|
+
return i + wsLen;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const c = redacted[i];
|
|
177
|
+
if (c === "(")
|
|
178
|
+
parenDepth++;
|
|
179
|
+
else if (c === ")")
|
|
180
|
+
parenDepth--;
|
|
181
|
+
else if (c === "[")
|
|
182
|
+
bracketDepth++;
|
|
183
|
+
else if (c === "]")
|
|
184
|
+
bracketDepth--;
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
return i;
|
|
188
|
+
}
|
|
189
|
+
function extractLabeledAliases(redactedSlice) {
|
|
190
|
+
const out = [];
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
const re = /\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*[A-Z]/g;
|
|
193
|
+
for (const m of redactedSlice.matchAll(re)) {
|
|
194
|
+
const alias = m[1];
|
|
195
|
+
if (!seen.has(alias)) {
|
|
196
|
+
seen.add(alias);
|
|
197
|
+
out.push(alias);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
function aliasAlreadyStamped(slice, alias) {
|
|
203
|
+
// Per-pattern scope (review F1). The `slice` covers this CREATE/MERGE
|
|
204
|
+
// pattern's possible SET / ON CREATE SET clauses up to the next CREATE/
|
|
205
|
+
// MERGE boundary — scoping the marker check globally would mis-classify
|
|
206
|
+
// a re-introduction of the same alias name across two distinct patterns
|
|
207
|
+
// as "already stamped" (the first pattern's stamp would shadow the second).
|
|
208
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
209
|
+
const marker = new RegExp(`\\b${escaped}\\.createdBySession\\s*=\\s*\\$__autoSession\\b`);
|
|
210
|
+
return marker.test(slice);
|
|
211
|
+
}
|
|
212
|
+
function buildAssignments(aliases) {
|
|
213
|
+
return aliases
|
|
214
|
+
.flatMap((a) => [
|
|
215
|
+
`${a}.createdAt = ${AUTO_PARAM_TIMESTAMP}`,
|
|
216
|
+
`${a}.createdByAgent = ${AUTO_PARAM_AGENT}`,
|
|
217
|
+
`${a}.createdByTool = ${AUTO_TOOL_LITERAL}`,
|
|
218
|
+
`${a}.createdBySession = ${AUTO_PARAM_SESSION}`,
|
|
219
|
+
])
|
|
220
|
+
.join(", ");
|
|
221
|
+
}
|
|
222
|
+
const ON_CREATE_SET_PROBE = /^ON\s+CREATE\s+SET\s+/i;
|
|
223
|
+
const ON_MATCH_SET_PROBE = /^ON\s+MATCH\s+SET\b/i;
|
|
224
|
+
function findOnCreateSetEnd(redacted, fromIdx) {
|
|
225
|
+
let i = fromIdx;
|
|
226
|
+
while (i < redacted.length && /\s/.test(redacted[i]))
|
|
227
|
+
i++;
|
|
228
|
+
const m = redacted.slice(i).match(ON_CREATE_SET_PROBE);
|
|
229
|
+
if (!m)
|
|
230
|
+
return null;
|
|
231
|
+
let j = i + m[0].length;
|
|
232
|
+
let parenDepth = 0;
|
|
233
|
+
let bracketDepth = 0;
|
|
234
|
+
while (j < redacted.length) {
|
|
235
|
+
if (parenDepth === 0 && bracketDepth === 0) {
|
|
236
|
+
const tmatch = redacted.slice(j).match(TERMINATOR_PROBE);
|
|
237
|
+
if (tmatch) {
|
|
238
|
+
const wsLen = tmatch[1].length;
|
|
239
|
+
const ch = redacted[j + wsLen];
|
|
240
|
+
if (ch === ";")
|
|
241
|
+
return { end: j + wsLen };
|
|
242
|
+
const word = tmatch[2];
|
|
243
|
+
if (word &&
|
|
244
|
+
KEYWORD_TERMINATORS.has(word.toUpperCase()) &&
|
|
245
|
+
atWordBoundary(redacted, j + wsLen)) {
|
|
246
|
+
return { end: j + wsLen };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const c = redacted[j];
|
|
251
|
+
if (c === "(")
|
|
252
|
+
parenDepth++;
|
|
253
|
+
else if (c === ")")
|
|
254
|
+
parenDepth--;
|
|
255
|
+
else if (c === "[")
|
|
256
|
+
bracketDepth++;
|
|
257
|
+
else if (c === "]")
|
|
258
|
+
bracketDepth--;
|
|
259
|
+
j++;
|
|
260
|
+
}
|
|
261
|
+
return { end: j };
|
|
262
|
+
}
|
|
263
|
+
function findOnMatchSetStart(redacted, fromIdx) {
|
|
264
|
+
let i = fromIdx;
|
|
265
|
+
while (i < redacted.length && /\s/.test(redacted[i]))
|
|
266
|
+
i++;
|
|
267
|
+
if (ON_MATCH_SET_PROBE.test(redacted.slice(i))) {
|
|
268
|
+
return i;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function rewriteWithProvenanceStamps(cypher) {
|
|
273
|
+
const redacted = buildRedacted(cypher);
|
|
274
|
+
const hits = findKeywordHits(redacted);
|
|
275
|
+
const edits = [];
|
|
276
|
+
let stampsAppended = 0;
|
|
277
|
+
for (let h = 0; h < hits.length; h++) {
|
|
278
|
+
const { kw, afterKw } = hits[h];
|
|
279
|
+
const patternEnd = findPatternEnd(redacted, afterKw);
|
|
280
|
+
if (patternEnd === null)
|
|
281
|
+
continue;
|
|
282
|
+
let pStart = afterKw;
|
|
283
|
+
while (pStart < redacted.length && /\s/.test(redacted[pStart]))
|
|
284
|
+
pStart++;
|
|
285
|
+
const patternSlice = redacted.slice(pStart, patternEnd);
|
|
286
|
+
const labeled = extractLabeledAliases(patternSlice);
|
|
287
|
+
// Per-pattern stamp window: from this pattern's start through the next
|
|
288
|
+
// CREATE/MERGE boundary (or end of cypher). Scoping the "already stamped"
|
|
289
|
+
// check globally would mis-classify a re-introduction of the same alias
|
|
290
|
+
// name across two distinct patterns (review F1).
|
|
291
|
+
const nextHitAfterKw = h + 1 < hits.length ? hits[h + 1].afterKw : redacted.length;
|
|
292
|
+
const stampWindow = redacted.slice(pStart, nextHitAfterKw);
|
|
293
|
+
const aliases = labeled.filter((a) => !aliasAlreadyStamped(stampWindow, a));
|
|
294
|
+
if (aliases.length === 0)
|
|
295
|
+
continue;
|
|
296
|
+
if (kw === "CREATE") {
|
|
297
|
+
// Trailing space matters: when patternEnd lands at a clause keyword
|
|
298
|
+
// (e.g. ` WITH` immediately after `(n:Person)`), our `findPatternEnd`
|
|
299
|
+
// returns the keyword's position. Without a trailing space, the
|
|
300
|
+
// injected `$__autoSession` runs into the keyword and Cypher parses
|
|
301
|
+
// `$__autoSessionWITH` as one parameter name.
|
|
302
|
+
edits.push({
|
|
303
|
+
at: patternEnd,
|
|
304
|
+
insert: ` SET ${buildAssignments(aliases)} `,
|
|
305
|
+
});
|
|
306
|
+
stampsAppended += aliases.length;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const existingOnCreate = findOnCreateSetEnd(redacted, patternEnd);
|
|
310
|
+
if (existingOnCreate) {
|
|
311
|
+
edits.push({
|
|
312
|
+
at: existingOnCreate.end,
|
|
313
|
+
insert: `, ${buildAssignments(aliases)} `,
|
|
314
|
+
});
|
|
315
|
+
stampsAppended += aliases.length;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const onMatchStart = findOnMatchSetStart(redacted, patternEnd);
|
|
319
|
+
const insertAt = onMatchStart ?? patternEnd;
|
|
320
|
+
edits.push({
|
|
321
|
+
at: insertAt,
|
|
322
|
+
insert: ` ON CREATE SET ${buildAssignments(aliases)} `,
|
|
323
|
+
});
|
|
324
|
+
stampsAppended += aliases.length;
|
|
325
|
+
}
|
|
326
|
+
edits.sort((a, b) => b.at - a.at);
|
|
327
|
+
let result = cypher;
|
|
328
|
+
for (const edit of edits) {
|
|
329
|
+
result = result.slice(0, edit.at) + edit.insert + result.slice(edit.at);
|
|
330
|
+
}
|
|
331
|
+
return { cypher: result, stampsAppended };
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=cypher-rewrite-stamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-rewrite-stamp.js","sourceRoot":"","sources":["../src/cypher-rewrite-stamp.ts"],"names":[],"mappings":"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"}
|