@rubytech/create-maxy 1.0.808 → 1.0.809
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/plugins/memory/mcp/dist/index.js +86 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.d.ts +23 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.js +401 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.d.ts +28 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.js +34 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.js.map +1 -0
- package/payload/platform/plugins/memory/references/schema-base.md +12 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +3 -1
- package/payload/platform/plugins/whatsapp-import/bin/ingest.mjs +225 -346
- package/payload/platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh +28 -10
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.d.ts +21 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.d.ts.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.js +41 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.js.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/filter.d.ts +29 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/filter.d.ts.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/filter.js +123 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/filter.js.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.d.ts +4 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.js +9 -1
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/filter-gate.test.ts +170 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/ingest-idempotence.test.ts +141 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/derive-keys.ts +59 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/filter.ts +136 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/index.ts +12 -0
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +80 -25
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import-enrich/SKILL.md +22 -3
- package/payload/platform/templates/specialists/agents/database-operator.md +9 -4
- package/payload/server/public/assets/admin-Bwrd2DBq.js +352 -0
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +271 -188
- package/payload/server/public/assets/admin-MxaCgGHZ.js +0 -352
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# =============================================================================
|
|
3
3
|
# whatsapp-ingest.sh — single deterministic Bash entry for WhatsApp archive
|
|
4
|
-
# ingestion (Task 855). Thin wrapper: arg-validate, resolve env,
|
|
5
|
-
# ingest.mjs in-process. The script is the database-operator
|
|
6
|
-
# operator-facing handle on the parse → archive-write
|
|
4
|
+
# ingestion (Task 855 / Task 871). Thin wrapper: arg-validate, resolve env,
|
|
5
|
+
# invoke ingest.mjs in-process. The script is the database-operator
|
|
6
|
+
# subagent's only operator-facing handle on the parse → filter → archive-write
|
|
7
|
+
# pipeline. Phase 1 has NO LLM. The Haiku insight pass is Phase 2 — invoked
|
|
8
|
+
# consciously via `mcp__memory__whatsapp-export-insight-pass`.
|
|
7
9
|
#
|
|
8
10
|
# Usage:
|
|
9
11
|
# bash whatsapp-ingest.sh <archive.zip|dir|_chat.txt>
|
|
10
12
|
# --owner-element-id <id>
|
|
11
13
|
# --scope <admin|public>
|
|
14
|
+
# --filter <all|senders=<csv>|date-range=<isoFrom>..<isoTo>>
|
|
12
15
|
# [--account-id <accountId>]
|
|
13
16
|
# [--timezone <iana-zone>]
|
|
14
17
|
# [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>]
|
|
15
|
-
#
|
|
18
|
+
#
|
|
19
|
+
# `--filter` is mandatory (Task 871). Forms:
|
|
20
|
+
# all — write every parsed row
|
|
21
|
+
# senders=Alice,Bob Carter — keep rows whose senderName ∈ csv
|
|
22
|
+
# date-range=2024-01-01..2024-06-30
|
|
23
|
+
# — keep rows whose dateSent falls inside
|
|
24
|
+
# the inclusive range (date-only or full
|
|
25
|
+
# ISO 8601 endpoints both accepted)
|
|
16
26
|
#
|
|
17
27
|
# Exit 0 + JSON summary on stdout on success.
|
|
18
28
|
# Exit !0 + one [whatsapp-ingest] FAIL line on stderr on failure.
|
|
@@ -45,18 +55,20 @@ fi
|
|
|
45
55
|
ARCHIVE=""
|
|
46
56
|
OWNER_VAL=""
|
|
47
57
|
SCOPE_VAL=""
|
|
58
|
+
FILTER_VAL=""
|
|
48
59
|
HAS_OWNER=0
|
|
49
60
|
HAS_SCOPE=0
|
|
61
|
+
HAS_FILTER=0
|
|
50
62
|
|
|
51
63
|
ARGS=("$@")
|
|
52
64
|
i=0
|
|
53
65
|
while [ $i -lt ${#ARGS[@]} ]; do
|
|
54
66
|
a="${ARGS[$i]}"
|
|
55
67
|
case "$a" in
|
|
56
|
-
--owner-element-id) HAS_OWNER=1;
|
|
57
|
-
--scope) HAS_SCOPE=1;
|
|
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 ;;
|
|
58
71
|
--account-id|--timezone|--date-format) i=$((i + 2)); continue ;;
|
|
59
|
-
--no-insight) i=$((i + 1)); continue ;;
|
|
60
72
|
--*) i=$((i + 2)); continue ;;
|
|
61
73
|
*)
|
|
62
74
|
if [ -z "$ARCHIVE" ]; then ARCHIVE="$a"; fi
|
|
@@ -66,13 +78,19 @@ while [ $i -lt ${#ARGS[@]} ]; do
|
|
|
66
78
|
esac
|
|
67
79
|
done
|
|
68
80
|
|
|
69
|
-
[ -n "$ARCHIVE" ]
|
|
70
|
-
[ "$HAS_OWNER"
|
|
71
|
-
[ "$HAS_SCOPE"
|
|
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)"
|
|
72
84
|
case "$SCOPE_VAL" in
|
|
73
85
|
admin|public) : ;;
|
|
74
86
|
*) arg_fail "invalid --scope \"$SCOPE_VAL\" (admin|public)" ;;
|
|
75
87
|
esac
|
|
88
|
+
if [ "$HAS_FILTER" -ne 1 ] || [ -z "$FILTER_VAL" ]; then
|
|
89
|
+
# Mirror ingest.mjs's pinned LOUD-FAIL line so a single grep covers both
|
|
90
|
+
# layers — the operator's runbook recipe is `grep '\[whatsapp-ingest\] FAIL filter-required'`.
|
|
91
|
+
echo "[whatsapp-ingest] FAIL filter-required reason=\"bulk-archive-gate (Task 871) — operator must specify --filter (one of all, senders=<csv>, date-range=<isoFrom>..<isoTo>)\"" >&2
|
|
92
|
+
arg_fail "missing --filter (one of all, senders=<csv>, date-range=<isoFrom>..<isoTo>)"
|
|
93
|
+
fi
|
|
76
94
|
|
|
77
95
|
# Lift NEO4J_PASSWORD from the install's config file when env doesn't carry it
|
|
78
96
|
# (e.g. operator running directly from a shell rather than via the platform
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function normaliseSenderName(name: string): string;
|
|
2
|
+
export declare function sha256Hex(input: string): string;
|
|
3
|
+
export interface DeriveMessageIdInput {
|
|
4
|
+
/** SHA-256 of the source `_chat.txt` bytes — stable across re-imports. */
|
|
5
|
+
conversationSha256: string;
|
|
6
|
+
/** ISO 8601 with timezone offset, as emitted by parseExport. */
|
|
7
|
+
dateSent: string;
|
|
8
|
+
/** Raw senderName from the export line. Normalised internally. */
|
|
9
|
+
senderName: string;
|
|
10
|
+
/** Raw message body. Hashed internally. */
|
|
11
|
+
body: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function deriveMessageId(input: DeriveMessageIdInput): string;
|
|
14
|
+
export interface ObservationContentFields {
|
|
15
|
+
summary?: string | null;
|
|
16
|
+
from?: string | null;
|
|
17
|
+
to?: string | null;
|
|
18
|
+
subject?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function observationContentHash(fields: ObservationContentFields): string;
|
|
21
|
+
//# sourceMappingURL=derive-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive-keys.d.ts","sourceRoot":"","sources":["../src/derive-keys.ts"],"names":[],"mappings":"AAqBA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAInE;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAK/E"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normaliseSenderName = normaliseSenderName;
|
|
4
|
+
exports.sha256Hex = sha256Hex;
|
|
5
|
+
exports.deriveMessageId = deriveMessageId;
|
|
6
|
+
exports.observationContentHash = observationContentHash;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// derive-keys — natural-key derivation for whatsapp-import (Task 870).
|
|
10
|
+
//
|
|
11
|
+
// Pure functions. No I/O. The whole point is that re-imports of the same
|
|
12
|
+
// archive collapse to the same Message identity regardless of release-level
|
|
13
|
+
// drift in array indices, hash widths, or arbitrary tiebreakers.
|
|
14
|
+
//
|
|
15
|
+
// Key shape (Task 870 brief):
|
|
16
|
+
//
|
|
17
|
+
// messageId = whatsapp-export:msg:<conversationSha256>:<dateSentISO>
|
|
18
|
+
// :<NFKC-trim-lower(senderName)>
|
|
19
|
+
// :<sha256-hex(body)>
|
|
20
|
+
//
|
|
21
|
+
// Operator constraint: the same archive must be re-imported with the same
|
|
22
|
+
// `--timezone` flag. Different timezones reinterpret wall-clock instants and
|
|
23
|
+
// will produce drifted messageIds — that is correct semantics, not a bug.
|
|
24
|
+
// Documented in .docs/whatsapp.md natural-key contract section.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function normaliseSenderName(name) {
|
|
27
|
+
return name.normalize("NFKC").trim().toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
function sha256Hex(input) {
|
|
30
|
+
return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
function deriveMessageId(input) {
|
|
33
|
+
const norm = normaliseSenderName(input.senderName);
|
|
34
|
+
const bodyHash = sha256Hex(input.body);
|
|
35
|
+
return `whatsapp-export:msg:${input.conversationSha256}:${input.dateSent}:${norm}:${bodyHash}`;
|
|
36
|
+
}
|
|
37
|
+
function observationContentHash(fields) {
|
|
38
|
+
const parts = [fields.summary, fields.from, fields.to, fields.subject].map((p) => (p ?? "").normalize("NFKC").trim().toLowerCase());
|
|
39
|
+
return sha256Hex(parts.join("|"));
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=derive-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive-keys.js","sourceRoot":"","sources":["../src/derive-keys.ts"],"names":[],"mappings":";;AAqBA,kDAEC;AAED,8BAEC;AAaD,0CAIC;AASD,wDAKC;AA1DD,6CAAyC;AAEzC,8EAA8E;AAC9E,uEAAuE;AACvE,EAAE;AACF,yEAAyE;AACzE,4EAA4E;AAC5E,iEAAiE;AACjE,EAAE;AACF,8BAA8B;AAC9B,EAAE;AACF,uEAAuE;AACvE,kEAAkE;AAClE,uDAAuD;AACvD,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,0EAA0E;AAC1E,gEAAgE;AAChE,8EAA8E;AAE9E,SAAgB,mBAAmB,CAAC,IAAY;IAC9C,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACrD,CAAC;AAED,SAAgB,SAAS,CAAC,KAAa;IACrC,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAaD,SAAgB,eAAe,CAAC,KAA2B;IACzD,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,uBAAuB,KAAK,CAAC,kBAAkB,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,IAAI,QAAQ,EAAE,CAAC;AACjG,CAAC;AASD,SAAgB,sBAAsB,CAAC,MAAgC;IACrE,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CACxE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CACxD,CAAC;IACF,OAAO,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ParsedLine } from "./parse-export.js";
|
|
2
|
+
export type Filter = {
|
|
3
|
+
kind: "all";
|
|
4
|
+
} | {
|
|
5
|
+
kind: "senders";
|
|
6
|
+
senders: string[];
|
|
7
|
+
} | {
|
|
8
|
+
kind: "date-range";
|
|
9
|
+
fromIso: string;
|
|
10
|
+
toIso: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Parse a CLI `--filter` argument into a structured Filter.
|
|
14
|
+
*
|
|
15
|
+
* Throws Error with message starting "filter: …" on malformed input. The
|
|
16
|
+
* caller (ingest.mjs / vitest) surfaces the reason verbatim — the brief
|
|
17
|
+
* mandates `[whatsapp-ingest] FAIL filter-required reason="…"` so the
|
|
18
|
+
* operator can grep one line.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseFilterArg(raw: string | undefined | null): Filter;
|
|
21
|
+
/**
|
|
22
|
+
* Apply a parsed Filter to ParsedLine[]. Returns a new array of kept lines
|
|
23
|
+
* with the parser's original `sequenceIndex` preserved (the filter never
|
|
24
|
+
* reorders). ingest.mjs re-stamps `sequenceIndex` to its post-filter position
|
|
25
|
+
* during row construction for archive-write — re-stamping here too would be
|
|
26
|
+
* redundant.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyFilter(parsedLines: readonly ParsedLine[], filter: Filter): ParsedLine[];
|
|
29
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,MAAM,MAAM,GACd;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CA+CrE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,WAAW,EAAE,SAAS,UAAU,EAAE,EAClC,MAAM,EAAE,MAAM,GACb,UAAU,EAAE,CAQd"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// filter — operator-supplied gate over ParsedLine[] (Task 871).
|
|
4
|
+
//
|
|
5
|
+
// Phase 1 ingest is now mandatory-filter: the deterministic Bash entry refuses
|
|
6
|
+
// to write a bulk archive without `--filter`. Three forms cover the operator
|
|
7
|
+
// patterns named in the brief:
|
|
8
|
+
//
|
|
9
|
+
// --filter all → no row drop
|
|
10
|
+
// --filter senders=Alice,Bob Carter → keep rows whose
|
|
11
|
+
// senderName matches
|
|
12
|
+
// any csv entry exactly
|
|
13
|
+
// --filter date-range=2024-01-01..2024-06-30 → keep rows whose
|
|
14
|
+
// dateSent ISO falls
|
|
15
|
+
// inside the inclusive
|
|
16
|
+
// range (date or full
|
|
17
|
+
// ISO 8601)
|
|
18
|
+
//
|
|
19
|
+
// Doctrine alignment:
|
|
20
|
+
// - feedback_compress_at_ingest_for_bulk_archives.md — the gate is
|
|
21
|
+
// mandatory at write-time, not after.
|
|
22
|
+
// - feedback_deterministic_means_remove_llm.md — the filter parser is a
|
|
23
|
+
// pure function, no LLM in the per-row decision path.
|
|
24
|
+
// - feedback_loud_failures.md — malformed `--filter` raises a structured
|
|
25
|
+
// error with a named reason rather than silently coercing to `all`.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.parseFilterArg = parseFilterArg;
|
|
29
|
+
exports.applyFilter = applyFilter;
|
|
30
|
+
/**
|
|
31
|
+
* Parse a CLI `--filter` argument into a structured Filter.
|
|
32
|
+
*
|
|
33
|
+
* Throws Error with message starting "filter: …" on malformed input. The
|
|
34
|
+
* caller (ingest.mjs / vitest) surfaces the reason verbatim — the brief
|
|
35
|
+
* mandates `[whatsapp-ingest] FAIL filter-required reason="…"` so the
|
|
36
|
+
* operator can grep one line.
|
|
37
|
+
*/
|
|
38
|
+
function parseFilterArg(raw) {
|
|
39
|
+
if (raw == null || raw.trim() === "") {
|
|
40
|
+
throw new Error('filter: --filter is required (one of "all", "senders=<csv>", "date-range=<isoFrom>..<isoTo>")');
|
|
41
|
+
}
|
|
42
|
+
const value = raw.trim();
|
|
43
|
+
if (value === "all")
|
|
44
|
+
return { kind: "all" };
|
|
45
|
+
if (value.startsWith("senders=")) {
|
|
46
|
+
const csv = value.slice("senders=".length);
|
|
47
|
+
const senders = csv
|
|
48
|
+
.split(",")
|
|
49
|
+
.map((s) => s.trim())
|
|
50
|
+
.filter((s) => s.length > 0);
|
|
51
|
+
if (senders.length === 0) {
|
|
52
|
+
throw new Error('filter: senders= requires at least one comma-separated name');
|
|
53
|
+
}
|
|
54
|
+
return { kind: "senders", senders };
|
|
55
|
+
}
|
|
56
|
+
if (value.startsWith("date-range=")) {
|
|
57
|
+
const range = value.slice("date-range=".length);
|
|
58
|
+
const parts = range.split("..");
|
|
59
|
+
if (parts.length !== 2) {
|
|
60
|
+
throw new Error(`filter: date-range must be "<isoFrom>..<isoTo>" — got "${range}"`);
|
|
61
|
+
}
|
|
62
|
+
const [fromIso, toIso] = parts.map((p) => p.trim());
|
|
63
|
+
if (!fromIso || !toIso) {
|
|
64
|
+
throw new Error(`filter: date-range requires both endpoints — got "${range}"`);
|
|
65
|
+
}
|
|
66
|
+
if (Number.isNaN(Date.parse(fromIso))) {
|
|
67
|
+
throw new Error(`filter: date-range fromIso="${fromIso}" is not parseable as ISO 8601`);
|
|
68
|
+
}
|
|
69
|
+
if (Number.isNaN(Date.parse(toIso))) {
|
|
70
|
+
throw new Error(`filter: date-range toIso="${toIso}" is not parseable as ISO 8601`);
|
|
71
|
+
}
|
|
72
|
+
if (Date.parse(fromIso) > Date.parse(toIso)) {
|
|
73
|
+
throw new Error(`filter: date-range fromIso="${fromIso}" is later than toIso="${toIso}"`);
|
|
74
|
+
}
|
|
75
|
+
return { kind: "date-range", fromIso, toIso };
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`filter: unrecognised form "${value}" — must be "all", "senders=<csv>", or "date-range=<isoFrom>..<isoTo>"`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Apply a parsed Filter to ParsedLine[]. Returns a new array of kept lines
|
|
81
|
+
* with the parser's original `sequenceIndex` preserved (the filter never
|
|
82
|
+
* reorders). ingest.mjs re-stamps `sequenceIndex` to its post-filter position
|
|
83
|
+
* during row construction for archive-write — re-stamping here too would be
|
|
84
|
+
* redundant.
|
|
85
|
+
*/
|
|
86
|
+
function applyFilter(parsedLines, filter) {
|
|
87
|
+
const predicate = makePredicate(filter);
|
|
88
|
+
const kept = [];
|
|
89
|
+
for (const line of parsedLines) {
|
|
90
|
+
if (!predicate(line))
|
|
91
|
+
continue;
|
|
92
|
+
kept.push(line);
|
|
93
|
+
}
|
|
94
|
+
return kept;
|
|
95
|
+
}
|
|
96
|
+
function makePredicate(filter) {
|
|
97
|
+
if (filter.kind === "all")
|
|
98
|
+
return () => true;
|
|
99
|
+
if (filter.kind === "senders") {
|
|
100
|
+
const allow = new Set(filter.senders);
|
|
101
|
+
return (line) => allow.has(line.senderName);
|
|
102
|
+
}
|
|
103
|
+
// date-range: inclusive on both ends. Date-only endpoints widen to whole-
|
|
104
|
+
// day semantics: `from=YYYY-MM-DD` → `T00:00:00Z`, `to=YYYY-MM-DD` →
|
|
105
|
+
// `T23:59:59.999Z`. Full ISO 8601 endpoints with `T` are passed through.
|
|
106
|
+
// Without this widening, `--filter date-range=2024-01-01..2024-06-30`
|
|
107
|
+
// would silently drop every message later than 2024-06-30T00:00:00Z on the
|
|
108
|
+
// last day — a UX trap that contradicts the operator's reading.
|
|
109
|
+
const fromMs = parseRangeEndpoint(filter.fromIso, "start");
|
|
110
|
+
const toMs = parseRangeEndpoint(filter.toIso, "end");
|
|
111
|
+
return (line) => {
|
|
112
|
+
const ms = Date.parse(line.dateSent);
|
|
113
|
+
return ms >= fromMs && ms <= toMs;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function parseRangeEndpoint(iso, edge) {
|
|
117
|
+
if (/T/.test(iso))
|
|
118
|
+
return Date.parse(iso);
|
|
119
|
+
// Date-only — widen to whole-day inclusive on the requested edge.
|
|
120
|
+
const suffix = edge === "start" ? "T00:00:00.000Z" : "T23:59:59.999Z";
|
|
121
|
+
return Date.parse(iso + suffix);
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { parseExport } from "./parse-export.js";
|
|
2
2
|
export type { ParseExportInput, ParseExportResult, ParseExportCounters, ParsedLine, } from "./parse-export.js";
|
|
3
|
+
export { parseFilterArg, applyFilter } from "./filter.js";
|
|
4
|
+
export type { Filter } from "./filter.js";
|
|
5
|
+
export { normaliseSenderName, sha256Hex, deriveMessageId, observationContentHash, } from "./derive-keys.js";
|
|
6
|
+
export type { DeriveMessageIdInput, ObservationContentFields, } from "./derive-keys.js";
|
|
3
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,UAAU,GACX,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,UAAU,GACX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseExport = void 0;
|
|
3
|
+
exports.observationContentHash = exports.deriveMessageId = exports.sha256Hex = exports.normaliseSenderName = exports.applyFilter = exports.parseFilterArg = exports.parseExport = void 0;
|
|
4
4
|
var parse_export_js_1 = require("./parse-export.js");
|
|
5
5
|
Object.defineProperty(exports, "parseExport", { enumerable: true, get: function () { return parse_export_js_1.parseExport; } });
|
|
6
|
+
var filter_js_1 = require("./filter.js");
|
|
7
|
+
Object.defineProperty(exports, "parseFilterArg", { enumerable: true, get: function () { return filter_js_1.parseFilterArg; } });
|
|
8
|
+
Object.defineProperty(exports, "applyFilter", { enumerable: true, get: function () { return filter_js_1.applyFilter; } });
|
|
9
|
+
var derive_keys_js_1 = require("./derive-keys.js");
|
|
10
|
+
Object.defineProperty(exports, "normaliseSenderName", { enumerable: true, get: function () { return derive_keys_js_1.normaliseSenderName; } });
|
|
11
|
+
Object.defineProperty(exports, "sha256Hex", { enumerable: true, get: function () { return derive_keys_js_1.sha256Hex; } });
|
|
12
|
+
Object.defineProperty(exports, "deriveMessageId", { enumerable: true, get: function () { return derive_keys_js_1.deriveMessageId; } });
|
|
13
|
+
Object.defineProperty(exports, "observationContentHash", { enumerable: true, get: function () { return derive_keys_js_1.observationContentHash; } });
|
|
6
14
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA;AAOpB,yCAA0D;AAAjD,2GAAA,cAAc,OAAA;AAAE,wGAAA,WAAW,OAAA;AAEpC,mDAK0B;AAJxB,qHAAA,mBAAmB,OAAA;AACnB,2GAAA,SAAS,OAAA;AACT,iHAAA,eAAe,OAAA;AACf,wHAAA,sBAAsB,OAAA"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { parseFilterArg, applyFilter } from "../filter.js";
|
|
9
|
+
import type { ParsedLine } from "../parse-export.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// filter-gate — Task 871 contract: --filter is mandatory, three forms.
|
|
13
|
+
//
|
|
14
|
+
// The unit tests cover the helper module's pure-function surface (parse +
|
|
15
|
+
// apply). The integration test spawns ingest.mjs as a subprocess and
|
|
16
|
+
// asserts the no-flag invocation exits non-zero with the LOUD-FAIL line
|
|
17
|
+
// (`[whatsapp-ingest] FAIL phase=argv reason="--filter is required …"`)
|
|
18
|
+
// before any Neo4j connection — argv parsing fires before imports/sessions.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
// src/__tests__/ → ../../bin/ingest.mjs (up to src/, up to lib/, up to plugin
|
|
23
|
+
// root, then bin/).
|
|
24
|
+
const INGEST_MJS = resolve(__dirname, "..", "..", "..", "bin", "ingest.mjs");
|
|
25
|
+
|
|
26
|
+
function lines(parsed: ParsedLine[]): string[] {
|
|
27
|
+
return parsed.map((p) => `${p.senderName}@${p.dateSent}:${p.body}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SAMPLE: ParsedLine[] = [
|
|
31
|
+
{ senderName: "Alice", dateSent: "2024-01-15T09:00:00+00:00", body: "morning", sequenceIndex: 0 },
|
|
32
|
+
{ senderName: "Bob", dateSent: "2024-01-15T09:05:00+00:00", body: "hi", sequenceIndex: 1 },
|
|
33
|
+
{ senderName: "Alice", dateSent: "2024-03-20T11:00:00+00:00", body: "lunch?", sequenceIndex: 2 },
|
|
34
|
+
{ senderName: "Carol", dateSent: "2024-06-30T23:59:59+00:00: ", body: "bye", sequenceIndex: 3 },
|
|
35
|
+
{ senderName: "Bob", dateSent: "2024-07-01T00:00:01+00:00", body: "next", sequenceIndex: 4 },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Repair the malformed dateSent fixture (extra ':' suffix would Date.parse to
|
|
39
|
+
// NaN). Keep this in code, not in the const above, so the fixture reads
|
|
40
|
+
// natural while we still hold a strict tuple.
|
|
41
|
+
SAMPLE[3] = { ...SAMPLE[3], dateSent: "2024-06-30T23:59:59+00:00" };
|
|
42
|
+
|
|
43
|
+
describe("parseFilterArg — happy path", () => {
|
|
44
|
+
it('parses --filter all', () => {
|
|
45
|
+
expect(parseFilterArg("all")).toEqual({ kind: "all" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('parses senders=<csv> with whitespace tolerance', () => {
|
|
49
|
+
expect(parseFilterArg("senders=Alice, Bob Carter , Carol")).toEqual({
|
|
50
|
+
kind: "senders",
|
|
51
|
+
senders: ["Alice", "Bob Carter", "Carol"],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('parses date-range=<from>..<to>', () => {
|
|
56
|
+
expect(parseFilterArg("date-range=2024-01-01..2024-06-30")).toEqual({
|
|
57
|
+
kind: "date-range",
|
|
58
|
+
fromIso: "2024-01-01",
|
|
59
|
+
toIso: "2024-06-30",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("parseFilterArg — LOUD-FAIL on malformed input", () => {
|
|
65
|
+
it("rejects undefined / empty", () => {
|
|
66
|
+
expect(() => parseFilterArg(undefined)).toThrow(/--filter is required/);
|
|
67
|
+
expect(() => parseFilterArg("")).toThrow(/--filter is required/);
|
|
68
|
+
expect(() => parseFilterArg(" ")).toThrow(/--filter is required/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects unknown form", () => {
|
|
72
|
+
expect(() => parseFilterArg("everyone")).toThrow(/unrecognised form/);
|
|
73
|
+
expect(() => parseFilterArg("nope=yes")).toThrow(/unrecognised form/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("rejects senders= with no names", () => {
|
|
77
|
+
expect(() => parseFilterArg("senders=")).toThrow(/at least one/);
|
|
78
|
+
expect(() => parseFilterArg("senders=,,,")).toThrow(/at least one/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("rejects date-range with malformed shape or unparseable ISO", () => {
|
|
82
|
+
expect(() => parseFilterArg("date-range=2024-01-01")).toThrow(/<isoFrom>\.\.<isoTo>/);
|
|
83
|
+
expect(() => parseFilterArg("date-range=..2024-06-30")).toThrow(/both endpoints/);
|
|
84
|
+
expect(() => parseFilterArg("date-range=2024-01-01..")).toThrow(/both endpoints/);
|
|
85
|
+
expect(() => parseFilterArg("date-range=not-a-date..2024-06-30")).toThrow(/not parseable/);
|
|
86
|
+
expect(() => parseFilterArg("date-range=2024-06-30..2024-01-01")).toThrow(/later than/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("applyFilter — semantics", () => {
|
|
91
|
+
it("--filter all keeps every row, preserves the parser's original sequenceIndex", () => {
|
|
92
|
+
const out = applyFilter(SAMPLE, parseFilterArg("all"));
|
|
93
|
+
expect(out.length).toBe(SAMPLE.length);
|
|
94
|
+
expect(out.map((l) => l.sequenceIndex)).toEqual([0, 1, 2, 3, 4]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("--filter senders=Alice,Bob keeps only those senders, sequenceIndex stays as the parser-stamped value (Carol's index 3 drops out)", () => {
|
|
98
|
+
const out = applyFilter(SAMPLE, parseFilterArg("senders=Alice,Bob"));
|
|
99
|
+
expect(lines(out)).toEqual([
|
|
100
|
+
"Alice@2024-01-15T09:00:00+00:00:morning",
|
|
101
|
+
"Bob@2024-01-15T09:05:00+00:00:hi",
|
|
102
|
+
"Alice@2024-03-20T11:00:00+00:00:lunch?",
|
|
103
|
+
"Bob@2024-07-01T00:00:01+00:00:next",
|
|
104
|
+
]);
|
|
105
|
+
expect(out.map((l) => l.sequenceIndex)).toEqual([0, 1, 2, 4]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("--filter date-range clips inclusively on both ends", () => {
|
|
109
|
+
const out = applyFilter(SAMPLE, parseFilterArg("date-range=2024-01-01..2024-06-30"));
|
|
110
|
+
// 2024-06-30T23:59:59 IS inclusive; 2024-07-01 is NOT.
|
|
111
|
+
expect(out.map((l) => `${l.senderName}/${l.dateSent}`)).toEqual([
|
|
112
|
+
"Alice/2024-01-15T09:00:00+00:00",
|
|
113
|
+
"Bob/2024-01-15T09:05:00+00:00",
|
|
114
|
+
"Alice/2024-03-20T11:00:00+00:00",
|
|
115
|
+
"Carol/2024-06-30T23:59:59+00:00",
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("--filter date-range with full ISO 8601 endpoints", () => {
|
|
120
|
+
const out = applyFilter(
|
|
121
|
+
SAMPLE,
|
|
122
|
+
parseFilterArg("date-range=2024-03-01T00:00:00Z..2024-06-15T00:00:00Z"),
|
|
123
|
+
);
|
|
124
|
+
expect(out).toHaveLength(1);
|
|
125
|
+
expect(out[0].senderName).toBe("Alice");
|
|
126
|
+
expect(out[0].body).toBe("lunch?");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("an unmatched senders filter returns []", () => {
|
|
130
|
+
const out = applyFilter(SAMPLE, parseFilterArg("senders=Nobody"));
|
|
131
|
+
expect(out).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("ingest.mjs — missing --filter exits non-zero with LOUD-FAIL line", () => {
|
|
136
|
+
it("emits [whatsapp-ingest] FAIL phase=argv reason=\"--filter required ...\" before touching Neo4j", () => {
|
|
137
|
+
// Write a stub _chat.txt; argv parse fires first and rejects the call
|
|
138
|
+
// before resolveChatTxt/Neo4j connection. We do NOT need a real archive
|
|
139
|
+
// to test the gate.
|
|
140
|
+
const work = mkdtempSync(join(tmpdir(), "filter-gate-"));
|
|
141
|
+
try {
|
|
142
|
+
const stub = join(work, "_chat.txt");
|
|
143
|
+
writeFileSync(stub, "[14/03/26, 10:15:23] Joel: stub\n");
|
|
144
|
+
|
|
145
|
+
const res = spawnSync(
|
|
146
|
+
"node",
|
|
147
|
+
[
|
|
148
|
+
INGEST_MJS,
|
|
149
|
+
stub,
|
|
150
|
+
"--owner-element-id",
|
|
151
|
+
"stub-owner",
|
|
152
|
+
"--scope",
|
|
153
|
+
"admin",
|
|
154
|
+
],
|
|
155
|
+
{ encoding: "utf8" },
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(res.status).not.toBe(0);
|
|
159
|
+
const stderr = res.stderr ?? "";
|
|
160
|
+
expect(stderr).toMatch(/\[whatsapp-ingest\] FAIL phase=argv/);
|
|
161
|
+
expect(stderr).toMatch(/reason="--filter is required/);
|
|
162
|
+
// Confirm the gate fired BEFORE any Neo4j or import work — no
|
|
163
|
+
// [whatsapp-ingest] start line, no archive-write log line.
|
|
164
|
+
expect(stderr).not.toMatch(/\[whatsapp-ingest\] start /);
|
|
165
|
+
expect(stderr).not.toMatch(/\[memory-archive-write\]/);
|
|
166
|
+
} finally {
|
|
167
|
+
rmSync(work, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|