@rubytech/create-maxy-code 0.1.119 → 0.1.121

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.119",
3
+ "version": "0.1.121",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -355,7 +355,7 @@ FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organi
355
355
  |Task|Project|Event
356
356
  |Workflow|WorkflowStep|WorkflowRun|StepResult
357
357
  |Email|EmailAccount
358
- |Position|Credential|Agent)
358
+ |Position|Credential|Agent|Idea)
359
359
  ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
360
360
  n.title, n.currentTitle, n.summary, n.body, n.content, n.text, n.description, n.headline, n.abstract,
361
361
  n.email, n.note, n.label, n.value, n.message, n.preview, n.tagline,
@@ -1204,3 +1204,94 @@ FOR (a:AEOAudit) ON (a.target);
1204
1204
 
1205
1205
  CREATE INDEX aeo_audit_run_at IF NOT EXISTS
1206
1206
  FOR (a:AEOAudit) ON (a.runAt);
1207
+
1208
+ // ----------------------------------------------------------
1209
+ // Visitor graph (Task 357)
1210
+ //
1211
+ // Pixel emits pageview/click/scroll/dwell to POST /v/event. Events land
1212
+ // under :Session, attributed to :Person (via signed-token cookie) or
1213
+ // :AnonVisitor (cookie-less). When a previously anonymous browser
1214
+ // completes its first signed-token click, the AnonVisitor merges into
1215
+ // the Person and prior :Session edges reattribute. Schema-org-ish:
1216
+ // Session/PageView/Click are bespoke (no schema.org coverage of analytics);
1217
+ // Page is `schema:WebPage`-shaped (url, name).
1218
+ //
1219
+ // Multi-tenancy: every node carries accountId. Uniqueness constraints
1220
+ // are composite (accountId, idKey) — sessionId/visitorId/url collisions
1221
+ // across accounts are valid.
1222
+ //
1223
+ // Cascade: contact-erase on :Person trashes (:Session)-[:HAS_EVENT]->(*)
1224
+ // the same way profile-delete cascades preferences. :Page and :Listing
1225
+ // are content metadata and intentionally survive (no PII).
1226
+ // ----------------------------------------------------------
1227
+
1228
+ CREATE CONSTRAINT session_id_unique IF NOT EXISTS
1229
+ FOR (s:Session) REQUIRE (s.accountId, s.sessionId) IS UNIQUE;
1230
+
1231
+ CREATE INDEX session_account IF NOT EXISTS
1232
+ FOR (s:Session) ON (s.accountId);
1233
+
1234
+ CREATE INDEX session_started_at IF NOT EXISTS
1235
+ FOR (s:Session) ON (s.startedAt);
1236
+
1237
+ CREATE CONSTRAINT anon_visitor_id_unique IF NOT EXISTS
1238
+ FOR (av:AnonVisitor) REQUIRE (av.accountId, av.visitorId) IS UNIQUE;
1239
+
1240
+ CREATE INDEX anon_visitor_account IF NOT EXISTS
1241
+ FOR (av:AnonVisitor) ON (av.accountId);
1242
+
1243
+ CREATE CONSTRAINT page_url_unique IF NOT EXISTS
1244
+ FOR (pg:Page) REQUIRE (pg.accountId, pg.url) IS UNIQUE;
1245
+
1246
+ CREATE INDEX page_account IF NOT EXISTS
1247
+ FOR (pg:Page) ON (pg.accountId);
1248
+
1249
+ // PageView/Click/ScrollMilestone/Recommendation: no uniqueness, just
1250
+ // accountId index. ScrollMilestone is a roll-up (one per PageView,
1251
+ // updated in place) so a uniqueness constraint on (sessionId, pageViewId)
1252
+ // would also work — left as index until a write pattern needs it.
1253
+ CREATE INDEX pageview_account IF NOT EXISTS
1254
+ FOR (pv:PageView) ON (pv.accountId);
1255
+
1256
+ CREATE INDEX pageview_occurred_at IF NOT EXISTS
1257
+ FOR (pv:PageView) ON (pv.occurredAt);
1258
+
1259
+ CREATE INDEX click_account IF NOT EXISTS
1260
+ FOR (cl:Click) ON (cl.accountId);
1261
+
1262
+ CREATE INDEX click_occurred_at IF NOT EXISTS
1263
+ FOR (cl:Click) ON (cl.occurredAt);
1264
+
1265
+ CREATE INDEX scroll_milestone_account IF NOT EXISTS
1266
+ FOR (sm:ScrollMilestone) ON (sm.accountId);
1267
+
1268
+ CREATE INDEX recommendation_account IF NOT EXISTS
1269
+ FOR (rec:Recommendation) ON (rec.accountId);
1270
+
1271
+ CREATE INDEX recommendation_session_key IF NOT EXISTS
1272
+ FOR (rec:Recommendation) ON (rec.sessionKey);
1273
+
1274
+ // ----------------------------------------------------------
1275
+ // Idea (Task 352) — original-thinking candidates extracted by
1276
+ // the signal-detector specialist from admin-side conversations.
1277
+ // Written via memory-write(labels:["Idea"], ...) with one edge
1278
+ // (:Conversation)-[:HAS_IDEA]->(:Idea). Forward-only write surface:
1279
+ // no historical backfill, no dedup, no clustering — let the brain
1280
+ // accumulate first. Declared here so memory-write's label validator
1281
+ // (db.labels() ∪ schema.cypher) registers the label on a fresh
1282
+ // deploy where no :Idea node has yet landed; without this, the
1283
+ // first write is rejected as "unknown label" and the specialist's
1284
+ // LOUD-FAIL surfaces the rejection without anything landing.
1285
+ // ----------------------------------------------------------
1286
+
1287
+ CREATE INDEX idea_account IF NOT EXISTS
1288
+ FOR (i:Idea) ON (i.accountId);
1289
+
1290
+ CREATE VECTOR INDEX idea_embedding IF NOT EXISTS
1291
+ FOR (i:Idea) ON (i.embedding)
1292
+ OPTIONS {
1293
+ indexConfig: {
1294
+ `vector.dimensions`: 768,
1295
+ `vector.similarity_function`: 'cosine'
1296
+ }
1297
+ };
@@ -6,7 +6,12 @@
6
6
  * 2. Conversations left empty after message removal
7
7
  * 3. Emails matching the contact's email (fromAddress or toAddresses)
8
8
  * 4. AccessGrants via (Person)-[:HAS_ACCESS]->(AccessGrant)
9
- * 5. Person node itself (DETACH DELETE)
9
+ * 5. Visitor-graph data (Task 357):
10
+ * - :Session reachable via (Person)-[:VISITED]->(Session)
11
+ * - all :HAS_EVENT children (PageView/Click/ScrollMilestone)
12
+ * - :AnonVisitor via (Person)-[:OWNS_VISITOR]->(AnonVisitor)
13
+ * :Page and :Listing are content metadata and are intentionally preserved.
14
+ * 6. Person node itself (DETACH DELETE)
10
15
  *
11
16
  * Requires `confirm: true` to execute. Without it, returns a preview
12
17
  * of what would be deleted (counts per node type) but does not delete.
@@ -33,6 +38,12 @@ interface EraseCounts {
33
38
  conversations: number;
34
39
  emails: number;
35
40
  accessGrants: number;
41
+ /** Visitor-graph Sessions removed (Task 357). */
42
+ visitorSessions: number;
43
+ /** Visitor-graph event nodes removed (PageView + Click + ScrollMilestone). */
44
+ visitorEvents: number;
45
+ /** AnonVisitor nodes owned by this Person and removed. */
46
+ anonVisitors: number;
36
47
  person: number;
37
48
  }
38
49
  export type ContactEraseResult = ErasePreview | EraseReceipt;
@@ -1 +1 @@
1
- {"version":3,"file":"contact-erase.d.ts","sourceRoot":"","sources":["../../src/tools/contact-erase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,UAAU,kBAAkB;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,YAAY;IACpB,SAAS,EAAE,KAAK,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,UAAU,YAAY;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,YAAY,CAAC;AAE7D,wBAAsB,YAAY,CAChC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAsL7B"}
1
+ {"version":3,"file":"contact-erase.d.ts","sourceRoot":"","sources":["../../src/tools/contact-erase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,UAAU,kBAAkB;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,YAAY;IACpB,SAAS,EAAE,KAAK,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,UAAU,YAAY;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,YAAY,CAAC;AAE7D,wBAAsB,YAAY,CAChC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CA6N7B"}
@@ -6,7 +6,12 @@
6
6
  * 2. Conversations left empty after message removal
7
7
  * 3. Emails matching the contact's email (fromAddress or toAddresses)
8
8
  * 4. AccessGrants via (Person)-[:HAS_ACCESS]->(AccessGrant)
9
- * 5. Person node itself (DETACH DELETE)
9
+ * 5. Visitor-graph data (Task 357):
10
+ * - :Session reachable via (Person)-[:VISITED]->(Session)
11
+ * - all :HAS_EVENT children (PageView/Click/ScrollMilestone)
12
+ * - :AnonVisitor via (Person)-[:OWNS_VISITOR]->(AnonVisitor)
13
+ * :Page and :Listing are content metadata and are intentionally preserved.
14
+ * 6. Person node itself (DETACH DELETE)
10
15
  *
11
16
  * Requires `confirm: true` to execute. Without it, returns a preview
12
17
  * of what would be deleted (counts per node type) but does not delete.
@@ -45,8 +50,27 @@ export async function contactErase(params) {
45
50
  conversations: 0,
46
51
  emails: 0,
47
52
  accessGrants: 0,
53
+ visitorSessions: 0,
54
+ visitorEvents: 0,
55
+ anonVisitors: 0,
48
56
  person: 1,
49
57
  };
58
+ // Count visitor-graph data — Sessions, events, AnonVisitors (Task 357)
59
+ {
60
+ const visitorCounts = await session.run(`MATCH (p:Person) WHERE elementId(p) = $personNodeId
61
+ OPTIONAL MATCH (p)-[:VISITED]->(vs:Session)
62
+ OPTIONAL MATCH (vs)-[:HAS_EVENT]->(ev)
63
+ OPTIONAL MATCH (p)-[:OWNS_VISITOR]->(av:AnonVisitor)
64
+ RETURN count(DISTINCT vs) AS visitorSessions,
65
+ count(DISTINCT ev) AS visitorEvents,
66
+ count(DISTINCT av) AS anonVisitors`, { personNodeId });
67
+ if (visitorCounts.records.length > 0) {
68
+ const row = visitorCounts.records[0];
69
+ counts.visitorSessions = Number(row.get("visitorSessions")?.valueOf?.() ?? row.get("visitorSessions") ?? 0);
70
+ counts.visitorEvents = Number(row.get("visitorEvents")?.valueOf?.() ?? row.get("visitorEvents") ?? 0);
71
+ counts.anonVisitors = Number(row.get("anonVisitors")?.valueOf?.() ?? row.get("anonVisitors") ?? 0);
72
+ }
73
+ }
50
74
  // Count Messages + Conversations via sessionId
51
75
  if (sessionId) {
52
76
  const convCountResult = await session.run(`MATCH (c:Conversation {conversationId: $sessionId, accountId: $accountId})
@@ -107,6 +131,15 @@ export async function contactErase(params) {
107
131
  await tx.run(`MATCH (p:Person)-[:HAS_ACCESS]->(g:AccessGrant)
108
132
  WHERE elementId(p) = $personNodeId
109
133
  DELETE g`, { personNodeId });
134
+ // 3d-bis. Visitor-graph cascade (Task 357). :Page and :Listing are
135
+ // content metadata and intentionally preserved — they have no PII.
136
+ await tx.run(`MATCH (p:Person) WHERE elementId(p) = $personNodeId
137
+ OPTIONAL MATCH (p)-[:VISITED]->(vs:Session)
138
+ OPTIONAL MATCH (vs)-[:HAS_EVENT]->(ev)
139
+ DETACH DELETE ev, vs`, { personNodeId });
140
+ await tx.run(`MATCH (p:Person)-[:OWNS_VISITOR]->(av:AnonVisitor)
141
+ WHERE elementId(p) = $personNodeId
142
+ DETACH DELETE av`, { personNodeId });
110
143
  // 3e. Delete Person node (DETACH DELETE to remove any remaining relationships)
111
144
  await tx.run(`MATCH (p:Person) WHERE elementId(p) = $personNodeId
112
145
  DETACH DELETE p`, { personNodeId });
@@ -1 +1 @@
1
- {"version":3,"file":"contact-erase.js","sourceRoot":"","sources":["../../src/tools/contact-erase.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contact-erase.js","sourceRoot":"","sources":["../../src/tools/contact-erase.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ # Visitor graph (Task 357)
2
+
3
+ Behavioural analytics that connect anonymous page visits to known `:Person` contacts. Replaces the anonymous click-through metric from Task 336 with a fully attributed graph.
4
+
5
+ ## What this gives the operator
6
+
7
+ - A morning briefing surface: "who has been on the site overnight, and what did they look at?"
8
+ - An engagement-ranked nurture queue, ordered by recency × depth × dwell.
9
+ - A graph-backed click-through-rate report for property recommendations.
10
+ - A full event timeline for any one session, for prep and diagnosis.
11
+ - A signed-cookie that recognises a named visitor on later visits without re-clicking the marketing link.
12
+
13
+ ## Data model
14
+
15
+ | Node | Meaning |
16
+ |------|---------|
17
+ | `:Session` | One browser tab session. Composite key `(accountId, sessionId)`. |
18
+ | `:AnonVisitor` | Pre-identification browser identity. Merges into `:Person` on first signed-token click. |
19
+ | `:PageView` | One page load. Carries `referrer`, `path`, optional `dwellMs`. |
20
+ | `:Click` | One DOM click on a tagged element (`data-track="<label>"`). |
21
+ | `:ScrollMilestone` | Roll-up of scroll depth — one node per `:PageView`, `maxDepth` ∈ {25,50,75,100}. |
22
+ | `:Page` | URL metadata. Content-only, survives erasure. |
23
+ | `:Recommendation` | Materialised `[property-recommended]` log line for CTR computation. |
24
+
25
+ | Edge | Direction |
26
+ |------|-----------|
27
+ | `VISITED` | `Person → Session` or `AnonVisitor → Session` |
28
+ | `OWNS_VISITOR` | `Person → AnonVisitor` (cross-session merge) |
29
+ | `HAS_EVENT` | `Session → PageView / Click / ScrollMilestone` |
30
+ | `OF_PAGE` | `PageView → Page` |
31
+ | `OF_LISTING` | `PageView → Listing` |
32
+ | `FOR_SESSION` | `Recommendation → Session` |
33
+
34
+ ## How identity gets resolved
35
+
36
+ The signed-token cookie `mxy_v` carries a `:Person` elementId, signed HMAC-SHA256 with a brand-local 32-byte secret (file at `~/.<brand>/credentials/visitor-token-secret`, minted on first read). When the recommender's `/listings/<slug>/click?session=<sk>&v=<token>` URL is visited:
37
+
38
+ 1. The click handler verifies the HMAC on `<token>`.
39
+ 2. If valid, the same token value is written into `mxy_v` (Max-Age 30 days, SameSite=Lax, HttpOnly, Secure).
40
+ 3. On subsequent visits, `POST /v/event` reads the cookie, verifies it, and attributes every event to the bound `:Person`.
41
+
42
+ When a previously-anonymous browser binds for the first time, any `:Session` already attributed to the `:AnonVisitor` is re-attached to the `:Person`, and the merge fires `[anonvisitor-merge]` with the count of reattributed sessions.
43
+
44
+ ## Tools
45
+
46
+ All under the `real-agent-buyers` plugin, admin-side only:
47
+
48
+ | Tool | Purpose |
49
+ |------|---------|
50
+ | `visitor-recent-by-person` | Recent sessions for a known `:Person` (morning round, 1:1 prep). |
51
+ | `visitor-recent-by-page` | Recent visitors of a given listing slug or URL. |
52
+ | `visitor-engagement-score` | Engagement-ranked `:Person` list (nurture queue). |
53
+ | `visitor-recommendation-ctr` | Graph-backed CTR over a window. Replaces the Task 336 log-scan after the 14-day parity cutover. |
54
+ | `visitor-session-detail` | Full event timeline for one `:Session`. |
55
+ | `visitor-event-ingest` | Admin companion to `POST /v/event` for test harness work. |
56
+ | `visitor-backfill-from-logs` | One-shot importer: parses `[property-recommended]` and `[property-card-click]` log lines, computes a parity check, populates the graph. |
57
+
58
+ ## Privacy
59
+
60
+ The full description is at `/privacy` on every brand domain. Highlights:
61
+
62
+ - First-party cookie only, no third-party scripts.
63
+ - 180-day default retention; configurable per brand via `brand.json:visitorGraphRetentionDays`.
64
+ - Right-to-erasure cascades through `contact-erase`: `:Session`, every `:HAS_EVENT` child, and any owned `:AnonVisitor` are removed when the `:Person` is erased. `:Page` and `:Listing` are content metadata and intentionally preserved.
65
+
66
+ ## Verification
67
+
68
+ Quick checks the operator can run after deployment:
69
+
70
+ 1. Load a published listing page; grep `[visitor-event] type=pageview` in `server.log` within 1s.
71
+ 2. Scroll past 50%; grep `[visitor-event] type=scroll depth=50`.
72
+ 3. Click an element marked `data-track="floorplan"`; grep `[visitor-event] type=click label=floorplan`.
73
+ 4. Run `visitor-backfill-from-logs` over the existing Task 336 log window; the response includes `parityCheck=ok` when graph CTR matches log-scan CTR exactly.
74
+
75
+ ## Failure signals
76
+
77
+ | Symptom | What it means | Where to look |
78
+ |---------|---------------|---------------|
79
+ | `[visitor-event]` count drops to zero with no `[v-event-error]` | Pixel silently failing on the brand domain (probably CSP, CORS, or origin mismatch). | Check brand.json `publishedSiteOrigins`; check browser console on a published listing page. |
80
+ | `[token-bind] reject reason=bad-sig` spikes | HMAC verify failing — either the secret rotated and old cookies are being rejected (expected during rotation) or the recommender is minting against a stale secret. | Compare `~/.<brand>/credentials/visitor-token-secret` across processes. |
81
+ | `[anonvisitor-merge]` never fires after first signed-token click | The pixel isn't reading the cookie. | Inspect the `mxy_v` cookie in DevTools; check CORS `Access-Control-Allow-Credentials: true`. |
82
+ | `[v-event-error] reason=rate-limit` for legitimate operator traffic | Operator IP shares a NAT with high-volume crawlers. | Adjust `RATE_LIMIT` in `visitor-event.ts` or whitelist the IP at the proxy. |
@@ -40,7 +40,11 @@ When loading this reference, confirm which schema files were consulted by noting
40
40
  | WhatsApp Conversation (legacy) | `WhatsAppConversation` | extends `schema:Conversation` | — | `accountId`, `conversationId`, `archiveSourceFile`, `firstMessageAt`, `lastMessageAt`, `participantCount`, `messageCount`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
41
41
  | WhatsApp Message (legacy) | `WhatsAppMessage` | extends `schema:Message` | — | `accountId`, `conversationId`, `messageId`, `dateSent`, `body`, `senderName`, `sequenceIndex`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
42
42
  | ConversationArchive | `ConversationArchive` | platform-native (chunked conversation archive parent) | — | `accountId`, `conversationIdentity`, `archiveSourceFile`, `summary`, `keywords`, `lastIngestedMessageHash`, `lastIngestedMessageAt`, `lastIngestedArchiveSha256`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
43
+ <<<<<<< HEAD
43
44
  | AEO Audit | `AEOAudit` | platform-native (AEO audit history) | — | `accountId`, `auditId`, `target`, `score`, `heuristicsJson`, `runAt`, `createdByAgent`, `createdBySession`, `createdAt` |
45
+ =======
46
+ | Idea | `Idea` | platform-native (signal-detector original-thinking candidate) | — | `accountId`, `body`, `conversationId`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
47
+ >>>>>>> worktree-task-352-idea-schema
44
48
 
45
49
  **Branding properties on LocalBusiness:** `primaryColor`, `accentColor`, `backgroundColor`, `tagline` — optional, used to brand the public chat endpoint. Hex color values must match `#[0-9a-fA-F]{3,8}`. Logo and icon are linked via `HAS_BRAND_ASSET → ImageObject` with `purpose: "logo"` or `"icon"`.
46
50
 
@@ -56,6 +60,8 @@ When loading this reference, confirm which schema files were consulted by noting
56
60
 
57
61
  **Position** carries a single role held by a `UserProfile` at an `Organization` — its title, dates, and per-role properties (e.g. achievement bullets in `description`, `endDate` for a finished role). Each role is its own node so it can carry its own structure; multiple Positions per UserProfile (career history); one Organization may host many Positions across many UserProfiles. Written by the `document-ingest` skill when a CV section classifies as `Position`. The natural pattern is `(:UserProfile)-[:HAS_POSITION]->(:Position)-[:AT]->(:Organization)` — never set the employer name as a property on Position; always create the AT edge to the (MERGEd) Organization.
58
62
 
63
+ **Idea** is a forward-only write surface for original-thinking candidates extracted from admin-side conversations by the `signal-detector` specialist (dispatched only by the admin agent's Stop hook). `body` is the verbatim operator statement; `conversationId` denormalises the source conversation; `scope` is always `"admin"`. Never written by any other path. No dedup or clustering at write time — let the brain accumulate first.
64
+
59
65
  **Durable-action properties on Task:** `kind` (string discriminator, e.g. `"onboarding-establish-owner"`, `"cloudflare-tunnel-login"`), `steps` (string[], append-only as the action progresses), `startedAt` (ISO timestamp, set when status transitions to `running`), `completedAt` (ISO timestamp, set when status transitions to `completed` or `failed`), `errorMessage` (string, set when status=`failed`; cleared to `null` when the same Task transitions to `completed`), `inputsProvided` (string[] of property names supplied at action start — the canonical "call shape" record), `inputs.<field>` (Record of arbitrary non-secret JSON values from the action's form payload, redacted at write time per the schema declared at the form definition). All optional. Per-kind value-field enumeration is intentionally absent — values vary by what the action records, governed by the central `redactSecrets` primitive rather than per-kind allow-lists. The `running` status value joins the existing `pending|active|completed|failed|cancelled` enum to discriminate "an action is mid-flight in this conversation" from `active` ("a commitment the operator has accepted").
60
66
 
61
67
  ---
@@ -138,6 +144,7 @@ Do not write placeholder values ("TBD", "unknown", empty strings) for properties
138
144
  (:Person|:AdminUser)-[:SENT]->(:Message)
139
145
  (:Person|:AdminUser)-[:PARTICIPANT_IN]->(:Conversation)
140
146
  (:Message)-[:NEXT]->(:Message)
147
+ (:Conversation)-[:HAS_IDEA]->(:Idea)
141
148
  ```
142
149
 
143
150
  ### WhatsApp ingest natural-key contract
@@ -226,6 +233,43 @@ Patterns with high occurrence count are candidates for new section-kind labels.
226
233
 
227
234
  ---
228
235
 
236
+ ## Visitor Graph (Task 357)
237
+
238
+ Behavioural analytics — pixel events from operator-owned listing pages land here.
239
+ Cross-session attribution is via a signed-token cookie that the property-recommender
240
+ issues; the pixel never sees a third-party domain.
241
+
242
+ | Entity | Neo4j Label | schema.org | Description | Required Properties |
243
+ |---|---|---|---|---|
244
+ | Session | `Session` | (none — bespoke) | One browser tab session. | `accountId`, `sessionId`, `startedAt` |
245
+ | AnonVisitor | `AnonVisitor` | (none — bespoke) | Pre-identification browser identity. Merges into `:Person` on signed-token click. | `accountId`, `visitorId` |
246
+ | PageView | `PageView` | `schema:ViewAction` | One page load. One node per pageview; idempotent within 5s. | `accountId`, `occurredAt`, `referrer` |
247
+ | Click | `Click` | `schema:InteractAction` | DOM click on `data-track="<label>"` or a known interaction (gallery / floorplan). | `accountId`, `label`, `occurredAt` |
248
+ | ScrollMilestone | `ScrollMilestone` | (none — bespoke) | Roll-up: one node per PageView with `maxDepth` ∈ {25,50,75,100}. Updated in place. | `accountId`, `maxDepth` |
249
+ | Page | `Page` | `schema:WebPage` | URL metadata. Persists across erasure (no PII). | `accountId`, `url` |
250
+ | Recommendation | `Recommendation` | `schema:Recommendation` | Materialised `[property-recommended]` log line — what the recommender suggested for this session. | `accountId`, `sessionKey`, `listingSlug`, `recommendedAt` |
251
+
252
+ ### Edges
253
+
254
+ | Edge | Direction | Note |
255
+ |---|---|---|
256
+ | `VISITED` | `Person` → `Session` or `AnonVisitor` → `Session` | One edge per session. Re-attribution on merge replaces the AnonVisitor edge with a Person edge. |
257
+ | `OWNS_VISITOR` | `Person` → `AnonVisitor` | Created on first signed-token click. Records the merge history. |
258
+ | `HAS_EVENT` | `Session` → `PageView` / `Click` / `ScrollMilestone` | Every event hangs off its Session. |
259
+ | `OF_PAGE` | `PageView` → `Page` | Click/Scroll inherit their Page through their PageView. |
260
+ | `OF_LISTING` | `PageView` → `Listing` | Optional: present when the URL was a known listing slug. |
261
+ | `FOR_SESSION` | `Recommendation` → `Session` | Joins recommendations to the session their CTR is computed against. |
262
+
263
+ ### Cascade rules
264
+
265
+ - `contact-erase` on a `:Person` trashes every `(:Session)<-[:VISITED]-(:Person)` and every `(:Session)-[:HAS_EVENT]->(*)` reachable from it. Uses the same `trashNode` pattern as `profile-delete`.
266
+ - `:Page` and `:Listing` are content metadata — they do not cascade.
267
+ - `:Recommendation` cascades only when its `:Session` cascades; orphan `:Recommendation`s without a session never exist (the writer always pairs them).
268
+
269
+ ### Retention
270
+
271
+ `accountId`-scoped setting `visitorGraphRetentionDays` (default 180). Sessions older than the retention window are trashed by a separate cron — wired in this task as a setting, the cron itself is filed as a follow-up. The privacy page surfaces the active value.
272
+
229
273
  ## Order Node — Disambiguation
230
274
 
231
275
  The base `Order` label is used for general jobs and orders. Several vertical schemas define specialised subtypes that also map to `schema:Order` but use different Neo4j labels:
@@ -2705,6 +2705,7 @@ var REMOTE_SESSION_SECRET_FILE = resolve(MAXY_DIR, "credentials", "remote-sessio
2705
2705
  var ADMIN_SESSION_SECRET_FILE = resolve(MAXY_DIR, "credentials", "admin-session-secret");
2706
2706
  var TELEGRAM_WEBHOOK_SECRET_FILE = resolve(MAXY_DIR, ".telegram-webhook-secret");
2707
2707
  var TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE = resolve(MAXY_DIR, ".telegram-admin-webhook-secret");
2708
+ var VISITOR_TOKEN_SECRET_FILE = resolve(MAXY_DIR, "credentials", "visitor-token-secret");
2708
2709
  var CLAUDE_CREDENTIALS_FILE = resolve(MAXY_DIR, ".claude", ".credentials.json");
2709
2710
 
2710
2711
  // app/lib/vnc-logger.ts
@@ -5476,6 +5477,7 @@ export {
5476
5477
  USERS_FILE,
5477
5478
  LOG_DIR,
5478
5479
  BIN_DIR,
5480
+ VISITOR_TOKEN_SECRET_FILE,
5479
5481
  CLAUDE_CREDENTIALS_FILE,
5480
5482
  vncLog,
5481
5483
  newCorrId,
@@ -13,7 +13,7 @@ import {
13
13
  sanitizeClientCorrId,
14
14
  vncLog,
15
15
  websockifyLog
16
- } from "./chunk-FJCI6X3H.js";
16
+ } from "./chunk-HCYM5FLU.js";
17
17
  import "./chunk-JSBRDJBE.js";
18
18
 
19
19
  // server/edge.ts
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Privacy</title>
7
+ <style>
8
+ :root { --fg:#222; --bg:#FAFAF8; --muted:#666; --accent:#7C8C72; }
9
+ * { box-sizing: border-box; }
10
+ body { font: 16px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; margin: 0; color: var(--fg); background: var(--bg); }
11
+ main { max-width: 720px; margin: 0 auto; padding: 48px 24px 96px; }
12
+ h1 { margin-top: 0; font-size: 32px; letter-spacing: -0.01em; }
13
+ h2 { margin-top: 36px; font-size: 20px; }
14
+ p, li { color: #333; }
15
+ ul { padding-left: 20px; }
16
+ code { background: #eee; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
17
+ table { border-collapse: collapse; width: 100%; margin: 12px 0; }
18
+ th, td { border-bottom: 1px solid #ddd; padding: 8px 6px; text-align: left; vertical-align: top; }
19
+ th { background: #f0f0ee; font-weight: 600; }
20
+ .muted { color: var(--muted); font-size: 14px; }
21
+ .accent { color: var(--accent); }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <main>
26
+ <h1>Privacy</h1>
27
+ <p>This page describes the data this site collects about your visit, why, and how to ask for it to be removed.</p>
28
+
29
+ <h2>What we collect</h2>
30
+ <p>When you load a property listing page, a small first-party script (<code>/v.js</code>) records:</p>
31
+ <table>
32
+ <thead><tr><th>Field</th><th>Why</th><th>Stored as</th></tr></thead>
33
+ <tbody>
34
+ <tr><td>Page URL, page title, referrer</td><td>So we know which listings get visitors and where they came from.</td><td><code>:PageView</code> node</td></tr>
35
+ <tr><td>Visitor ID (random, browser-local)</td><td>So we can tell two visits from one device apart from two devices.</td><td><code>:AnonVisitor</code> node (anonymous), or linked to a <code>:Person</code> if you arrived via a personalised marketing link.</td></tr>
36
+ <tr><td>Session ID (random, per tab)</td><td>So one tab's activity stays together.</td><td><code>:Session</code> node</td></tr>
37
+ <tr><td>Clicks on tagged elements (gallery, floorplan, etc.)</td><td>So we know which parts of a listing get interaction.</td><td><code>:Click</code> node</td></tr>
38
+ <tr><td>Scroll depth — 25%, 50%, 75%, 100%</td><td>So we know how far you read.</td><td><code>:ScrollMilestone</code> node (one per page view, max depth recorded)</td></tr>
39
+ <tr><td>Time spent on a page (dwell)</td><td>Same.</td><td>Property of the <code>:PageView</code> node</td></tr>
40
+ </tbody>
41
+ </table>
42
+ <p class="muted">We do not collect: cursor positions, keystrokes, form contents, screen recordings, or anything from third-party tracking networks. The pixel is loaded from the same site as the listing, never from a third party.</p>
43
+
44
+ <h2>Cookies</h2>
45
+ <p>One cookie is set when you arrive via a personalised marketing link from the agent:</p>
46
+ <ul>
47
+ <li><code>mxy_v</code> — signed token containing your contact identifier. Set <code>HttpOnly</code>, <code>SameSite=Lax</code>, <code>Secure</code>. Lives for 30 days. Lets the site recognise you on later visits without you clicking the marketing link again.</li>
48
+ </ul>
49
+
50
+ <h2>Cross-session attribution</h2>
51
+ <p>If you visit anonymously today and later click a personalised marketing link from the agent, prior anonymous visits from the same browser are linked to your contact record. This is the only way an anonymous visit ever becomes named.</p>
52
+
53
+ <h2>Retention</h2>
54
+ <p>Visit data is kept for <strong>180 days</strong> by default, then removed automatically. The operator can change this — your acting agent will tell you the current value if you ask.</p>
55
+
56
+ <h2>Right to access and erasure</h2>
57
+ <p>To see what we hold or to ask for it to be removed, contact the agent. We honour GDPR Article 15 (access) and Article 17 (erasure). Erasure cascades: removing your contact removes every session, page view, click, and scroll milestone tied to your identity. Anonymous page views from browsers we cannot link to you are not removed individually — they cannot be identified, and they expire under the retention window above.</p>
58
+
59
+ <h2>Bots and crawlers</h2>
60
+ <p>Search engines and link checkers are filtered out by user-agent before any data is written. They are also excluded from rate-limit budgets so legitimate visitors are never blocked because a crawler hammered the endpoint.</p>
61
+
62
+ <h2>This page</h2>
63
+ <p class="muted">This is a static description. The behaviour above is implemented in <code>platform/ui/server/routes/visitor-event.ts</code> and <code>platform/ui/public/v.js</code>. The schema is declared in <code>platform/neo4j/schema.cypher</code> under the Visitor Graph section.</p>
64
+ </main>
65
+ </body>
66
+ </html>