@rubytech/create-realagent 1.0.823 → 1.0.824

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 (32) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/migrations/006-prune-bogus-whatsapp-persons.ts +132 -0
  3. package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +1 -1
  4. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
  5. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  6. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  7. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +10 -5
  8. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  9. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.d.ts +4 -0
  10. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.d.ts.map +1 -1
  11. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.js +106 -30
  12. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.js.map +1 -1
  13. package/payload/platform/plugins/whatsapp-import/bin/ingest.mjs +174 -115
  14. package/payload/platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh +18 -7
  15. package/payload/platform/plugins/whatsapp-import/lib/dist/parse-export.js +24 -0
  16. package/payload/platform/plugins/whatsapp-import/lib/dist/parse-export.js.map +1 -1
  17. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/filter-gate.test.ts +2 -0
  18. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/parse-export-lrm.test.ts +83 -0
  19. package/payload/platform/plugins/whatsapp-import/lib/src/parse-export.ts +25 -0
  20. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +11 -9
  21. package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
  22. package/payload/server/chunk-JNVZQMTZ.js +593 -0
  23. package/payload/server/chunk-L3T7ECLI.js +1116 -0
  24. package/payload/server/chunk-LFOR2ACU.js +10079 -0
  25. package/payload/server/chunk-NAP6XMLW.js +2233 -0
  26. package/payload/server/client-pool-M2DU74ZP.js +32 -0
  27. package/payload/server/cloudflare-task-tracker-6S23B7QX.js +17 -0
  28. package/payload/server/maxy-edge.js +3 -3
  29. package/payload/server/neo4j-migrations-YED5CFPF.js +428 -0
  30. package/payload/server/public/assets/{admin-Bnj-1qCb.js → admin-DOkUspG1.js} +1 -1
  31. package/payload/server/public/index.html +1 -1
  32. package/payload/server/server.js +120 -7
@@ -11,10 +11,19 @@
11
11
  //
12
12
  // Argv (positional): <archive-path>
13
13
  // Argv (flags): --owner-element-id <id> --scope <admin|public>
14
+ // --subject-person-id <id>
14
15
  // --filter <all|senders=<csv>|date-range=<isoFrom>..<isoTo>>
15
16
  // [--account-id <accountId>] [--timezone <iana>]
16
17
  // [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>]
17
18
  //
19
+ // Task 887 §A0 — `--subject-person-id` is required. DM scope: the operator
20
+ // confirms the third party's :Person elementId from the preview histogram
21
+ // before invoking. The writer accepts EXACTLY the senderNames carried by
22
+ // {owner, subject}; any parsed senderName outside that set LOUD-FAILs with
23
+ // `parser-miss reason="senderName=<verbatim> not in preview histogram
24
+ // (parser failure — re-export or report)"`. Bounds writer cardinality to
25
+ // the deterministic preview output — closes the auto-Person leak structurally.
26
+ //
18
27
  // Stdout (success): one JSON line — Honest counters per Task 871.5.
19
28
  // {conversationElementId, conversationId,
20
29
  // parsed, mediaSkipped, systemSkipped,
@@ -139,6 +148,13 @@ function parseArgv(argv) {
139
148
  if (flags.scope !== "admin" && flags.scope !== "public") {
140
149
  fail("argv", { reason: `invalid --scope "${flags.scope}" (admin|public)` });
141
150
  }
151
+ // Task 887 §A0 — DM scope: a single `--subject-person-id` identifies the
152
+ // third party in the conversation. The owner + subject pair is the
153
+ // canonical sender set; the writer rejects any other senderName as
154
+ // parser-miss. Group-chat ingest (>2 distinct senders) is a future task.
155
+ if (!flags.subjectPersonId) {
156
+ fail("argv", { reason: "missing --subject-person-id (Task 887: operator-confirmed third-party :Person elementId from preview histogram)" });
157
+ }
142
158
  // Task 871: --filter is mandatory. The deterministic Bash entry refuses
143
159
  // bulk archive writes without an operator-supplied filter — closes the
144
160
  // doctrine gap named in feedback_compress_at_ingest_for_bulk_archives.md.
@@ -247,143 +263,176 @@ function resolveAccountId(flags) {
247
263
  }
248
264
 
249
265
  // ---------------------------------------------------------------------------
250
- // 6. Resolve participants match each sender against canonical
251
- // (:Person|:AdminUser {accountId}) by NFKC-trim-lower normalisation
252
- // before falling through to auto-Person creation (Task 870). Per-sender
253
- // `participant-resolved` log line so re-imports surface canonical reuse.
254
- // Auto-Person fallback retains the legacy `{accountId, source:'whatsapp',
255
- // name}` shape; switching to givenName/familyName is Task 874.
266
+ // 6. Bind senders to canonical {owner, subject} pair (Task 887 §A0).
267
+ //
268
+ // Pre-887 behaviour: a `resolveParticipants` step ran each parsed senderName
269
+ // against an account-wide canonical index, falling through to auto-create a
270
+ // `:Person {participantStatus:'auto-created'}` node for any miss. That
271
+ // fallback path was the structural defect: any parser failure that produced
272
+ // a polluted senderName (Task 845's LRM-prefixed body glued onto the prior
273
+ // header → senderName="\"Adam Mackay:\\n‎[04/02/2026, 11:52:16] Adam Mackay\"")
274
+ // leaked one bogus :Person per distinct miss. 23 leaked from the Adam Mackay
275
+ // archive in a single ingest.
276
+ //
277
+ // 887 §A0 closes the leak by deleting the fallback. The operator now passes
278
+ // `--subject-person-id` (third-party Person elementId from preview), the
279
+ // dispatch passes `--owner-element-id`, and the writer accepts EXACTLY the
280
+ // name candidates of those two nodes. Any parsed senderName outside that
281
+ // closed set LOUD-FAILs the ingest with the verbatim string — surfacing a
282
+ // parser bug as a hard exit instead of as graph pollution.
256
283
  // ---------------------------------------------------------------------------
257
284
 
258
- const CANONICAL_FETCH_CYPHER = `
259
- MATCH (p:Person {accountId: $accountId})
260
- WHERE coalesce(p.participantStatus, '') <> 'auto-created'
261
- RETURN elementId(p) AS elemId,
262
- coalesce(p.givenName, '') AS givenName,
263
- coalesce(p.familyName, '') AS familyName,
264
- NULL AS adminName,
265
- 'Person' AS label
266
- UNION
267
- MATCH (u:AdminUser {accountId: $accountId})
268
- RETURN elementId(u) AS elemId,
269
- '' AS givenName,
270
- '' AS familyName,
271
- coalesce(u.name, '') AS adminName,
272
- 'AdminUser' AS label
285
+ const CANONICAL_PAIR_FETCH_CYPHER = `
286
+ UNWIND $ids AS id
287
+ MATCH (n) WHERE elementId(n) = id
288
+ RETURN elementId(n) AS elemId,
289
+ labels(n) AS labels,
290
+ coalesce(n.name, '') AS name,
291
+ coalesce(n.givenName, '') AS givenName,
292
+ coalesce(n.familyName, '') AS familyName,
293
+ coalesce(n.accountId, '') AS accountId
273
294
  `;
274
295
 
275
- const PARTICIPANT_UPSERT_CYPHER = `
276
- UNWIND $names AS senderName
277
- MERGE (p:Person {accountId: $accountId, source: 'whatsapp', name: senderName})
278
- ON CREATE SET
279
- p.createdByAgent = 'whatsapp-import',
280
- p.createdBySource = 'whatsapp-import',
281
- p.createdBySession = $sessionId,
282
- p.createdAt = datetime(),
283
- p.scope = $scope,
284
- p.participantStatus = 'auto-created'
285
- RETURN elementId(p) AS elemId, senderName AS name
286
- `;
296
+ /**
297
+ * Sentinel error class so `main()`'s try/catch can recognise an operator
298
+ * LOUD-FAIL (already-emitted FAIL line) and exit cleanly with cleanup.
299
+ * Plain `process.exit(1)` from inside `bindCanonicalSenders` would skip
300
+ * `main()`'s `cleanup()` (unzip tmp dir) and `session.close()` paths.
301
+ */
302
+ class IngestUserFacingError extends Error {
303
+ constructor(message) {
304
+ super(message);
305
+ this.name = "IngestUserFacingError";
306
+ this.userFacing = true;
307
+ }
308
+ }
287
309
 
288
- async function fetchCanonicalIndex({ session, accountId, normaliseSenderName }) {
289
- // Returns Map<normalisedName, {elemId, label}> for canonical participants.
290
- // Excludes auto-created Persons so we never reuse our own shadow nodes.
310
+ async function bindCanonicalSenders({
311
+ session,
312
+ accountId,
313
+ ownerElementId,
314
+ subjectPersonId,
315
+ senderNames,
316
+ normaliseSenderName,
317
+ }) {
318
+ // Distinctness pre-flight — operator passing the same id for both flags
319
+ // collapses to a single bound participant; the drift between
320
+ // `participantCount=2` (from distinct senderNames) and
321
+ // `participantsAlreadyExisted=1` would propagate to the JSON summary
322
+ // silently. Refuse early, name the cause.
323
+ if (ownerElementId === subjectPersonId) {
324
+ throw new IngestUserFacingError(
325
+ `--owner-element-id and --subject-person-id must be distinct elementIds (both supplied as "${ownerElementId}")`,
326
+ );
327
+ }
328
+
329
+ const ids = [ownerElementId, subjectPersonId];
291
330
  const res = await session.executeRead(async (tx) =>
292
- tx.run(CANONICAL_FETCH_CYPHER, { accountId }),
331
+ tx.run(CANONICAL_PAIR_FETCH_CYPHER, { ids }),
293
332
  );
333
+
334
+ // Build normalised-name → elementId index from owner + subject candidates.
335
+ // For each node we accept the full name, given name, family name, and
336
+ // "given family" composite as match candidates so an export header that
337
+ // says "Adam" or "Adam Mackay" both resolve to the same node.
294
338
  const index = new Map();
339
+ const seenIds = new Set();
340
+ const labelByElemId = new Map();
295
341
  for (const r of res.records) {
296
342
  const elemId = r.get("elemId");
297
- const label = r.get("label");
298
- const candidates = [];
299
- if (label === "Person") {
300
- const given = r.get("givenName") || "";
301
- const family = r.get("familyName") || "";
302
- if (given || family) candidates.push(`${given} ${family}`.trim());
303
- if (given) candidates.push(given);
304
- if (family) candidates.push(family);
305
- } else {
306
- const adminName = r.get("adminName") || "";
307
- if (adminName) candidates.push(adminName);
343
+ const labels = r.get("labels") || [];
344
+ const acct = r.get("accountId") || "";
345
+ // Empty accountId on a canonical Person/AdminUser is a graph-data
346
+ // defect (migration 004 normally prunes account-less nodes). Refuse
347
+ // rather than silently accept — bound-pair correctness depends on
348
+ // accountId being present and matching.
349
+ if (!acct) {
350
+ throw new IngestUserFacingError(
351
+ `node ${elemId} has no accountId — corrupt canonical Person/AdminUser; refusing ingest`,
352
+ );
353
+ }
354
+ if (acct !== accountId) {
355
+ throw new IngestUserFacingError(
356
+ `node ${elemId} belongs to account ${acct}, not ${accountId} — refusing cross-account ingest`,
357
+ );
358
+ }
359
+ if (!labels.includes("Person") && !labels.includes("AdminUser")) {
360
+ throw new IngestUserFacingError(
361
+ `node ${elemId} has labels [${labels.join(",")}]; expected :Person or :AdminUser`,
362
+ );
308
363
  }
364
+ seenIds.add(elemId);
365
+ labelByElemId.set(
366
+ elemId,
367
+ labels.includes("Person") ? "Person" : "AdminUser",
368
+ );
369
+ const candidates = [];
370
+ const name = r.get("name") || "";
371
+ const given = r.get("givenName") || "";
372
+ const family = r.get("familyName") || "";
373
+ if (name) candidates.push(name);
374
+ if (given && family) candidates.push(`${given} ${family}`);
375
+ if (given) candidates.push(given);
376
+ if (family) candidates.push(family);
309
377
  for (const c of candidates) {
310
378
  const norm = normaliseSenderName(c);
311
379
  if (!norm) continue;
312
- // First write wins — Person before AdminUser per UNION order.
313
- if (!index.has(norm)) index.set(norm, { elemId, label });
380
+ if (!index.has(norm)) index.set(norm, elemId);
314
381
  }
315
382
  }
316
- return index;
317
- }
318
383
 
319
- async function resolveParticipants({
320
- session,
321
- accountId,
322
- scope,
323
- sessionId,
324
- senderNames,
325
- normaliseSenderName,
326
- }) {
327
- if (senderNames.length === 0) {
328
- return { idsByName: new Map(), participantsAlreadyExisted: 0 };
384
+ // Both ids must resolve. A typo'd or stale id is operator error; LOUD-FAIL
385
+ // before any parser work touches the graph.
386
+ if (!seenIds.has(ownerElementId)) {
387
+ throw new IngestUserFacingError(
388
+ `--owner-element-id ${ownerElementId} not found in graph`,
389
+ );
390
+ }
391
+ if (!seenIds.has(subjectPersonId)) {
392
+ throw new IngestUserFacingError(
393
+ `--subject-person-id ${subjectPersonId} not found in graph`,
394
+ );
395
+ }
396
+ // Subject must specifically be a `:Person` — the third party in a DM is
397
+ // never the operator's `:AdminUser`. (Owner can be either; both Adam and
398
+ // an external collaborator owning an export are operator-curated cases.)
399
+ if (labelByElemId.get(subjectPersonId) !== "Person") {
400
+ throw new IngestUserFacingError(
401
+ `--subject-person-id ${subjectPersonId} resolves to a :${labelByElemId.get(subjectPersonId)} — subject must be a :Person`,
402
+ );
329
403
  }
330
404
 
331
- const canonicalIndex = await fetchCanonicalIndex({
332
- session,
333
- accountId,
334
- normaliseSenderName,
335
- });
405
+ // Group-chat early-detect: the singular `--subject-person-id` flag is
406
+ // DM-scoped. A `_chat.txt` carrying ≥3 distinct senders is an unsupported
407
+ // scope, NOT a parser bug. Emit a distinct reason so the operator does
408
+ // not chase a phantom parser regression. Group-chat support is the
409
+ // separate Task 889 lane.
410
+ if (senderNames.length > 2) {
411
+ throw new IngestUserFacingError(
412
+ `unsupported-scope reason="archive carries ${senderNames.length} distinct senders; --subject-person-id is DM-only (≤2 senders) — group-chat ingest is the separate Task 889 lane"`,
413
+ );
414
+ }
336
415
 
416
+ // Validate every distinct parsed senderName against the closed candidate
417
+ // set. The first miss is the LOUD-FAIL — operators see one parser-miss
418
+ // line per bad import, not 23.
337
419
  const idsByName = new Map();
338
- const fallbackSenders = [];
339
- let canonicalMatches = 0;
340
420
  for (const senderName of senderNames) {
341
421
  const norm = normaliseSenderName(senderName);
342
- const hit = canonicalIndex.get(norm);
343
- if (hit) {
344
- idsByName.set(senderName, hit.elemId);
345
- canonicalMatches++;
346
- log(
347
- `participant-resolved senderName="${senderName}" matched=canonical nodeId=${hit.elemId} label=${hit.label}`,
348
- );
349
- } else {
350
- fallbackSenders.push(senderName);
351
- }
352
- }
353
-
354
- let autoCreated = 0;
355
- if (fallbackSenders.length > 0) {
356
- const result = await session.executeWrite(async (tx) => {
357
- const res = await tx.run(PARTICIPANT_UPSERT_CYPHER, {
358
- names: fallbackSenders,
359
- accountId,
360
- scope,
361
- sessionId,
362
- });
363
- const m = new Map();
364
- for (const r of res.records) {
365
- m.set(r.get("name"), r.get("elemId"));
366
- }
367
- const stats = res.summary.counters.updates();
368
- return { m, created: stats.nodesCreated };
369
- });
370
- autoCreated = result.created;
371
- for (const senderName of fallbackSenders) {
372
- const elemId = result.m.get(senderName);
373
- if (!elemId) continue;
374
- idsByName.set(senderName, elemId);
375
- log(
376
- `participant-resolved senderName="${senderName}" matched=auto nodeId=${elemId} label=Person`,
422
+ const hit = index.get(norm);
423
+ if (!hit) {
424
+ throw new IngestUserFacingError(
425
+ `parser-miss reason="senderName=${senderName} not in preview histogram (parser failure — re-export or report)"`,
377
426
  );
378
427
  }
428
+ idsByName.set(senderName, hit);
429
+ log(
430
+ `participant-resolved senderName="${senderName}" matched=canonical nodeId=${hit}`,
431
+ );
379
432
  }
380
-
381
- // participantsAlreadyExisted = canonical hits + auto-Persons that were already in graph.
382
- const autoAlreadyExisted = fallbackSenders.length - autoCreated;
383
- return {
384
- idsByName,
385
- participantsAlreadyExisted: canonicalMatches + autoAlreadyExisted,
386
- };
433
+ // participantsAlreadyExisted = always 2 (owner + subject) under the
434
+ // bound-pair contract; expose it for the JSON summary's existing field.
435
+ return { idsByName, participantsAlreadyExisted: seenIds.size };
387
436
  }
388
437
 
389
438
  // ---------------------------------------------------------------------------
@@ -394,6 +443,7 @@ async function main() {
394
443
  const startedMs = Date.now();
395
444
  const { archive, flags } = parseArgv(process.argv);
396
445
  const ownerElementId = flags.ownerElementId;
446
+ const subjectPersonId = flags.subjectPersonId;
397
447
  const scope = flags.scope;
398
448
  const accountId = resolveAccountId(flags);
399
449
  const timezone = flags.timezone || "Europe/London";
@@ -468,19 +518,28 @@ async function main() {
468
518
  );
469
519
 
470
520
  try {
471
- participantUpsert = await resolveParticipants({
521
+ participantUpsert = await bindCanonicalSenders({
472
522
  session,
473
523
  accountId,
474
- scope,
475
- sessionId,
524
+ ownerElementId,
525
+ subjectPersonId,
476
526
  senderNames: distinctSenderNames,
477
527
  normaliseSenderName,
478
528
  });
479
529
  } catch (err) {
480
530
  await session.close().catch(() => {});
481
531
  cleanup();
532
+ // IngestUserFacingError carries a brief-shaped FAIL line (parser-miss /
533
+ // unsupported-scope / argv mismatch) that the operator's grep recipes
534
+ // already match on. Preserve it verbatim instead of wrapping in
535
+ // phase=archive-write — wrapping would defeat
536
+ // `grep '\[whatsapp-ingest\] FAIL parser-miss'` and friends.
537
+ if (err && err.userFacing) {
538
+ process.stderr.write(`[whatsapp-ingest] FAIL ${err.message}\n`);
539
+ process.exit(1);
540
+ }
482
541
  fail("archive-write", {
483
- phase: "participant-resolve",
542
+ phase: "bind-canonical-senders",
484
543
  reason: err instanceof Error ? err.message : String(err),
485
544
  });
486
545
  }
@@ -10,12 +10,19 @@
10
10
  # Usage:
11
11
  # bash whatsapp-ingest.sh <archive.zip|dir|_chat.txt>
12
12
  # --owner-element-id <id>
13
+ # --subject-person-id <id>
13
14
  # --scope <admin|public>
14
15
  # --filter <all|senders=<csv>|date-range=<isoFrom>..<isoTo>>
15
16
  # [--account-id <accountId>]
16
17
  # [--timezone <iana-zone>]
17
18
  # [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>]
18
19
  #
20
+ # `--subject-person-id` is required (Task 887 §A0). DM scope: the
21
+ # operator-confirmed third-party :Person elementId from the preview
22
+ # histogram. Owner + subject form the closed sender set; any parsed
23
+ # senderName outside that set LOUD-FAILs with `parser-miss` and exits
24
+ # non-zero — bounds writer cardinality to the deterministic preview output.
25
+ #
19
26
  # `--filter` is mandatory (Task 871). Forms:
20
27
  # all — write every parsed row
21
28
  # senders=Alice,Bob Carter — keep rows whose senderName ∈ csv
@@ -54,9 +61,11 @@ fi
54
61
  # wrong invocation, not on a missing password.
55
62
  ARCHIVE=""
56
63
  OWNER_VAL=""
64
+ SUBJECT_VAL=""
57
65
  SCOPE_VAL=""
58
66
  FILTER_VAL=""
59
67
  HAS_OWNER=0
68
+ HAS_SUBJECT=0
60
69
  HAS_SCOPE=0
61
70
  HAS_FILTER=0
62
71
 
@@ -65,11 +74,12 @@ i=0
65
74
  while [ $i -lt ${#ARGS[@]} ]; do
66
75
  a="${ARGS[$i]}"
67
76
  case "$a" in
68
- --owner-element-id) HAS_OWNER=1; OWNER_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
69
- --scope) HAS_SCOPE=1; SCOPE_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
70
- --filter) HAS_FILTER=1; FILTER_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
77
+ --owner-element-id) HAS_OWNER=1; OWNER_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
78
+ --subject-person-id) HAS_SUBJECT=1; SUBJECT_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
79
+ --scope) HAS_SCOPE=1; SCOPE_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
80
+ --filter) HAS_FILTER=1; FILTER_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
71
81
  --account-id|--timezone|--date-format) i=$((i + 2)); continue ;;
72
- --*) i=$((i + 2)); continue ;;
82
+ --*) i=$((i + 2)); continue ;;
73
83
  *)
74
84
  if [ -z "$ARCHIVE" ]; then ARCHIVE="$a"; fi
75
85
  i=$((i + 1))
@@ -78,9 +88,10 @@ while [ $i -lt ${#ARGS[@]} ]; do
78
88
  esac
79
89
  done
80
90
 
81
- [ -n "$ARCHIVE" ] || arg_fail "missing positional <archive>"
82
- [ "$HAS_OWNER" -eq 1 ] && [ -n "$OWNER_VAL" ] || arg_fail "missing --owner-element-id (or empty value)"
83
- [ "$HAS_SCOPE" -eq 1 ] && [ -n "$SCOPE_VAL" ] || arg_fail "missing --scope (or empty value)"
91
+ [ -n "$ARCHIVE" ] || arg_fail "missing positional <archive>"
92
+ [ "$HAS_OWNER" -eq 1 ] && [ -n "$OWNER_VAL" ] || arg_fail "missing --owner-element-id (or empty value)"
93
+ [ "$HAS_SUBJECT" -eq 1 ] && [ -n "$SUBJECT_VAL" ] || arg_fail "missing --subject-person-id (Task 887: operator-confirmed third-party :Person elementId from preview histogram)"
94
+ [ "$HAS_SCOPE" -eq 1 ] && [ -n "$SCOPE_VAL" ] || arg_fail "missing --scope (or empty value)"
84
95
  case "$SCOPE_VAL" in
85
96
  admin|public) : ;;
86
97
  *) arg_fail "invalid --scope \"$SCOPE_VAL\" (admin|public)" ;;
@@ -177,6 +177,30 @@ function decodeAndNormalise(bytes) {
177
177
  }
178
178
  // Normalise mixed line endings to LF.
179
179
  text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
180
+ // Task 887 — strip Unicode bidi marks (U+200E LRM, U+200F RLM) only at
181
+ // line-start, where some WhatsApp builds prefix the timestamp header.
182
+ // Without stripping, `^\[(\d{2})\/...` fails on the prefixed line, the
183
+ // line is appended as a continuation of the previous body, and the next
184
+ // clean header parses its senderName off the polluted body — leaking 23
185
+ // bogus :Person nodes per import in the Adam Mackay archive. Body-internal
186
+ // bidi marks (e.g. the LRM in `: ‎Forwarded`) are preserved — they carry
187
+ // semantic information about message origin and are exercised by
188
+ // parse-export.test.ts. Counts emitted to stderr for the operator's tail.
189
+ const leadingBidiMatches = text.match(/(?:^|\n)[‎‏]+/g) || [];
190
+ let lrmStripped = 0;
191
+ let rlmStripped = 0;
192
+ for (const m of leadingBidiMatches) {
193
+ for (const ch of m) {
194
+ if (ch === "‎")
195
+ lrmStripped++;
196
+ else if (ch === "‏")
197
+ rlmStripped++;
198
+ }
199
+ }
200
+ if (leadingBidiMatches.length > 0) {
201
+ text = text.replace(/(^|\n)[‎‏]+/g, "$1");
202
+ process.stderr.write(`[whatsapp-ingest] decoded normalised lrm-stripped=${lrmStripped} rlm-stripped=${rlmStripped}\n`);
203
+ }
180
204
  return text;
181
205
  }
182
206
  function matchTimestampPrefix(line, ordering) {
@@ -1 +1 @@
1
- {"version":3,"file":"parse-export.js","sourceRoot":"","sources":["../src/parse-export.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parse-export.js","sourceRoot":"","sources":["../src/parse-export.ts"],"names":[],"mappings":"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"}
@@ -149,6 +149,8 @@ describe("ingest.mjs — missing --filter exits non-zero with LOUD-FAIL line", (
149
149
  stub,
150
150
  "--owner-element-id",
151
151
  "stub-owner",
152
+ "--subject-person-id",
153
+ "stub-subject",
152
154
  "--scope",
153
155
  "admin",
154
156
  ],
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { parseExport } from "../parse-export.js";
6
+
7
+ // Task 887 — bidi-strip regression. Some WhatsApp exports prefix every
8
+ // timestamp header with U+200E (LEFT-TO-RIGHT MARK) or U+200F (RTL MARK).
9
+ // Pre-fix `decodeAndNormalise` left those bytes in place; the timestamp
10
+ // regex (`^\[(\d{2})\/...`) failed; the LRM-prefixed line was glued onto
11
+ // the previous body as a continuation; the next clean header parsed its
12
+ // senderName off the polluted body — leaking 23 :Person nodes per import
13
+ // in the Adam Mackay archive. The fix strips U+200E/U+200F before
14
+ // tokenisation; this test reproduces the failure shape.
15
+
16
+ let workDir: string;
17
+
18
+ beforeEach(() => {
19
+ workDir = mkdtempSync(join(tmpdir(), "whatsapp-export-lrm-"));
20
+ });
21
+
22
+ afterEach(() => {
23
+ rmSync(workDir, { recursive: true, force: true });
24
+ });
25
+
26
+ function writeChat(name: string, content: string): string {
27
+ const filePath = join(workDir, name);
28
+ writeFileSync(filePath, content);
29
+ return filePath;
30
+ }
31
+
32
+ describe("parseExport — bidi-strip (Task 887)", () => {
33
+ it("strips U+200E from timestamp headers and parses each row independently", () => {
34
+ const LRM = "‎";
35
+ const filePath = writeChat(
36
+ "_chat.txt",
37
+ [
38
+ `${LRM}[04/02/26, 11:52:16] Adam Mackay: hi`,
39
+ `${LRM}[04/02/26, 11:52:30] Joel Smalley: hey`,
40
+ "",
41
+ ].join("\n"),
42
+ );
43
+
44
+ const result = parseExport({
45
+ filePath,
46
+ accountId: "acct-887",
47
+ timezone: "Europe/London",
48
+ });
49
+
50
+ expect(result.parsedLines).toHaveLength(2);
51
+ expect(result.parsedLines.map((l) => l.senderName).sort()).toEqual([
52
+ "Adam Mackay",
53
+ "Joel Smalley",
54
+ ]);
55
+ for (const line of result.parsedLines) {
56
+ expect(line.senderName).not.toContain("\n");
57
+ expect(line.senderName).not.toContain("[");
58
+ expect(line.senderName).not.toContain(LRM);
59
+ }
60
+ });
61
+
62
+ it("strips U+200F (RLM) on the timestamp line", () => {
63
+ const RLM = "‏";
64
+ const filePath = writeChat(
65
+ "_chat.txt",
66
+ [
67
+ `${RLM}[14/03/26, 10:15:23] Joel: Hello`,
68
+ `${RLM}[14/03/26, 10:16:01] Sarah: Hi back`,
69
+ "",
70
+ ].join("\n"),
71
+ );
72
+
73
+ const result = parseExport({
74
+ filePath,
75
+ accountId: "acct-887",
76
+ timezone: "Europe/London",
77
+ });
78
+
79
+ expect(result.parsedLines).toHaveLength(2);
80
+ expect(result.parsedLines[0].senderName).toBe("Joel");
81
+ expect(result.parsedLines[1].senderName).toBe("Sarah");
82
+ });
83
+ });
@@ -292,6 +292,31 @@ function decodeAndNormalise(bytes: Buffer): string {
292
292
  // Normalise mixed line endings to LF.
293
293
  text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
294
294
 
295
+ // Task 887 — strip Unicode bidi marks (U+200E LRM, U+200F RLM) only at
296
+ // line-start, where some WhatsApp builds prefix the timestamp header.
297
+ // Without stripping, `^\[(\d{2})\/...` fails on the prefixed line, the
298
+ // line is appended as a continuation of the previous body, and the next
299
+ // clean header parses its senderName off the polluted body — leaking 23
300
+ // bogus :Person nodes per import in the Adam Mackay archive. Body-internal
301
+ // bidi marks (e.g. the LRM in `: ‎Forwarded`) are preserved — they carry
302
+ // semantic information about message origin and are exercised by
303
+ // parse-export.test.ts. Counts emitted to stderr for the operator's tail.
304
+ const leadingBidiMatches = text.match(/(?:^|\n)[‎‏]+/g) || [];
305
+ let lrmStripped = 0;
306
+ let rlmStripped = 0;
307
+ for (const m of leadingBidiMatches) {
308
+ for (const ch of m) {
309
+ if (ch === "‎") lrmStripped++;
310
+ else if (ch === "‏") rlmStripped++;
311
+ }
312
+ }
313
+ if (leadingBidiMatches.length > 0) {
314
+ text = text.replace(/(^|\n)[‎‏]+/g, "$1");
315
+ process.stderr.write(
316
+ `[whatsapp-ingest] decoded normalised lrm-stripped=${lrmStripped} rlm-stripped=${rlmStripped}\n`,
317
+ );
318
+ }
319
+
295
320
  return text;
296
321
  }
297
322