@rubytech/create-maxy 1.0.743 → 1.0.744
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/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts +2 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js +97 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts +13 -1
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js +70 -3
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.js +154 -11
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/__tests__/cypher-validate-write.test.ts +150 -0
- package/payload/platform/lib/graph-mcp/src/cypher-validate.ts +95 -3
- package/payload/platform/lib/graph-mcp/src/index.ts +202 -17
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts +2 -0
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js +147 -0
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js.map +1 -0
- package/payload/platform/lib/graph-write/dist/audit.d.ts +84 -0
- package/payload/platform/lib/graph-write/dist/audit.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/audit.js +129 -0
- package/payload/platform/lib/graph-write/dist/audit.js.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +1 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +18 -22
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/__tests__/audit.test.ts +162 -0
- package/payload/platform/lib/graph-write/src/audit.ts +182 -0
- package/payload/platform/lib/graph-write/src/index.ts +5 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/docs/references/memory-guide.md +2 -0
- package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +39 -6
- package/payload/server/chunk-2T4RRIJK.js +9462 -0
- package/payload/server/maxy-edge.js +94 -16
- package/payload/server/server.js +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-write audit for the database-operator's raw `write_neo4j_cypher` tool
|
|
3
|
+
* (Task 796). Sibling to `[graph-write] accepted` for the wrapped writers
|
|
4
|
+
* (`memory-write`, `contact-create`, etc. — Task 673) — same module so the
|
|
5
|
+
* `[graph-*]` log family stays uniform across both write surfaces.
|
|
6
|
+
*
|
|
7
|
+
* Design posture:
|
|
8
|
+
* - Static parse first. Pre-flight regex on the cypher text catches missing
|
|
9
|
+
* provenance stamps and unknown edge types cheaply, no driver round-trip.
|
|
10
|
+
* - Dynamic orphan detection is the caller's responsibility — the graph-mcp
|
|
11
|
+
* shim runs the detection query against Neo4j after the upstream commits
|
|
12
|
+
* and passes the resulting elementId list as `orphanIds`. Keeping the
|
|
13
|
+
* module pure (no neo4j-driver import) makes it unit-testable without a
|
|
14
|
+
* live database.
|
|
15
|
+
* - Audit is observational. Warnings never block the write. The
|
|
16
|
+
* prompt-level Graph Stewardship Doctrine in
|
|
17
|
+
* [database-operator.md](../../../templates/specialists/agents/database-operator.md)
|
|
18
|
+
* names the discipline; the audit is the verification stream.
|
|
19
|
+
*
|
|
20
|
+
* String literals are stripped before pattern checks so that property values
|
|
21
|
+
* containing words like `'createdByAgent in my bio'` cannot trip a false
|
|
22
|
+
* provenance count, and edge-like patterns inside quoted strings cannot
|
|
23
|
+
* trip a false unknown-type warning. The regex shape is duplicated from
|
|
24
|
+
* [cypher-validate.ts](../../graph-mcp/src/cypher-validate.ts) intentionally:
|
|
25
|
+
* graph-write does not depend on graph-mcp, and a one-line regex helper
|
|
26
|
+
* doesn't justify a third package. Drift in either copy is a regression
|
|
27
|
+
* surfaced by both the validator tests and the audit tests.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface SchemaSnapshot {
|
|
31
|
+
readonly labels: ReadonlySet<string>;
|
|
32
|
+
readonly relationshipTypes: ReadonlySet<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CypherWriteAuditInput {
|
|
36
|
+
cypher: string;
|
|
37
|
+
schema: SchemaSnapshot;
|
|
38
|
+
agentName: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
/** Counter from upstream response — drives missing-provenance arithmetic. */
|
|
41
|
+
nodesCreated: number;
|
|
42
|
+
/** Counter from upstream response — informational only. */
|
|
43
|
+
relsCreated: number;
|
|
44
|
+
/**
|
|
45
|
+
* elementIds of nodes the post-write orphan query identified. Caller scopes
|
|
46
|
+
* the query to (createdBySession, createdAt >= writeStartTimestamp) so this
|
|
47
|
+
* list cannot include nodes from prior writes in the same session. Empty
|
|
48
|
+
* array = no orphans (or no scope to check).
|
|
49
|
+
*/
|
|
50
|
+
orphanIds: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AuditWarning =
|
|
54
|
+
| { kind: "orphan-warning"; orphanIds: string[] }
|
|
55
|
+
| { kind: "unknown-type-warning"; type: string }
|
|
56
|
+
| { kind: "missing-provenance-warning"; created: number; stamped: number };
|
|
57
|
+
|
|
58
|
+
export type AuditLine =
|
|
59
|
+
| {
|
|
60
|
+
kind: "accepted";
|
|
61
|
+
cypherPrefix: string;
|
|
62
|
+
nodesCreated: number;
|
|
63
|
+
relsCreated: number;
|
|
64
|
+
agentName: string;
|
|
65
|
+
sessionId: string;
|
|
66
|
+
}
|
|
67
|
+
| { kind: "orphan-warning"; cypherPrefix: string; orphanIds: string[] }
|
|
68
|
+
| { kind: "unknown-type-warning"; cypherPrefix: string; type: string }
|
|
69
|
+
| {
|
|
70
|
+
kind: "missing-provenance-warning";
|
|
71
|
+
cypherPrefix: string;
|
|
72
|
+
created: number;
|
|
73
|
+
stamped: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Mirrors cypher-validate's pattern. Captures TYPE(|TYPE)* alternation; the
|
|
77
|
+
// audit splits on `|` to enumerate every referenced type.
|
|
78
|
+
const EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
|
|
79
|
+
|
|
80
|
+
// Counts CREATE (n:Label) and MERGE (n:Label) statements that introduce a
|
|
81
|
+
// node. The trailing `[A-Z]` requires at least one label (so `CREATE INDEX`
|
|
82
|
+
// and bare `CREATE (a)-[:R]->(b)` shapes don't trip this — bare patterns
|
|
83
|
+
// reference a previously-MATCHed alias and don't introduce new nodes).
|
|
84
|
+
const CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
|
|
85
|
+
|
|
86
|
+
// Stamp tokens — at least one of these MUST appear per created node for the
|
|
87
|
+
// write to satisfy provenance discipline. The audit counts occurrences;
|
|
88
|
+
// stamp-count >= node-count is the pass criterion.
|
|
89
|
+
const PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
|
|
90
|
+
|
|
91
|
+
function stripStringLiterals(cypher: string): string {
|
|
92
|
+
// Identical regex to cypher-validate.ts. Replaces single- and double-
|
|
93
|
+
// quoted literals with empty quotes so e.g. 'CREATE (n:Person)' inside a
|
|
94
|
+
// property value cannot inflate the create-count.
|
|
95
|
+
return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractEdgeTypes(cleaned: string): Set<string> {
|
|
99
|
+
const out = new Set<string>();
|
|
100
|
+
// Use String.matchAll for stateless iteration — no shared regex lastIndex.
|
|
101
|
+
for (const m of cleaned.matchAll(EDGE_PATTERN)) {
|
|
102
|
+
for (const t of m[1].split("|")) {
|
|
103
|
+
const clean = t.trim();
|
|
104
|
+
if (clean) out.add(clean);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function countCreateOrMergeNodes(cleaned: string): number {
|
|
111
|
+
const matches = cleaned.match(CREATE_OR_MERGE_NODE);
|
|
112
|
+
return matches ? matches.length : 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function countProvenanceStamps(cleaned: string): number {
|
|
116
|
+
const matches = cleaned.match(PROVENANCE_TOKEN);
|
|
117
|
+
return matches ? matches.length : 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function auditCypherWrite(input: CypherWriteAuditInput): AuditWarning[] {
|
|
121
|
+
const warnings: AuditWarning[] = [];
|
|
122
|
+
const cleaned = stripStringLiterals(input.cypher);
|
|
123
|
+
|
|
124
|
+
// 1. Unknown edge type — cypher names a type not in the live ontology.
|
|
125
|
+
// Enumerate every referenced type; emit one warning per unknown.
|
|
126
|
+
const referencedTypes = extractEdgeTypes(cleaned);
|
|
127
|
+
for (const t of referencedTypes) {
|
|
128
|
+
if (!input.schema.relationshipTypes.has(t)) {
|
|
129
|
+
warnings.push({ kind: "unknown-type-warning", type: t });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Missing provenance — count CREATE/MERGE-with-label statements vs
|
|
134
|
+
// `createdBy*` token occurrences. Stamps may live in inline maps
|
|
135
|
+
// (`{createdByAgent: $agent}`) or SET clauses; both shapes contribute
|
|
136
|
+
// to the token count. The arithmetic is precision-imprecise — a
|
|
137
|
+
// multi-stamp single CREATE inflates `stamped`, a multi-CREATE single
|
|
138
|
+
// SET undercounts — but the directional signal (zero stamps for many
|
|
139
|
+
// creates) catches the failure mode this audit exists to surface.
|
|
140
|
+
//
|
|
141
|
+
// nodesCreated=0 short-circuits the check: the upstream counter is
|
|
142
|
+
// ground truth for whether any node was actually persisted. An
|
|
143
|
+
// idempotent MERGE that matched an existing node has a static
|
|
144
|
+
// create-count of 1 but contributed no new node — no provenance was
|
|
145
|
+
// needed, and warning here would be a false positive.
|
|
146
|
+
if (input.nodesCreated > 0) {
|
|
147
|
+
const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
|
|
148
|
+
if (createOrMergeNodes > 0) {
|
|
149
|
+
const stamps = countProvenanceStamps(cleaned);
|
|
150
|
+
if (stamps < createOrMergeNodes) {
|
|
151
|
+
warnings.push({
|
|
152
|
+
kind: "missing-provenance-warning",
|
|
153
|
+
created: createOrMergeNodes,
|
|
154
|
+
stamped: stamps,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 3. Orphans — caller-supplied. The dynamic detection happens at the
|
|
161
|
+
// graph-mcp shim layer (which has the Neo4j driver); the audit module
|
|
162
|
+
// only renders the warning shape.
|
|
163
|
+
if (input.orphanIds.length > 0) {
|
|
164
|
+
warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return warnings;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function formatAuditLine(line: AuditLine): string {
|
|
171
|
+
const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
|
|
172
|
+
switch (line.kind) {
|
|
173
|
+
case "accepted":
|
|
174
|
+
return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
|
|
175
|
+
case "orphan-warning":
|
|
176
|
+
return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
|
|
177
|
+
case "unknown-type-warning":
|
|
178
|
+
return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
|
|
179
|
+
case "missing-provenance-warning":
|
|
180
|
+
return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Task 796: re-export the post-write audit so consumers (graph-mcp shim)
|
|
2
|
+
// import from the same package as `writeNodeWithEdges`. Keeps the
|
|
3
|
+
// `[graph-*]` log family colocated.
|
|
4
|
+
export * from "./audit.js";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Write doctrine (Task 673): a node without at least one adjacency is noise,
|
|
3
8
|
* not knowledge. `writeNodeWithEdges` is the single primitive every graph
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -106,6 +106,8 @@ Every new node in {{productName}}'s graph is created with at least one connectio
|
|
|
106
106
|
|
|
107
107
|
Every node also carries a provenance stamp — which agent wrote it, in which session, via which tool. You never see these fields, but they are how operators trace unusual growth back to the code path that produced it, and why your graph stays clean over time.
|
|
108
108
|
|
|
109
|
+
**Two write surfaces, one substrate.** General agents write through schema-aware helpers — {{productName}} can record a new contact, a new commitment, a new preference without ever typing a database query, and the helper enforces the connection-and-provenance rule above structurally. The graph-steward role (the specialist {{productName}} dispatches when you ask for graph hygiene — "merge those two duplicate contacts," "wire those four tasks to the meeting," "rename the legacy label across the graph") additionally has a raw Cypher write tool for the multi-step operations the helpers cannot express. The steward role internalises the same connection-and-provenance discipline in its prompt; a post-write audit emits a warning on every breach so the same rules apply to both surfaces. Both paths feed the same hourly orphan trend and the same forensic provenance fields — read-side, you cannot tell the two apart, and that is the point.
|
|
110
|
+
|
|
109
111
|
## Privacy
|
|
110
112
|
|
|
111
113
|
All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.
|
|
@@ -121,6 +121,22 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
+
## "Bad Gateway" or holding page during an upgrade
|
|
125
|
+
|
|
126
|
+
Task 795 — `maxy-edge.service` (always-on front door) classifies upstream errors and serves a brand-aware response. There are two distinct user-visible shapes; the right one depends on what failed.
|
|
127
|
+
|
|
128
|
+
**Branded holding page ("{{productName}} is starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds.
|
|
129
|
+
|
|
130
|
+
**Branded plain-text 502 ("Bad Gateway ({{productName}} unavailable)") — real upstream failure, not cold-start.** Any error class other than `ECONNREFUSED` (timeouts, resets, host-unreachable) returns the existing 502 path. The diagnostic line carries `err-class=other`. Read the log with `tail -200 ~/.maxy/logs/edge.log | rg 'err-class=other'` and check `~/.maxy/logs/server.log` for upstream stack traces — the upstream itself is the source.
|
|
131
|
+
|
|
132
|
+
**Continuous `err-class=econnrefused-coldstart` for >30 s past the last `[edge] listening` line** indicates the upstream never binds — the upgrade or boot has stalled. Recover via `sudo systemctl --user status maxy.service` and check the action runner log per the next section. Permanent-failure UI escalation (turning the holding page into an error after N seconds) is intentionally deferred.
|
|
133
|
+
|
|
134
|
+
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running pre-Task-795 code — re-bundle and re-publish.
|
|
135
|
+
|
|
136
|
+
**Verifying the holding page locally:** `curl -sS -H 'Accept: text/html' http://127.0.0.1:<EDGE_PORT>/` while `maxy.service` is stopped should return HTML containing the brand `productName`. The `Accept: text/html` header is required — non-html clients (default `curl`, `fetch()`, XHR) get the branded plain-text 502 instead, so the holding page's own `/api/health` polls don't break themselves during cold-start.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
124
140
|
## Action runner — upgrade or Cloudflare setup appears stuck
|
|
125
141
|
|
|
126
142
|
Task 664 replaced the ttyd/xterm admin terminal with a detached action runner. Upgrades and Cloudflare setup now run under transient `systemd-run --user` units whose stdout+stderr land in a persisted per-action log, streamed to the browser via SSE. Task 666 moved the four routes that serve the modal (`/api/admin/actions/*`, `/api/admin/version`) onto `maxy-edge.service`, so the log panel's stream survives a mid-run restart of `maxy.service` without reconnecting.
|
|
@@ -3,7 +3,7 @@ name: database-operator
|
|
|
3
3
|
description: "Document and archive ingestion and ad-hoc graph operations — running the universal `document-ingest` skill for any unstructured document (PDF, text, transcript, web page, audio, video) and per-source archive-import skills (LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels). Delegate when the operator uploads any document, drops an archive directory into chat, or asks for any graph operation that is not a routine per-turn write."
|
|
4
4
|
summary: "Ingests every unstructured document and external archive into your graph (LinkedIn today; other CRM sources in future) and handles ad-hoc graph tidy-ups on request. For example, when you upload a CV, a pricing guide, or a contract; when you drop a LinkedIn export folder into chat; or when you ask to prune orphan nodes, merge duplicate people, or add edges between entities."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
|
-
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
6
|
+
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-write_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Database Operator
|
|
@@ -124,6 +124,39 @@ Future CRM-type seed plugins (HubSpot, Salesforce, Pipedrive, iCloud contacts, G
|
|
|
124
124
|
|
|
125
125
|
When the admin delegates a graph operation — pruning, dedup, edge addition, label normalisation, schema-drift tidy — follow this discipline.
|
|
126
126
|
|
|
127
|
+
You wield two write surfaces. Wrapped writers (`memory-write`, `memory-update`, `memory-delete`, `memory-find-candidates`, `contact-create`, etc.) are schema-aware helpers that enforce orphan-prevention and provenance structurally — the right surface for single-node creates, contact ingestion, soft-delete sweeps. Raw Cypher via `mcp__graph__maxy-graph-write_neo4j_cypher` is the right surface for the writes wrapped helpers cannot express: edges between two pre-existing nodes, multi-statement hygiene (`DETACH DELETE` orphans, `apoc.refactor.mergeNodes` duplicates, `REMOVE :OldLabel SET :NewLabel`), and any operation that scopes a transaction across multiple matched node sets. The graph is the account's knowledge substrate; raw Cypher is power tooling for the role responsible for that substrate.
|
|
128
|
+
|
|
129
|
+
### Graph stewardship doctrine — raw Cypher writes
|
|
130
|
+
|
|
131
|
+
Four rules govern every raw Cypher write. The wrapped writers enforce these structurally for general agents; you internalise them because your write surface has no equivalent gate. The post-write `[graph-cypher-write]` audit emits a warning per discipline breach (`unknown-type-warning`, `missing-provenance-warning`, `orphan-warning`), but the audit is observational — discipline is yours, not the audit's.
|
|
132
|
+
|
|
133
|
+
**1. Every node has ≥1 typed edge in the same transaction.** A bare `CREATE (n:Person {name: 'Ian'})` is data, not knowledge — retrieval finds the node but it carries no context. Anchor every node creation to the relationship that explains it: `MATCH (anchor) WHERE … CREATE (anchor)-[:TYPE]->(n:Label {…})`, or `MERGE` the node and its edge in the same statement. *Why:* a graph that accumulates islands defeats relationship-traversal queries — the value of the graph is in the edges, not the rows.
|
|
134
|
+
|
|
135
|
+
**2. Every edge type is in the live ontology.** Inventing types fragments retrieval — `KNOWS` ≠ `knows` ≠ `HAS_KNOWN`. Call `mcp__graph__maxy-graph-get_neo4j_schema` before authoring any write whose edge type you are not certain about; if no fitting type exists, stop and ask the admin agent for ontology guidance — never coin a synonym. *Why:* edge typology compounds over time. A synonym today blocks every future query that expected the canonical type, and the only fix is a label-rewrite Cypher pass that touches the same edge from both sides.
|
|
136
|
+
|
|
137
|
+
**3. Every write carries provenance.** Stamp the same flattened fields the wrapped writers stamp on every node and every relationship the transaction creates:
|
|
138
|
+
|
|
139
|
+
```cypher
|
|
140
|
+
SET n.createdAt = datetime(),
|
|
141
|
+
n.createdByAgent = 'database-operator',
|
|
142
|
+
n.createdByTool = 'graph-cypher-write',
|
|
143
|
+
n.createdBySession = $sessionId
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Apply the same SET to relationships (`SET r....`). The session id is the conversation correlation key; the admin dispatch passes it as a parameter. *Why:* a write without provenance cannot be attributed back to the dispatch that produced it. The post-write audit emits `missing-provenance-warning created=<n> stamped=<m>` when the static parser counts CREATE/MERGE statements that exceed the count of `createdBy*` tokens — this is the directional signal, not a structural gate.
|
|
147
|
+
|
|
148
|
+
**4. Orphan prevention is your first-class duty.** Multi-statement transactions self-audit before completing. Capture the elementIds of every node the transaction creates, then run the orphan check inline:
|
|
149
|
+
|
|
150
|
+
```cypher
|
|
151
|
+
WITH collect(elementId(n)) AS writtenIds
|
|
152
|
+
MATCH (n) WHERE elementId(n) IN writtenIds AND NOT (n)--()
|
|
153
|
+
RETURN count(n) AS orphans
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
If `orphans > 0`, do not commit a "fix it up later" transaction — roll back, anchor the unattached nodes to their semantic parent, and re-run. *Why:* the hourly `[graph-health] orphans total=<N>` signal is your scorecard. A transaction that leaves orphans is a regression the role owns; the operator's stewardship is the primary defense against landfill.
|
|
157
|
+
|
|
158
|
+
The four rules together replace the LOUD-FAIL improvisation pattern that prior versions of this prompt prescribed when a wrapped writer lacked an edge-between-existing-nodes path. You no longer loud-fail on missing graph-write tools — you have them. You loud-fail on credentials, on out-of-surface tools (a skill prescribing a non-graph MCP token you do not hold), and on dispatched skills whose prerequisites are unmet — exactly as the LOUD-FAIL prerogative names. Discipline failures within the graph-write surface produce audit warnings, not blockers; the role's prompt-internalised doctrine is the contract.
|
|
159
|
+
|
|
127
160
|
### Before writing any Cypher
|
|
128
161
|
|
|
129
162
|
1. **Consult the SCHEMA block.** Your MCP tool set includes `maxy-graph-get_neo4j_schema` for live schema snapshots. Call it before authoring any write Cypher you are not certain about. The upstream cypher-mcp validator rejects writes with unknown labels or edges; catch the mismatch upfront, not at the rejection.
|
|
@@ -148,14 +181,14 @@ When two nodes represent the same real-world entity (two `:Person` rows for the
|
|
|
148
181
|
|
|
149
182
|
1. Read both via `memory-search` or direct `maxy-graph-read_neo4j_cypher`.
|
|
150
183
|
2. Decide which is the survivor (usually the older `createdAt`, or the one with richer properties). State the choice.
|
|
151
|
-
3.
|
|
152
|
-
4.
|
|
153
|
-
5. `memory-delete` the duplicate with a single-node call — the operator named this merge explicitly, no filter-token needed.
|
|
184
|
+
3. Property reconciliation: `memory-update` for property copies, or `write_neo4j_cypher` with `apoc.refactor.mergeNodes([survivor, duplicate])` when the merge is multi-edge (apoc reparents every relationship onto the survivor in one transaction). Stamp `mergedAt`, `mergedFromAgent`, `mergedFromSession` per the stewardship doctrine.
|
|
185
|
+
4. For property-only merges that leave edges unchanged, follow up with `memory-update` on the survivor and `memory-delete` on the duplicate. The duplicate delete is single-node, no filter-token needed (the operator named this merge explicitly).
|
|
154
186
|
|
|
155
187
|
### Edge addition, label normalisation, prune-denylist
|
|
156
188
|
|
|
157
|
-
- **Edge addition** — `
|
|
158
|
-
- **
|
|
189
|
+
- **Edge addition between two existing nodes** — raw Cypher via `mcp__graph__maxy-graph-write_neo4j_cypher`: `MATCH (a:LabelA {…}), (b:LabelB {…}) MERGE (a)-[r:TYPE]->(b) SET r.createdAt = datetime(), r.createdByAgent = $agent, r.createdByTool = 'graph-cypher-write', r.createdBySession = $sessionId`. The wrapped `memory-write` requires a new node payload — for edge-only writes, raw Cypher is the surface.
|
|
190
|
+
- **Edge addition that creates a new node** — `memory-write` with a `relationships` payload naming the exact edge type. Validator rejects unknown edge types structurally; re-read the schema before authoring.
|
|
191
|
+
- **Label normalisation** — `write_neo4j_cypher` with `MATCH (n:OldLabel) WHERE … REMOVE n:OldLabel SET n:NewLabel SET n.relabelledAt = datetime(), n.relabelledByAgent = $agent`. Bulk renames are schema changes — always confirm the owner / scope with the admin before executing across the whole graph.
|
|
159
192
|
- **Prune denylist** — when the operator wants to preserve specific nodes from future prune passes (current or scheduled-autonomous), use `graph-prune-denylist-add/remove/list` to manage the registry.
|
|
160
193
|
|
|
161
194
|
---
|