@rubytech/create-realagent 1.0.838 → 1.0.840

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 (80) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-write/dist/index.js +1 -1
  3. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  4. package/payload/platform/lib/graph-write/src/index.ts +1 -1
  5. package/payload/platform/plugins/admin/PLUGIN.md +2 -0
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +5 -5
  9. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +11 -11
  10. package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +2 -0
  11. package/payload/platform/plugins/contacts/mcp/dist/index.js +1 -1
  12. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/docs/references/internals.md +1 -1
  14. package/payload/platform/plugins/docs/references/platform.md +1 -1
  15. package/payload/platform/plugins/docs/references/troubleshooting.md +20 -0
  16. package/payload/platform/plugins/memory/PLUGIN.md +3 -3
  17. package/payload/platform/plugins/memory/mcp/dist/index.js +14 -14
  18. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  19. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +12 -11
  20. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +22 -10
  22. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/lib/uuid.js +7 -7
  24. package/payload/platform/plugins/memory/mcp/dist/lib/uuid.js.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  27. package/payload/platform/plugins/memory/references/schema-base.md +17 -17
  28. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +14 -14
  29. package/payload/platform/plugins/tasks/PLUGIN.md +2 -2
  30. package/payload/platform/plugins/tasks/mcp/dist/index.js +11 -11
  31. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  32. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js +1 -1
  33. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js.map +1 -1
  34. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  35. package/payload/platform/scripts/seed-neo4j.sh +13 -3
  36. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -0
  37. package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
  38. package/payload/server/adminuser-self-heal-QAWOZ3JV.js +45 -0
  39. package/payload/server/chunk-7PLAT6UR.js +2103 -0
  40. package/payload/server/chunk-CJWFM3WX.js +2098 -0
  41. package/payload/server/chunk-D5U4XQ66.js +656 -0
  42. package/payload/server/chunk-DJXPAH7T.js +1480 -0
  43. package/payload/server/chunk-M6J4JM3D.js +656 -0
  44. package/payload/server/chunk-PZZ3IKUU.js +1116 -0
  45. package/payload/server/chunk-T2MQIKBT.js +10001 -0
  46. package/payload/server/chunk-TSOYVJC4.js +10003 -0
  47. package/payload/server/client-pool-M25CGILI.js +32 -0
  48. package/payload/server/client-pool-OX75YUFD.js +33 -0
  49. package/payload/server/cloudflare-task-tracker-GQFKLY62.js +20 -0
  50. package/payload/server/cloudflare-task-tracker-OQTQWFWK.js +20 -0
  51. package/payload/server/maxy-edge.js +4 -4
  52. package/payload/server/neo4j-migrations-4PG2KB4W.js +665 -0
  53. package/payload/server/public/assets/{Checkbox-Bq6ORjz2.js → Checkbox-aCc0UGp3.js} +1 -1
  54. package/payload/server/public/assets/{admin-CstEkw-G.js → admin-D678VwpH.js} +2 -2
  55. package/payload/server/public/assets/data-DsItQm8c.js +1 -0
  56. package/payload/server/public/assets/graph-C-HOmfmU.js +1 -0
  57. package/payload/server/public/assets/{jsx-runtime-DidQeNoZ.css → jsx-runtime-BKoartnM.css} +1 -1
  58. package/payload/server/public/assets/{page-CFWoVkgV.js → page-D7LchjvY.js} +1 -1
  59. package/payload/server/public/assets/{page-Bpi_jPw6.js → page-DTmTvkNo.js} +1 -1
  60. package/payload/server/public/assets/{public-BWMwq5Jj.js → public-C7mCgRX0.js} +1 -1
  61. package/payload/server/public/assets/{useAdminFetch-B93ig7ef.js → useAdminFetch-BgDL3JGd.js} +1 -1
  62. package/payload/server/public/assets/{useVoiceRecorder-Cb0nAtOo.js → useVoiceRecorder-Bx903Mk1.js} +1 -1
  63. package/payload/server/public/data.html +5 -5
  64. package/payload/server/public/graph.html +6 -6
  65. package/payload/server/public/index.html +8 -8
  66. package/payload/server/public/public.html +5 -5
  67. package/payload/server/server.js +39 -28
  68. package/payload/platform/neo4j/migrations/001-backfill-scope.cypher +0 -30
  69. package/payload/platform/neo4j/migrations/002-project-public-agents.ts +0 -191
  70. package/payload/platform/neo4j/migrations/003-person-name-eradicate.cypher +0 -24
  71. package/payload/platform/neo4j/migrations/004-project-admin-agent.ts +0 -348
  72. package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +0 -133
  73. package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +0 -102
  74. package/payload/platform/neo4j/migrations/006-prune-bogus-whatsapp-persons.ts +0 -132
  75. package/payload/platform/neo4j/migrations/007-conversation-archive-source.ts +0 -116
  76. package/payload/platform/neo4j/migrations/008-adminuser-accountid-backfill.ts +0 -85
  77. package/payload/platform/neo4j/migrations/009-conversation-archive-title.ts +0 -197
  78. package/payload/server/public/assets/data-DwZZ7qbH.js +0 -1
  79. package/payload/server/public/assets/graph-DceEv42K.js +0 -1
  80. /package/payload/server/public/assets/{jsx-runtime-DH5S-MwB.js → jsx-runtime-WW3O7tSz.js} +0 -0
@@ -1,348 +0,0 @@
1
- /**
2
- * Migration 004 — Project the admin agent into the graph and clean up
3
- * Conversation channel data (Task 864). Numbered 004 because the 003
4
- * slot is held by `003-person-name-eradicate.cypher` (boot-time apply).
5
- *
6
- * Three idempotent passes:
7
- *
8
- * 1. For every account directory under data/accounts/<accountId>/agents/admin/
9
- * that carries a config.json, call projectAgent(accountId, accountDir,
10
- * 'admin'). The projector is the same function migration 002 uses for
11
- * public agents and is content-agnostic — it reads config.json plus any
12
- * IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY files present and MERGEs the
13
- * :Agent node + four owned :KnowledgeDocument projections. Re-running
14
- * this migration produces no duplicate nodes or edges.
15
- *
16
- * Migration 002 explicitly SKIPS admin (line 60: `if (entry.name ===
17
- * "admin") continue`); doing the projection here keeps that skip valid
18
- * and isolates admin-specific concerns to one file.
19
- *
20
- * 2. For every existing :AdminConversation that does NOT yet have a
21
- * :HANDLED_BY edge, MATCH the freshly-projected admin :Agent and MERGE
22
- * the edge. Guarded with `WHERE NOT EXISTS((c)-[:HANDLED_BY]->(:Agent))`
23
- * so re-runs short-circuit per conversation.
24
- *
25
- * 3. Backfill `c.channel = 'webchat'` for every Conversation node where
26
- * channel IS NULL. Pre-Task-863 conversations were written without the
27
- * property; channel='webchat' is the correct default — only WhatsApp
28
- * and Telegram sessions ever set non-webchat values, and those have
29
- * always come through sessionKeys prefixed `whatsapp:` or `telegram:`
30
- * (see neo4j-store.ts:171). Idempotent: subsequent runs no-op because
31
- * the WHERE clause matches zero rows.
32
- *
33
- * Boot-time entry point (Task 871): {@link applyAdminAgentBackfill}. The
34
- * neo4j-migrations runner calls it from `applyBootMigrations` AFTER
35
- * `pruneAlienAccounts` so we never project :Agent for an account that the
36
- * preceding pass would prune.
37
- *
38
- * Manual standalone entry point (legacy, retained for ad-hoc operator
39
- * runs):
40
- *
41
- * cd platform/ui && \
42
- * NEO4J_URI=bolt://… NEO4J_PASSWORD=… \
43
- * npx tsx ../neo4j/migrations/004-project-admin-agent.ts
44
- *
45
- * Output: structured `[admin-agent-graph-backfill]` lines per account + a
46
- * final totals line. Both entry points emit identical log lines.
47
- */
48
-
49
- import { existsSync, readFileSync, readdirSync } from "node:fs";
50
- import { resolve } from "node:path";
51
- import { projectAgent, getSession } from "../../ui/app/lib/neo4j-store";
52
- import { ACCOUNTS_DIR } from "../../ui/app/lib/claude-agent/account";
53
-
54
- /**
55
- * Account-json filter (Task 900 sub-scope F) — admits only directories whose
56
- * `account.json` parses. Stub directories (e.g. `0dbf29ef-…/logs/` left
57
- * behind after install 1's `account.json` was lost) are SKIPPED with a
58
- * one-line `[platform] accounts-state STUB-DIR id=<dir>` log. Mirrors
59
- * `listValidAccounts()` in `account.ts`; copied here because this migration
60
- * runs at boot from a separate node_modules-resolution context (see the
61
- * structural-typing rationale at the top of `004-prune-alien-accounts.ts`).
62
- */
63
- function readValidAccountDirs(accountsDir: string): string[] {
64
- const valid: string[] = [];
65
- const entries = readdirSync(accountsDir, { withFileTypes: true })
66
- .filter((e) => e.isDirectory())
67
- .filter((e) => !e.name.startsWith("."));
68
- for (const e of entries) {
69
- const configPath = resolve(accountsDir, e.name, "account.json");
70
- if (!existsSync(configPath)) {
71
- console.error(
72
- `[platform] accounts-state STUB-DIR id=${e.name} — onboarding leak; remove or repair`,
73
- );
74
- continue;
75
- }
76
- try {
77
- JSON.parse(readFileSync(configPath, "utf-8"));
78
- valid.push(e.name);
79
- } catch {
80
- console.error(
81
- `[platform] accounts-state CORRUPT-JSON id=${e.name} — account.json failed to parse`,
82
- );
83
- }
84
- }
85
- return valid;
86
- }
87
-
88
- /**
89
- * Structural alias for the `Driver` instance the runner passes in. Same
90
- * rationale as `004-prune-alien-accounts.ts`: this file lives outside
91
- * `platform/ui/`, and depending on the workspace's dedupe state,
92
- * `neo4j-driver` resolves to a different node_modules copy here than at
93
- * the boot-loader site. Structural typing sidesteps the nominal-distinct-
94
- * types-from-different-paths trap.
95
- */
96
- type Neo4jDriverLike = {
97
- session(): {
98
- run(
99
- cypher: string,
100
- params?: Record<string, unknown>,
101
- ): Promise<{ records: Array<{ get(key: string): unknown }> }>;
102
- close(): Promise<void>;
103
- };
104
- };
105
-
106
- interface PerAccountStats {
107
- accountId: string;
108
- projected: 0 | 1;
109
- failed: 0 | 1;
110
- handledByCandidates: number;
111
- handledByEdges: number;
112
- channelBackfilled: number;
113
- }
114
-
115
- async function projectAccountAdmin(
116
- accountId: string,
117
- accountDir: string,
118
- ): Promise<{ projected: 0 | 1; failed: 0 | 1 }> {
119
- const adminDir = resolve(accountDir, "agents", "admin");
120
- const configPath = resolve(adminDir, "config.json");
121
- if (!existsSync(adminDir) || !existsSync(configPath)) {
122
- return { projected: 0, failed: 0 };
123
- }
124
- try {
125
- await projectAgent(accountId, accountDir, "admin");
126
- return { projected: 1, failed: 0 };
127
- } catch (err) {
128
- const msg = err instanceof Error ? err.message : String(err);
129
- console.error(
130
- `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} project FAILED error="${msg}"`,
131
- );
132
- return { projected: 0, failed: 1 };
133
- }
134
- }
135
-
136
- interface HandledByStats {
137
- candidates: number;
138
- edges: number;
139
- }
140
-
141
- /**
142
- * Backfill HANDLED_BY edges from AdminConversation nodes to the admin Agent
143
- * node. Guarded with NOT EXISTS so re-runs of this migration don't redo work
144
- * — and so AdminConversations that already gained a HANDLED_BY edge through
145
- * the forward path are skipped.
146
- *
147
- * `candidates` counts AdminConversations that LACK a HANDLED_BY edge; `edges`
148
- * counts edges newly created. The admin :Agent must already exist (created
149
- * by pass 1); if it doesn't, the OPTIONAL MATCH falls through and zero edges
150
- * are written — surfaced through `candidates - edges`.
151
- */
152
- async function backfillAdminHandledBy(
153
- driver: Neo4jDriverLike,
154
- accountId: string,
155
- ): Promise<HandledByStats> {
156
- const session = driver.session();
157
- try {
158
- const result = await session.run(
159
- `MATCH (c:AdminConversation {accountId: $accountId})
160
- WHERE NOT EXISTS((c)-[:HANDLED_BY]->(:Agent))
161
- OPTIONAL MATCH (a:Agent {accountId: $accountId, slug: 'admin'})
162
- FOREACH (_ IN CASE WHEN a IS NULL THEN [] ELSE [1] END | MERGE (c)-[:HANDLED_BY]->(a))
163
- RETURN
164
- count(c) AS candidates,
165
- sum(CASE WHEN a IS NULL THEN 0 ELSE 1 END) AS edges`,
166
- { accountId },
167
- );
168
- const toNum = (v: unknown): number => {
169
- if (typeof v === "number") return v;
170
- if (v && typeof (v as { toNumber: () => number }).toNumber === "function") {
171
- return (v as { toNumber: () => number }).toNumber();
172
- }
173
- return 0;
174
- };
175
- return {
176
- candidates: toNum(result.records[0]?.get("candidates")),
177
- edges: toNum(result.records[0]?.get("edges")),
178
- };
179
- } finally {
180
- await session.close();
181
- }
182
- }
183
-
184
- /**
185
- * Backfill `c.channel = 'webchat'` on Conversation nodes that lack the
186
- * property. Hits both AdminConversation and PublicConversation — the
187
- * predicate is `c.channel IS NULL`, label-agnostic.
188
- *
189
- * Why default to 'webchat': new writes set channel from sessionKey prefix
190
- * (neo4j-store.ts:171). Only `whatsapp:` and `telegram:` prefixes produce
191
- * non-webchat values, and those prefixes have always existed. So a NULL
192
- * channel can only mean "Conversation written before Task 863 added the
193
- * SET clause" — which by definition was a webchat session.
194
- *
195
- * Idempotent: WHERE clause matches zero rows on re-run.
196
- */
197
- async function backfillChannel(
198
- driver: Neo4jDriverLike,
199
- accountId: string,
200
- ): Promise<number> {
201
- const session = driver.session();
202
- try {
203
- const result = await session.run(
204
- `MATCH (c:Conversation {accountId: $accountId})
205
- WHERE c.channel IS NULL
206
- SET c.channel = 'webchat'
207
- RETURN count(c) AS backfilled`,
208
- { accountId },
209
- );
210
- const raw = result.records[0]?.get("backfilled");
211
- if (typeof raw === "number") return raw;
212
- if (raw && typeof (raw as { toNumber: () => number }).toNumber === "function") {
213
- return (raw as { toNumber: () => number }).toNumber();
214
- }
215
- return 0;
216
- } finally {
217
- await session.close();
218
- }
219
- }
220
-
221
- /**
222
- * Boot-time entry point (Task 871). Idempotent across every pass; safe to
223
- * run on every server boot. Throws on fatal failure; per-account failures
224
- * are logged and counted but do not abort the run (subsequent accounts
225
- * still get processed). The neo4j-migrations runner wraps this in its own
226
- * try/catch so a throw here is logged as `[migration] failed
227
- * admin-agent-graph-backfill error="…"` and boot continues.
228
- *
229
- * Refusal posture: if `<platformRoot>/../data/accounts` does not exist,
230
- * logs and returns silently — nothing to do (matches pruneAlienAccounts).
231
- */
232
- export async function applyAdminAgentBackfill(
233
- driver: Neo4jDriverLike,
234
- platformRoot: string,
235
- ): Promise<void> {
236
- const accountsDir = resolve(platformRoot, "..", "data", "accounts");
237
- const start = Date.now();
238
-
239
- if (!existsSync(accountsDir)) {
240
- console.error(
241
- `[admin-agent-graph-backfill] accounts-dir missing at ${accountsDir} — nothing to do`,
242
- );
243
- return;
244
- }
245
-
246
- const accountEntries = readValidAccountDirs(accountsDir).map((name) => ({ name }));
247
-
248
- console.error(
249
- `[admin-agent-graph-backfill] start accounts=${accountEntries.length}`,
250
- );
251
-
252
- let totalProjected = 0;
253
- let totalFailed = 0;
254
- let totalHandledByCandidates = 0;
255
- let totalHandledByEdges = 0;
256
- let totalChannelBackfilled = 0;
257
- const perAccount: PerAccountStats[] = [];
258
-
259
- for (const entry of accountEntries) {
260
- const accountDir = resolve(accountsDir, entry.name);
261
- const accountId = entry.name;
262
- const accountStart = Date.now();
263
-
264
- const { projected, failed } = await projectAccountAdmin(
265
- accountId,
266
- accountDir,
267
- );
268
- totalProjected += projected;
269
- totalFailed += failed;
270
-
271
- let handledByStats: HandledByStats = { candidates: 0, edges: 0 };
272
- let channelBackfilled = 0;
273
-
274
- try {
275
- handledByStats = await backfillAdminHandledBy(driver, accountId);
276
- totalHandledByCandidates += handledByStats.candidates;
277
- totalHandledByEdges += handledByStats.edges;
278
- } catch (err) {
279
- const msg = err instanceof Error ? err.message : String(err);
280
- console.error(
281
- `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} handled-by-backfill FAILED error="${msg}"`,
282
- );
283
- }
284
-
285
- try {
286
- channelBackfilled = await backfillChannel(driver, accountId);
287
- totalChannelBackfilled += channelBackfilled;
288
- } catch (err) {
289
- const msg = err instanceof Error ? err.message : String(err);
290
- console.error(
291
- `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} channel-backfill FAILED error="${msg}"`,
292
- );
293
- }
294
-
295
- perAccount.push({
296
- accountId,
297
- projected,
298
- failed,
299
- handledByCandidates: handledByStats.candidates,
300
- handledByEdges: handledByStats.edges,
301
- channelBackfilled,
302
- });
303
- const ms = Date.now() - accountStart;
304
- console.error(
305
- `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} projected=${projected} failed=${failed} handled-by-candidates=${handledByStats.candidates} handled-by-edges=${handledByStats.edges} channel-backfilled=${channelBackfilled} ms=${ms}`,
306
- );
307
- }
308
-
309
- const ms = Date.now() - start;
310
- console.error(
311
- `[admin-agent-graph-backfill] done totals: projected=${totalProjected} failed=${totalFailed} handled-by-candidates=${totalHandledByCandidates} handled-by-edges=${totalHandledByEdges} channel-backfilled=${totalChannelBackfilled} ms=${ms}`,
312
- );
313
- }
314
-
315
- /**
316
- * Standalone entry point — retained for manual `npx tsx` runs (Task 871
317
- * preserved this so an operator can re-run the backfill ad-hoc without a
318
- * full server boot). Wraps `applyAdminAgentBackfill` with a structural
319
- * driver-shape that delegates to the neo4j-store singleton's getSession,
320
- * so the env-var resolution (NEO4J_URI / NEO4J_PASSWORD / config file)
321
- * matches the boot path exactly.
322
- */
323
- async function main(): Promise<void> {
324
- if (!existsSync(ACCOUNTS_DIR)) {
325
- console.error(
326
- `[admin-agent-graph-backfill] ACCOUNTS_DIR missing at ${ACCOUNTS_DIR} — nothing to do`,
327
- );
328
- process.exit(0);
329
- }
330
- const driverShim: Neo4jDriverLike = { session: () => getSession() };
331
- const platformRoot = resolve(ACCOUNTS_DIR, "..", "..", "platform");
332
- try {
333
- await applyAdminAgentBackfill(driverShim, platformRoot);
334
- process.exit(0);
335
- } catch (err) {
336
- const msg = err instanceof Error ? err.message : String(err);
337
- console.error(`[admin-agent-graph-backfill] fatal error="${msg}"`);
338
- process.exit(2);
339
- }
340
- }
341
-
342
- if (process.argv[1]?.endsWith("004-project-admin-agent.ts")) {
343
- main().catch((err) => {
344
- const msg = err instanceof Error ? err.message : String(err);
345
- console.error(`[admin-agent-graph-backfill] fatal error="${msg}"`);
346
- process.exit(2);
347
- });
348
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * Migration 004 — Prune alien-account nodes (Task 847).
3
- *
4
- * Deletes every node whose `accountId` is not present on disk under
5
- * `${DATA_ROOT}/accounts/<uuid>/account.json`. Idempotent — silent when
6
- * the graph is already clean.
7
- *
8
- * Why a backstop, not a writer fix: the leaked nodes were written by a
9
- * since-removed writer. With no live writer to fix, this is the surface
10
- * that catches future writer drift.
11
- *
12
- * Hard guard: refuses to run when the on-disk account set is empty
13
- * (corrupt-install scenario). Refusing to wipe the graph is louder than
14
- * silently wiping it.
15
- *
16
- * Doctrine in `.docs/neo4j.md` "Account isolation invariant" requires
17
- * any writer that stamps `n.accountId` to verify the value against
18
- * `${DATA_ROOT}/accounts/<id>/account.json` before write. This migration
19
- * is a backstop, not a license.
20
- */
21
-
22
- import { readFileSync, readdirSync } from "node:fs";
23
- import { resolve } from "node:path";
24
-
25
- const UUID_RE =
26
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
27
-
28
- /**
29
- * Structural alias for the `Driver` instance the runner passes in. We type
30
- * structurally rather than importing `Driver` from `neo4j-driver` because
31
- * this file lives outside `platform/ui/` — depending on the workspace's
32
- * dedupe state, `neo4j-driver` resolves to a different node_modules copy
33
- * here than at the runner site, and TS treats two same-shape types from
34
- * different paths as nominally distinct. Structural typing sidesteps that.
35
- */
36
- type Neo4jDriverLike = {
37
- session(): {
38
- run(
39
- cypher: string,
40
- params?: Record<string, unknown>,
41
- ): Promise<{ records: Array<{ get(key: string): unknown }> }>;
42
- close(): Promise<void>;
43
- };
44
- };
45
-
46
- export async function pruneAlienAccounts(
47
- driver: Neo4jDriverLike,
48
- platformRoot: string,
49
- ): Promise<void> {
50
- const accountsDir = resolve(platformRoot, "..", "data", "accounts");
51
- const validIds = enumerateValidAccountIds(accountsDir);
52
-
53
- if (validIds.size === 0) {
54
- throw new Error(
55
- `refusing to prune: no valid accounts found under ${accountsDir} — corrupt install? not deleting anything to avoid wiping the entire graph.`,
56
- );
57
- }
58
-
59
- const valid = Array.from(validIds);
60
- const session = driver.session();
61
- try {
62
- // Two-step: first query collects the alien accountIds for the log
63
- // line, second query deletes. Two cheap queries beat a single query
64
- // that loses either the count or the id list under DELETE semantics.
65
- const peek = await session.run(
66
- `MATCH (n)
67
- WHERE n.accountId IS NOT NULL AND NOT n.accountId IN $valid
68
- RETURN DISTINCT n.accountId AS aid`,
69
- { valid },
70
- );
71
- const alienIds: string[] = [];
72
- for (const record of peek.records) {
73
- const aid: unknown = record.get("aid");
74
- if (typeof aid === "string") alienIds.push(aid);
75
- }
76
-
77
- if (alienIds.length === 0) return;
78
-
79
- const result = await session.run(
80
- `MATCH (n)
81
- WHERE n.accountId IS NOT NULL AND NOT n.accountId IN $valid
82
- DETACH DELETE n
83
- RETURN count(n) AS pruned`,
84
- { valid },
85
- );
86
- const prunedRaw = result.records[0]?.get("pruned");
87
- const pruned =
88
- typeof prunedRaw === "number"
89
- ? prunedRaw
90
- : (prunedRaw as { toNumber?: () => number })?.toNumber?.() ?? 0;
91
- console.error(
92
- `[graph-invariant] alien-accounts pruned=${pruned} accountIds=${alienIds.join(",")}`,
93
- );
94
- } finally {
95
- await session.close();
96
- }
97
- }
98
-
99
- /**
100
- * Enumerate accountIds with a parseable `account.json`. Directory name IS
101
- * the canonical accountId (matches the UUID_RE.test(name) predicate at
102
- * `platform/ui/server/routes/admin/files.ts`'s account-name resolver).
103
- *
104
- * Corruption discipline: a present-but-unparseable account.json is
105
- * EXCLUDED from the valid set and emits a skip log line. Better to
106
- * over-prune one suspect account than under-prune the leak it might
107
- * be hiding.
108
- */
109
- function enumerateValidAccountIds(accountsDir: string): Set<string> {
110
- const valid = new Set<string>();
111
- let names: string[];
112
- try {
113
- names = readdirSync(accountsDir);
114
- } catch (err) {
115
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return valid;
116
- throw err;
117
- }
118
- for (const name of names) {
119
- if (!UUID_RE.test(name)) continue;
120
- const configPath = resolve(accountsDir, name, "account.json");
121
- try {
122
- JSON.parse(readFileSync(configPath, "utf-8"));
123
- valid.add(name);
124
- } catch (err) {
125
- const code = (err as NodeJS.ErrnoException).code ?? "parse-error";
126
- if (code === "ENOENT") continue;
127
- console.error(
128
- `[graph-invariant] account-json-skip uuid=${name} reason=${code}`,
129
- );
130
- }
131
- }
132
- return valid;
133
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * Migration 005 — Remove review-detector / review-digest feature artefacts (Task 884).
3
- *
4
- * The review-detector subsystem was wired into boot, MERGEd a daily `:Event`
5
- * with `actionTool="review-digest-compose"` on every start, and aggregated
6
- * `:ReviewAlert` nodes via rule matches. The feature is deleted entirely;
7
- * this migration is the boot-sweep that removes the residue from existing
8
- * installs. Idempotent — once the graph is clean, both queries DETACH DELETE
9
- * zero rows and the log line records `events=0 alerts=0`.
10
- *
11
- * Filesystem artefacts (`review.log`, `review-rules.json`, `review-state.json`,
12
- * `review-pending-alerts.jsonl`) are also removed best-effort — ENOENT is the
13
- * idempotent no-op; any other errno is logged and ignored so a sticky permission
14
- * problem does not block boot.
15
- *
16
- * Same structural-type pattern as 004-prune-alien-accounts.ts: the runner sits
17
- * outside `platform/ui/` so depending on the workspace dedupe state the
18
- * `neo4j-driver` import resolves to a different node_modules copy, and TS
19
- * treats two same-shape types from different paths as nominally distinct.
20
- */
21
-
22
- import { resolve } from "node:path";
23
- import { unlinkSync } from "node:fs";
24
- import { homedir } from "node:os";
25
- import { basename, dirname } from "node:path";
26
-
27
- type Neo4jDriverLike = {
28
- session(): {
29
- run(
30
- cypher: string,
31
- params?: Record<string, unknown>,
32
- ): Promise<{ records: Array<{ get(key: string): unknown }> }>;
33
- close(): Promise<void>;
34
- };
35
- };
36
-
37
- export async function removeReviewFeature(
38
- driver: Neo4jDriverLike,
39
- platformRoot: string,
40
- ): Promise<void> {
41
- const session = driver.session();
42
- let eventsDeleted = 0;
43
- let alertsDeleted = 0;
44
-
45
- try {
46
- const eventResult = await session.run(
47
- `MATCH (e:Event)
48
- WHERE e.actionTool = "review-digest-compose"
49
- OR e.eventId STARTS WITH "review-digest-"
50
- DETACH DELETE e
51
- RETURN count(e) AS deleted`,
52
- );
53
- eventsDeleted = toNumber(eventResult.records[0]?.get("deleted"));
54
-
55
- const alertResult = await session.run(
56
- `MATCH (a:ReviewAlert)
57
- DETACH DELETE a
58
- RETURN count(a) AS deleted`,
59
- );
60
- alertsDeleted = toNumber(alertResult.records[0]?.get("deleted"));
61
- } finally {
62
- await session.close();
63
- }
64
-
65
- // Best-effort filesystem cleanup. The configDir convention is
66
- // `~/.<installDirName>` where installDir = dirname(platformRoot)
67
- // (e.g. `~/maxy/platform` → `~/.maxy`).
68
- const installDirName = basename(dirname(platformRoot));
69
- const configDir = resolve(homedir(), `.${installDirName}`);
70
- const artefacts = [
71
- resolve(configDir, "logs", "review.log"),
72
- resolve(configDir, "review-rules.json"),
73
- resolve(configDir, "review-state.json"),
74
- resolve(configDir, "review-pending-alerts.jsonl"),
75
- ];
76
- for (const path of artefacts) {
77
- try {
78
- unlinkSync(path);
79
- } catch (err) {
80
- const code = (err as NodeJS.ErrnoException).code;
81
- if (code === "ENOENT") continue;
82
- console.error(
83
- `[migration] removed-review-feature unlink-skip path=${path} reason=${code ?? "unknown"}`,
84
- );
85
- }
86
- }
87
-
88
- console.error(
89
- `[migration] removed-review-feature events=${eventsDeleted} alerts=${alertsDeleted}`,
90
- );
91
- }
92
-
93
- function toNumber(v: unknown): number {
94
- if (typeof v === "number") return v;
95
- if (
96
- v &&
97
- typeof (v as { toNumber?: () => number }).toNumber === "function"
98
- ) {
99
- return (v as { toNumber: () => number }).toNumber();
100
- }
101
- return 0;
102
- }