@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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts +2 -0
  3. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts.map +1 -0
  4. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js +97 -0
  5. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js.map +1 -0
  6. package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts +13 -1
  7. package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts.map +1 -1
  8. package/payload/platform/lib/graph-mcp/dist/cypher-validate.js +70 -3
  9. package/payload/platform/lib/graph-mcp/dist/cypher-validate.js.map +1 -1
  10. package/payload/platform/lib/graph-mcp/dist/index.js +154 -11
  11. package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
  12. package/payload/platform/lib/graph-mcp/src/__tests__/cypher-validate-write.test.ts +150 -0
  13. package/payload/platform/lib/graph-mcp/src/cypher-validate.ts +95 -3
  14. package/payload/platform/lib/graph-mcp/src/index.ts +202 -17
  15. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts +2 -0
  16. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts.map +1 -0
  17. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js +147 -0
  18. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js.map +1 -0
  19. package/payload/platform/lib/graph-write/dist/audit.d.ts +84 -0
  20. package/payload/platform/lib/graph-write/dist/audit.d.ts.map +1 -0
  21. package/payload/platform/lib/graph-write/dist/audit.js +129 -0
  22. package/payload/platform/lib/graph-write/dist/audit.js.map +1 -0
  23. package/payload/platform/lib/graph-write/dist/index.d.ts +1 -0
  24. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  25. package/payload/platform/lib/graph-write/dist/index.js +18 -22
  26. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  27. package/payload/platform/lib/graph-write/src/__tests__/audit.test.ts +162 -0
  28. package/payload/platform/lib/graph-write/src/audit.ts +182 -0
  29. package/payload/platform/lib/graph-write/src/index.ts +5 -0
  30. package/payload/platform/package.json +2 -2
  31. package/payload/platform/plugins/docs/references/memory-guide.md +2 -0
  32. package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
  33. package/payload/platform/templates/specialists/agents/database-operator.md +39 -6
  34. package/payload/server/chunk-2T4RRIJK.js +9462 -0
  35. package/payload/server/maxy-edge.js +94 -16
  36. 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-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/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-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json",
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. Copy missing properties from the duplicate onto the survivor via `memory-update`.
152
- 4. Re-point every edge from the duplicate onto the survivor via `memory-update` (or a targeted `memory-write` with the new relationship and a follow-up `memory-delete` of the old one).
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** — `memory-write` with a `relationships` payload naming the exact edge type from the schema. Validator rejects unknown edge types; re-read the schema before authoring.
158
- - **Label normalisation** — call `memory-update` with the new label set. Never rename a label across the whole graph without a named owner asking for it label renames are schema changes.
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
  ---