@rubytech/create-realagent-code 0.1.125 → 0.1.126

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 (76) hide show
  1. package/dist/index.js +11 -1
  2. package/package.json +2 -2
  3. package/payload/platform/lib/graph-style/dist/index.d.ts +78 -0
  4. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/graph-style/dist/index.js +296 -0
  6. package/payload/platform/lib/graph-style/dist/index.js.map +1 -0
  7. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +114 -0
  8. package/payload/platform/lib/graph-style/src/index.ts +307 -0
  9. package/payload/platform/lib/graph-style/tsconfig.json +9 -0
  10. package/payload/platform/lib/graph-style/vitest.config.ts +9 -0
  11. package/payload/platform/package-lock.json +318 -0
  12. package/payload/platform/package.json +3 -2
  13. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  14. package/payload/platform/plugins/docs/references/visitor-graph.md +4 -3
  15. package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +8 -0
  16. package/payload/platform/plugins/graph-viewer/PLUGIN.md +56 -0
  17. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts +7 -0
  18. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts.map +1 -0
  19. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +74 -0
  20. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -0
  21. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts +5 -0
  22. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts.map +1 -0
  23. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js +43 -0
  24. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js.map +1 -0
  25. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts +28 -0
  26. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts.map +1 -0
  27. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js +73 -0
  28. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js.map +1 -0
  29. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts +40 -0
  30. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts.map +1 -0
  31. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js +77 -0
  32. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js.map +1 -0
  33. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts +45 -0
  34. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -0
  35. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +170 -0
  36. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -0
  37. package/payload/platform/plugins/graph-viewer/mcp/package.json +25 -0
  38. package/payload/platform/plugins/graph-viewer/mcp/vitest.config.ts +8 -0
  39. package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +35 -0
  40. package/payload/platform/scripts/check-plugin-tools-mcp-consistency.mjs +136 -0
  41. package/payload/premium-plugins/real-agent/plugins/buyers/PLUGIN.md +6 -2
  42. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/index.js +11 -5
  43. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/index.js.map +1 -1
  44. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/lib/visitor-token.d.ts +9 -0
  45. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/lib/visitor-token.d.ts.map +1 -0
  46. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/lib/visitor-token.js +66 -0
  47. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/lib/visitor-token.js.map +1 -0
  48. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/mint-visitor-token.d.ts +12 -0
  49. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/mint-visitor-token.d.ts.map +1 -0
  50. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/mint-visitor-token.js +21 -0
  51. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/mint-visitor-token.js.map +1 -0
  52. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/visitor-backfill-from-logs.d.ts +0 -5
  53. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/visitor-backfill-from-logs.d.ts.map +1 -1
  54. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/visitor-backfill-from-logs.js +6 -27
  55. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/dist/tools/visitor-backfill-from-logs.js.map +1 -1
  56. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/src/index.ts +17 -5
  57. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/src/lib/visitor-token.ts +79 -0
  58. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/src/tools/mint-visitor-token.ts +35 -0
  59. package/payload/premium-plugins/real-agent/plugins/buyers/mcp/src/tools/visitor-backfill-from-logs.ts +6 -33
  60. package/payload/premium-plugins/real-agent/plugins/buyers/skills/property-recommender/SKILL.md +1 -10
  61. package/payload/premium-plugins/real-agent/plugins/leads/skills/lead-nurturing/SKILL.md +1 -1
  62. package/payload/premium-plugins/real-agent/plugins/leads/skills/morning-round/SKILL.md +1 -1
  63. package/payload/premium-plugins/real-agent/plugins/vendors/skills/vendor-updates/SKILL.md +1 -1
  64. package/payload/server/public/assets/{admin-D6IfAzYY.js → admin-FcRHAL-3.js} +7 -7
  65. package/payload/server/public/assets/{data-BGbQyufe.js → data-Ds37mflX.js} +1 -1
  66. package/payload/server/public/assets/{graph-D-1lRTeL.js → graph-CmWRhaiS.js} +1 -1
  67. package/payload/server/public/assets/graph-labels-Ch2r00Gt.js +1 -0
  68. package/payload/server/public/assets/page-BOtNny_4.js +51 -0
  69. package/payload/server/public/assets/page-BcHhJXUt.js +1 -0
  70. package/payload/server/public/data.html +3 -3
  71. package/payload/server/public/graph.html +3 -3
  72. package/payload/server/public/index.html +4 -4
  73. package/payload/server/server.js +236 -92
  74. package/payload/server/public/assets/graph-labels-BRXJHNYE.js +0 -1
  75. package/payload/server/public/assets/page-B4oirCvn.js +0 -1
  76. package/payload/server/public/assets/page-FmJ7PIYx.js +0 -51
@@ -12,15 +12,15 @@
12
12
  // -> MATCH/MERGE Session, CREATE Click {label:'card', listingSlug}
13
13
  // MERGE Session-[:HAS_EVENT]->Click
14
14
  //
15
- // Parity check after backfill: graph counts vs log-scan counts for the
16
- // same window. parityCheck is 'ok' iff both deltas are zero, otherwise
17
- // 'mismatch' with the delta surfaced for diagnosis.
15
+ // Recovery path: used when a live POST /v/event write was lost (process
16
+ // restart, dropped connection) or for ad-hoc forensics. The graph is
17
+ // the canonical surface — these log lines are the join key, not the
18
+ // aggregation path.
18
19
  //
19
20
  // Idempotency: re-running over the same log window does not duplicate
20
21
  // Recommendation nodes (MERGE on the exact sessionKey + slug + ts tuple).
21
22
  // Click nodes are immutable event records by design; if you re-run on a
22
- // window already processed you get duplicates. Operator runs once per
23
- // cutover.
23
+ // window already processed you get duplicates.
24
24
  import { readFileSync } from "node:fs";
25
25
  import { getSession } from "../lib/neo4j.js";
26
26
  const REC_RE = /\[property-recommended\]\s+sessionKey=(\S+)\s+listingSlug=(\S+)\s+ts=(\S+)/;
@@ -116,26 +116,7 @@ export async function visitorBackfillFromLogs(p) {
116
116
  const row = result.records[0];
117
117
  clickedWritten += Number(row?.get("written") ?? 0);
118
118
  }
119
- const parityRes = await session.run(`MATCH (rec:Recommendation {accountId: $accountId})
120
- WHERE rec.recommendedAt >= datetime($since) AND rec.recommendedAt < datetime($until)
121
- WITH count(DISTINCT rec) AS recCount
122
- OPTIONAL MATCH (cl:Click {accountId: $accountId, label: 'card'})
123
- WHERE cl.occurredAt >= datetime($since) AND cl.occurredAt < datetime($until)
124
- AND cl.backfilled = true
125
- RETURN recCount, count(DISTINCT cl) AS clickCount`, {
126
- accountId: p.accountId,
127
- since: windowStart ?? new Date(0).toISOString(),
128
- until: windowEnd ? new Date(Date.parse(windowEnd) + 1).toISOString() : new Date().toISOString(),
129
- });
130
- const head = parityRes.records[0];
131
- const recGraph = Number(head?.get("recCount") ?? 0);
132
- const clickGraph = Number(head?.get("clickCount") ?? 0);
133
- const parityDelta = {
134
- recommended: recGraph - rec.length,
135
- clicked: clickGraph - click.length,
136
- };
137
- const parityCheck = parityDelta.recommended === 0 && parityDelta.clicked === 0 ? "ok" : "mismatch";
138
- console.error(`[visitor-backfill] done parityCheck=${parityCheck} recParsed=${rec.length} recWritten=${recommendedWritten} clickParsed=${click.length} clickWritten=${clickedWritten} unparseable=${unparseable}`);
119
+ console.error(`[visitor-backfill] done recParsed=${rec.length} recWritten=${recommendedWritten} clickParsed=${click.length} clickWritten=${clickedWritten} unparseable=${unparseable}`);
139
120
  return {
140
121
  windowStart,
141
122
  windowEnd,
@@ -145,8 +126,6 @@ export async function visitorBackfillFromLogs(p) {
145
126
  clickedParsed: click.length,
146
127
  clickedWritten,
147
128
  unparseable,
148
- parityCheck,
149
- parityDelta,
150
129
  };
151
130
  }
152
131
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"visitor-backfill-from-logs.js","sourceRoot":"","sources":["../../src/tools/visitor-backfill-from-logs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"visitor-backfill-from-logs.js","sourceRoot":"","sources":["../../src/tools/visitor-backfill-from-logs.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,14 @@
1
- // buyers MCP server — visitor-graph tools (Task 357).
1
+ // buyers MCP server — visitor-graph tools (Task 357 + 362).
2
2
  //
3
- // Seven tools, all admin-side (none exposed on the public agent):
3
+ // Eight tools, all admin-side (none exposed on the public agent):
4
4
  // visitor-recent-by-person query
5
5
  // visitor-recent-by-page query
6
6
  // visitor-engagement-score query
7
- // visitor-recommendation-ctr query (replaces 336 log-scan after parity)
7
+ // visitor-recommendation-ctr query (graph-backed CTR)
8
8
  // visitor-session-detail query
9
9
  // visitor-event-ingest write (test harness / manual replay)
10
- // visitor-backfill-from-logs write (one-shot importer + parity check)
10
+ // visitor-backfill-from-logs write (one-shot recovery importer)
11
+ // mint-visitor-token mint (Task 362 — sign &v=<token> URLs)
11
12
 
12
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -21,6 +22,7 @@ import { visitorRecommendationCtr } from "./tools/visitor-recommendation-ctr.js"
21
22
  import { visitorSessionDetail } from "./tools/visitor-session-detail.js";
22
23
  import { visitorEventIngest } from "./tools/visitor-event-ingest.js";
23
24
  import { visitorBackfillFromLogs } from "./tools/visitor-backfill-from-logs.js";
25
+ import { mintVisitorTokenTool } from "./tools/mint-visitor-token.js";
24
26
 
25
27
  const accountId = process.env.ACCOUNT_ID ?? "local";
26
28
 
@@ -119,7 +121,7 @@ server.tool(
119
121
 
120
122
  server.tool(
121
123
  "visitor-backfill-from-logs",
122
- "One-shot importer: parse [property-recommended] and [property-card-click] log lines and write into the visitor graph. Idempotent for Recommendations; immutable for Click events. Reports a parityCheck comparing graph counts to log counts.",
124
+ "One-shot importer: parse [property-recommended] and [property-card-click] log lines and write into the visitor graph. Idempotent for Recommendations; immutable for Click events. Use to recover late-arriving sessions or for ad-hoc forensics; the graph is the canonical aggregation surface.",
123
125
  {
124
126
  logPath: z.string().min(1).describe("Absolute path to the platform log file"),
125
127
  sinceIso: z.string().optional().describe("ISO timestamp lower bound (inclusive)"),
@@ -129,6 +131,16 @@ server.tool(
129
131
  jsonHandler((p) => visitorBackfillFromLogs(p as unknown as Parameters<typeof visitorBackfillFromLogs>[0])),
130
132
  );
131
133
 
134
+ server.tool(
135
+ "mint-visitor-token",
136
+ "Mint a signed visitor token bound to a known :Person elementId. Append the returned token to outbound listing URLs as `&v=<token>` (e.g. /listings/<slug>/click?session=<sk>&v=<token>). When the recipient clicks, the UI server verifies the HMAC and writes the same value into the `mxy_v` cookie so subsequent /v/event posts attribute to that :Person. TTL defaults to 30 days. Use from morning-round, lead-nurturing, and vendor-updates whenever the outbound URL goes to a known :Person.",
137
+ {
138
+ personId: z.string().min(1).describe(":Person elementId to bind the token to"),
139
+ ttlDays: z.number().int().positive().max(365).optional().describe("Token lifetime in days (default 30, max 365)"),
140
+ },
141
+ jsonHandler((p) => mintVisitorTokenTool(p as unknown as Parameters<typeof mintVisitorTokenTool>[0])),
142
+ );
143
+
132
144
  process.on("SIGINT", async () => {
133
145
  console.error("[buyers] SIGINT — closing driver");
134
146
  await closeDriver();
@@ -0,0 +1,79 @@
1
+ // Visitor-token minting — admin-side surface (Task 362).
2
+ //
3
+ // Mirrors the mint half of platform/ui/app/lib/visitor-token.ts. Verification
4
+ // still lives in the UI server (the only verifier; mxy_v cookie + /v/event +
5
+ // /listings/:slug/click consume tokens). Both processes share one secret file
6
+ // under <MAXY_DIR>/credentials/visitor-token-secret via the wx-create pattern,
7
+ // so whichever process reads first mints; the other reads the same bytes.
8
+ //
9
+ // MAXY_DIR is resolved from process.env.LOG_DIR's parent — the buyers MCP is
10
+ // spawned with LOG_DIR=<MAXY_DIR>/logs (see buyers/PLUGIN.md env block), so
11
+ // dirname(LOG_DIR) is the brand-scoped config dir whether the brand is maxy,
12
+ // realagent, or any future brand.
13
+
14
+ import { createHmac, randomBytes } from "node:crypto";
15
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { dirname, resolve } from "node:path";
17
+
18
+ const TOKEN_PREFIX = "v1.";
19
+ const SECRET_BYTES = 32;
20
+ const DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
21
+
22
+ interface TokenPayload {
23
+ p: string;
24
+ e: number;
25
+ }
26
+
27
+ let cachedSecret: Buffer | null = null;
28
+
29
+ function secretFilePath(): string {
30
+ const logDir = process.env.LOG_DIR;
31
+ if (!logDir) {
32
+ throw new Error("[buyers] LOG_DIR unset — cannot resolve visitor-token secret path");
33
+ }
34
+ return resolve(dirname(logDir), "credentials", "visitor-token-secret");
35
+ }
36
+
37
+ function getSecret(): Buffer {
38
+ if (cachedSecret) return cachedSecret;
39
+ const path = secretFilePath();
40
+ try {
41
+ const hex = readFileSync(path, "utf-8").trim();
42
+ if (hex.length === SECRET_BYTES * 2) {
43
+ cachedSecret = Buffer.from(hex, "hex");
44
+ return cachedSecret;
45
+ }
46
+ } catch {
47
+ /* fall through to mint */
48
+ }
49
+ const fresh = randomBytes(SECRET_BYTES).toString("hex");
50
+ try {
51
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
52
+ writeFileSync(path, fresh, { mode: 0o600, flag: "wx" });
53
+ console.error(`[buyers] visitor-token secret minted path=${path}`);
54
+ } catch {
55
+ // Race with the UI server (or a peer reader) between read and write;
56
+ // the second read below picks up their value.
57
+ }
58
+ const hex = readFileSync(path, "utf-8").trim();
59
+ cachedSecret = Buffer.from(hex, "hex");
60
+ return cachedSecret;
61
+ }
62
+
63
+ function base64urlEncode(buf: Buffer): string {
64
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
65
+ }
66
+
67
+ export interface MintOpts {
68
+ personId: string;
69
+ expiryMs?: number;
70
+ }
71
+
72
+ export function mintVisitorToken(opts: MintOpts): { token: string; expiryMs: number } {
73
+ const expiryMs = opts.expiryMs ?? Date.now() + DEFAULT_TTL_MS;
74
+ const payload: TokenPayload = { p: opts.personId, e: expiryMs };
75
+ const payloadJson = JSON.stringify(payload);
76
+ const payloadB64 = base64urlEncode(Buffer.from(payloadJson, "utf-8"));
77
+ const sig = base64urlEncode(createHmac("sha256", getSecret()).update(payloadJson).digest());
78
+ return { token: `${TOKEN_PREFIX}${payloadB64}.${sig}`, expiryMs };
79
+ }
@@ -0,0 +1,35 @@
1
+ // mint-visitor-token — admin-side mint of a signed visitor token bound to
2
+ // a known :Person elementId. The agent appends `&v=<token>` to listing-click
3
+ // URLs in outbound marketing surfaces (morning-round, lead-nurturing,
4
+ // vendor-updates). Verification stays in the UI server; this tool only mints.
5
+
6
+ import { mintVisitorToken } from "../lib/visitor-token.js";
7
+
8
+ export interface MintParams {
9
+ accountId: string;
10
+ personId: string;
11
+ ttlDays?: number;
12
+ }
13
+
14
+ export interface MintResult {
15
+ token: string;
16
+ expiryMs: number;
17
+ expiresAt: string;
18
+ }
19
+
20
+ const DEFAULT_TTL_DAYS = 30;
21
+ const MAX_TTL_DAYS = 365;
22
+
23
+ export async function mintVisitorTokenTool(p: MintParams): Promise<MintResult> {
24
+ if (!p.personId || typeof p.personId !== "string") {
25
+ throw new Error("personId required");
26
+ }
27
+ const ttlDays = Math.max(1, Math.min(p.ttlDays ?? DEFAULT_TTL_DAYS, MAX_TTL_DAYS));
28
+ const expiryMs = Date.now() + ttlDays * 24 * 60 * 60 * 1000;
29
+ const minted = mintVisitorToken({ personId: p.personId, expiryMs });
30
+ return {
31
+ token: minted.token,
32
+ expiryMs: minted.expiryMs,
33
+ expiresAt: new Date(minted.expiryMs).toISOString(),
34
+ };
35
+ }
@@ -12,15 +12,15 @@
12
12
  // -> MATCH/MERGE Session, CREATE Click {label:'card', listingSlug}
13
13
  // MERGE Session-[:HAS_EVENT]->Click
14
14
  //
15
- // Parity check after backfill: graph counts vs log-scan counts for the
16
- // same window. parityCheck is 'ok' iff both deltas are zero, otherwise
17
- // 'mismatch' with the delta surfaced for diagnosis.
15
+ // Recovery path: used when a live POST /v/event write was lost (process
16
+ // restart, dropped connection) or for ad-hoc forensics. The graph is
17
+ // the canonical surface — these log lines are the join key, not the
18
+ // aggregation path.
18
19
  //
19
20
  // Idempotency: re-running over the same log window does not duplicate
20
21
  // Recommendation nodes (MERGE on the exact sessionKey + slug + ts tuple).
21
22
  // Click nodes are immutable event records by design; if you re-run on a
22
- // window already processed you get duplicates. Operator runs once per
23
- // cutover.
23
+ // window already processed you get duplicates.
24
24
 
25
25
  import { readFileSync } from "node:fs";
26
26
  import { getSession } from "../lib/neo4j.js";
@@ -42,8 +42,6 @@ export interface BackfillResult {
42
42
  clickedParsed: number;
43
43
  clickedWritten: number;
44
44
  unparseable: number;
45
- parityCheck: "ok" | "mismatch";
46
- parityDelta: { recommended: number; clicked: number };
47
45
  }
48
46
 
49
47
  interface RecLine { kind: "rec"; sessionKey: string; listingSlug: string; ts: string }
@@ -144,31 +142,8 @@ export async function visitorBackfillFromLogs(p: BackfillParams): Promise<Backfi
144
142
  clickedWritten += Number(row?.get("written") ?? 0);
145
143
  }
146
144
 
147
- const parityRes = await session.run(
148
- `MATCH (rec:Recommendation {accountId: $accountId})
149
- WHERE rec.recommendedAt >= datetime($since) AND rec.recommendedAt < datetime($until)
150
- WITH count(DISTINCT rec) AS recCount
151
- OPTIONAL MATCH (cl:Click {accountId: $accountId, label: 'card'})
152
- WHERE cl.occurredAt >= datetime($since) AND cl.occurredAt < datetime($until)
153
- AND cl.backfilled = true
154
- RETURN recCount, count(DISTINCT cl) AS clickCount`,
155
- {
156
- accountId: p.accountId,
157
- since: windowStart ?? new Date(0).toISOString(),
158
- until: windowEnd ? new Date(Date.parse(windowEnd) + 1).toISOString() : new Date().toISOString(),
159
- },
160
- );
161
- const head = parityRes.records[0];
162
- const recGraph = Number(head?.get("recCount") ?? 0);
163
- const clickGraph = Number(head?.get("clickCount") ?? 0);
164
- const parityDelta = {
165
- recommended: recGraph - rec.length,
166
- clicked: clickGraph - click.length,
167
- };
168
- const parityCheck: "ok" | "mismatch" = parityDelta.recommended === 0 && parityDelta.clicked === 0 ? "ok" : "mismatch";
169
-
170
145
  console.error(
171
- `[visitor-backfill] done parityCheck=${parityCheck} recParsed=${rec.length} recWritten=${recommendedWritten} clickParsed=${click.length} clickWritten=${clickedWritten} unparseable=${unparseable}`,
146
+ `[visitor-backfill] done recParsed=${rec.length} recWritten=${recommendedWritten} clickParsed=${click.length} clickWritten=${clickedWritten} unparseable=${unparseable}`,
172
147
  );
173
148
 
174
149
  return {
@@ -180,8 +155,6 @@ export async function visitorBackfillFromLogs(p: BackfillParams): Promise<Backfi
180
155
  clickedParsed: click.length,
181
156
  clickedWritten,
182
157
  unparseable,
183
- parityCheck,
184
- parityDelta,
185
158
  };
186
159
  } finally {
187
160
  await session.close();
@@ -104,16 +104,7 @@ The Hono route at `/listings/:slug/click` emits the companion line on each card-
104
104
  [property-card-click] sessionKey=<sk> listingSlug=<slug> ts=<iso>
105
105
  ```
106
106
 
107
- The two log lines share key names (`sessionKey`, `listingSlug`) so a grep + awk over `server.log` joins them per session. Per-session click-through rate:
108
-
109
- ```bash
110
- # cards emitted per session
111
- awk -F'sessionKey=' '/\[property-recommended\]/{split($2,a," "); print a[1]}' server.log | sort | uniq -c
112
- # cards clicked per session
113
- awk -F'sessionKey=' '/\[property-card-click\]/{split($2,a," "); print a[1]}' server.log | sort | uniq -c
114
- ```
115
-
116
- Sessions appearing in the first count but not the second never clicked through. A high uniform zero is the load-bearing alarm — the recommendation is not earning the click. Sessions where `sessionKey=invalid` appears on the click line indicate a malformed `session` query param on the redirect URL (skill template defect); the click still landed but cannot be joined to its fire log.
107
+ CTR is read from the graph via the `visitor-recommendation-ctr` MCP tool, which joins `:Recommendation` and `:Click` nodes by `sessionKey` + `listingSlug` over the requested window. The log lines remain the join key for `visitor-backfill-from-logs` (one-shot importer for late-arriving sessions) and for ad-hoc forensics; they are not the aggregation surface. Sessions where `sessionKey=invalid` appears on the click line indicate a malformed `session` query param on the redirect URL (skill template defect); the click still landed but cannot be joined to its fire log.
117
108
 
118
109
  ## Visitor-graph integration (Task 357)
119
110
 
@@ -140,7 +140,7 @@ Encourage the user to capture:
140
140
 
141
141
  Call `visitor-engagement-score` with the default 30-day window to rank `:Person` contacts by engagement: visit count × recency × scroll depth × dwell time. The output is the nurture queue. Top of the queue gets outreach today; bottom decays into low-touch.
142
142
 
143
- When composing outbound messages that include a listing URL, append `&v=<token>` to the click URL. The token is minted server-side against the recipient's `:Person` elementId; the agent receives it as a sentinel in the system prompt for that outreach. Without the token the recipient's click is anonymous, defeating the point of the nurture loop.
143
+ When composing outbound messages that include a listing URL, mint a token via the `mint-visitor-token` tool against the recipient's `:Person` elementId (`mint-visitor-token({ personId })`) and append the returned `token` as `&v=<token>` to the click URL. Without the token the recipient's click is anonymous, defeating the point of the nurture loop. Mint a fresh token per outbound message — TTL defaults to 30 days.
144
144
 
145
145
  The signal that says "this person stopped engaging": their score drops two consecutive weeks. That's a prompt for a pattern-break message (different listing, different framing), not a follow-up of the same thread.
146
146
 
@@ -79,5 +79,5 @@ Before the morning round, call `visitor-recent-by-person` for every `:Person` in
79
79
 
80
80
  Also call `visitor-recent-by-page` for any listing that picks up activity from previously-unidentified visitors — these are anonymous prospects worth surfacing to the operator even when no `:Person` is attached.
81
81
 
82
- Outbound URLs in the briefing (links the operator might click to send to the contact) are minted via `mint-visitor-token` against the `:Person` and appended as `&v=<token>` to the existing `/listings/<slug>/click?session=<sk>` URL. The token binds the recipient's browser to the `:Person` on click.
82
+ Outbound URLs in the briefing (links the operator might click to send to the contact) are minted via the `mint-visitor-token` tool — call it with the recipient's `:Person` elementId (`mint-visitor-token({ personId })`) and append the returned `token` as `&v=<token>` to the existing `/listings/<slug>/click?session=<sk>` URL. The token binds the recipient's browser to the `:Person` on click; never inline the secret yourself, always call the tool. Mint a fresh token per outbound URL — TTL defaults to 30 days and tokens are cheap to re-issue.
83
83
 
@@ -162,5 +162,5 @@ The weekly vendor email reports on viewing activity against the vendor's listing
162
162
 
163
163
  The vendor sees real engagement, not just enquiry count. Tone stays factual — "twelve known visitors and twenty-three anonymous browsers" is honest; "interest is strong" is editorial. Let the numbers speak.
164
164
 
165
- Outbound URLs in vendor emails are NOT tokenised — the vendor is not the visitor, and binding the vendor's browser to their own listing's `:Person` is meaningless.
165
+ When the update embeds a link to the vendor's listing, mint a token via `mint-visitor-token({ personId })` against the vendor's `:Person` elementId and append the returned `token` as `&v=<token>` to the `/listings/<slug>/click?session=<sk>` URL. The vendor is a `:Person` like any other contact — binding their click attributes their own engagement with the marketing page (did they open the report, did they actually look at their listing) to the same visitor graph the buyer side feeds. Mint a fresh token per outbound update; TTL defaults to 30 days.
166
166