@rubytech/create-maxy-code 0.1.118 → 0.1.120
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/neo4j/schema.cypher +95 -4
- package/payload/platform/package-lock.json +141 -125
- package/payload/platform/plugins/aeo/PLUGIN.md +1 -1
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/audit-heuristics.test.d.ts +2 -0
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/audit-heuristics.test.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/audit-heuristics.test.js +121 -0
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/audit-heuristics.test.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/schema-mapping.test.d.ts +2 -0
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/schema-mapping.test.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/schema-mapping.test.js +129 -0
- package/payload/platform/plugins/aeo/mcp/dist/__tests__/schema-mapping.test.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/aeo/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/index.js +171 -0
- package/payload/platform/plugins/aeo/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/audit-heuristics.d.ts +27 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/audit-heuristics.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/audit-heuristics.js +274 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/audit-heuristics.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/neo4j.d.ts +5 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/neo4j.js +38 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/schema-mapping.d.ts +48 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/schema-mapping.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/schema-mapping.js +254 -0
- package/payload/platform/plugins/aeo/mcp/dist/lib/schema-mapping.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-audit-page.d.ts +25 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-audit-page.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-audit-page.js +78 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-audit-page.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-emit-jsonld.d.ts +18 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-emit-jsonld.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-emit-jsonld.js +56 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-emit-jsonld.js.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts +37 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts.map +1 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js +94 -0
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.d.ts +12 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js +34 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js.map +1 -1
- package/payload/platform/plugins/docs/references/aeo.md +2 -2
- package/payload/platform/plugins/docs/references/visitor-graph.md +82 -0
- package/payload/platform/plugins/memory/references/schema-base.md +44 -0
- package/payload/server/{chunk-FJCI6X3H.js → chunk-HCYM5FLU.js} +2 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/privacy.html +66 -0
- package/payload/server/public/v.js +191 -0
- package/payload/server/server.js +473 -85
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type AuditResult } from "../lib/audit-heuristics.js";
|
|
2
|
+
export interface AuditPageInput {
|
|
3
|
+
accountId: string;
|
|
4
|
+
/** Provide one of `url` or `html`. */
|
|
5
|
+
url?: string;
|
|
6
|
+
html?: string;
|
|
7
|
+
/**
|
|
8
|
+
* When true, the audit result is persisted as an `:AEOAudit` node
|
|
9
|
+
* with `HAS_AEO_AUDIT` edge from `targetKnowledgeDocumentId` (if
|
|
10
|
+
* given) or the synthetic `targetUrl` audit history.
|
|
11
|
+
*/
|
|
12
|
+
persist?: boolean;
|
|
13
|
+
targetKnowledgeDocumentId?: string;
|
|
14
|
+
agentSlug?: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AuditPageResult extends AuditResult {
|
|
18
|
+
target: string;
|
|
19
|
+
audit: {
|
|
20
|
+
elementId?: string;
|
|
21
|
+
runAt: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function auditPage(input: AuditPageInput): Promise<AuditPageResult>;
|
|
25
|
+
//# sourceMappingURL=aeo-audit-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aeo-audit-page.d.ts","sourceRoot":"","sources":["../../src/tools/aeo-audit-page.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAGzE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QACL,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAeD,wBAAsB,SAAS,CAC7B,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,eAAe,CAAC,CA2E1B"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { auditHtml } from "../lib/audit-heuristics.js";
|
|
3
|
+
import { getSession } from "../lib/neo4j.js";
|
|
4
|
+
async function fetchHtml(url) {
|
|
5
|
+
const res = await fetch(url, {
|
|
6
|
+
redirect: "follow",
|
|
7
|
+
headers: { "User-Agent": "maxy-aeo-audit/0.1" },
|
|
8
|
+
});
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
throw new Error(`aeo-audit-page: GET ${url} returned HTTP ${res.status}`);
|
|
11
|
+
}
|
|
12
|
+
return await res.text();
|
|
13
|
+
}
|
|
14
|
+
export async function auditPage(input) {
|
|
15
|
+
if (!input.url && !input.html) {
|
|
16
|
+
throw new Error("aeo-audit-page requires one of url or html");
|
|
17
|
+
}
|
|
18
|
+
if (input.url && input.html) {
|
|
19
|
+
throw new Error("aeo-audit-page: pass exactly one of url or html, not both");
|
|
20
|
+
}
|
|
21
|
+
const html = input.html ?? (await fetchHtml(input.url));
|
|
22
|
+
const target = input.url ?? "(inline html)";
|
|
23
|
+
const result = auditHtml(html);
|
|
24
|
+
const runAt = new Date().toISOString();
|
|
25
|
+
const fails = result.heuristics
|
|
26
|
+
.filter((h) => h.status === "fail")
|
|
27
|
+
.map((h) => h.name);
|
|
28
|
+
const warns = result.heuristics
|
|
29
|
+
.filter((h) => h.status === "warn")
|
|
30
|
+
.map((h) => h.name);
|
|
31
|
+
process.stderr.write(`[aeo-audit] target=${target} score=${result.score} fails=${fails.join(",") || "-"} warns=${warns.join(",") || "-"}\n`);
|
|
32
|
+
let elementId;
|
|
33
|
+
if (input.persist) {
|
|
34
|
+
const auditId = randomUUID();
|
|
35
|
+
const session = getSession();
|
|
36
|
+
try {
|
|
37
|
+
const r = await session.run(`CREATE (a:AEOAudit {
|
|
38
|
+
accountId: $accountId,
|
|
39
|
+
auditId: $auditId,
|
|
40
|
+
target: $target,
|
|
41
|
+
score: $score,
|
|
42
|
+
heuristicsJson: $heuristicsJson,
|
|
43
|
+
runAt: $runAt,
|
|
44
|
+
createdByAgent: $agent,
|
|
45
|
+
createdBySession: $session,
|
|
46
|
+
createdAt: $runAt
|
|
47
|
+
})
|
|
48
|
+
WITH a
|
|
49
|
+
OPTIONAL MATCH (d:KnowledgeDocument)
|
|
50
|
+
WHERE elementId(d) = $kdocId AND d.accountId = $accountId
|
|
51
|
+
FOREACH (_ IN CASE WHEN d IS NULL THEN [] ELSE [1] END |
|
|
52
|
+
MERGE (d)-[:HAS_AEO_AUDIT]->(a)
|
|
53
|
+
)
|
|
54
|
+
RETURN elementId(a) AS id`, {
|
|
55
|
+
accountId: input.accountId,
|
|
56
|
+
auditId,
|
|
57
|
+
target,
|
|
58
|
+
score: result.score,
|
|
59
|
+
heuristicsJson: JSON.stringify(result.heuristics),
|
|
60
|
+
runAt,
|
|
61
|
+
agent: input.agentSlug ?? "system",
|
|
62
|
+
session: input.sessionId ?? null,
|
|
63
|
+
kdocId: input.targetKnowledgeDocumentId ?? null,
|
|
64
|
+
});
|
|
65
|
+
elementId = r.records[0]?.get("id");
|
|
66
|
+
process.stderr.write(`[aeo-audit] persisted auditId=${auditId} elementId=${elementId ?? "-"}\n`);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await session.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...result,
|
|
74
|
+
target,
|
|
75
|
+
audit: { elementId, runAt },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=aeo-audit-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aeo-audit-page.js","sourceRoot":"","sources":["../../src/tools/aeo-audit-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAoB,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA0B7C,KAAK,UAAU,SAAS,CAAC,GAAW;IAClC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,EAAE,YAAY,EAAE,oBAAoB,EAAE;KAChD,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,uBAAuB,GAAG,kBAAkB,GAAG,CAAC,MAAM,EAAE,CACzD,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAqB;IAErB,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,SAAS,CAAC,KAAK,CAAC,GAAa,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,IAAI,eAAe,CAAC;IAC5C,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAEvC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;SAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;SAClC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,sBAAsB,MAAM,UAAU,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,UAAU,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CACvH,CAAC;IAEF,IAAI,SAA6B,CAAC;IAClC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CACzB;;;;;;;;;;;;;;;;;mCAiB2B,EAC3B;gBACE,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,OAAO;gBACP,MAAM;gBACN,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,cAAc,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC;gBACjD,KAAK;gBACL,KAAK,EAAE,KAAK,CAAC,SAAS,IAAI,QAAQ;gBAClC,OAAO,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;gBAChC,MAAM,EAAE,KAAK,CAAC,yBAAyB,IAAI,IAAI;aAChD,CACF,CAAC;YACF,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAuB,CAAC;YAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCAAiC,OAAO,cAAc,SAAS,IAAI,GAAG,IAAI,CAC3E,CAAC;QACJ,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,MAAM;QACT,MAAM;QACN,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;KAC5B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MissingPropertyError, type JsonLdObject } from "../lib/schema-mapping.js";
|
|
2
|
+
export interface EmitJsonLdInput {
|
|
3
|
+
accountId: string;
|
|
4
|
+
/** Pull entity from the graph by Neo4j elementId. */
|
|
5
|
+
entityId?: string;
|
|
6
|
+
/** Or render a JSON-LD block from an inline label + property bag. */
|
|
7
|
+
label?: string;
|
|
8
|
+
properties?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface EmitJsonLdResult {
|
|
11
|
+
jsonLd: JsonLdObject;
|
|
12
|
+
scriptBlock: string;
|
|
13
|
+
schemaType: string;
|
|
14
|
+
source: "graph" | "inline";
|
|
15
|
+
}
|
|
16
|
+
export declare function emitJsonLd(input: EmitJsonLdInput): Promise<EmitJsonLdResult>;
|
|
17
|
+
export { MissingPropertyError };
|
|
18
|
+
//# sourceMappingURL=aeo-emit-jsonld.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aeo-emit-jsonld.d.ts","sourceRoot":"","sources":["../../src/tools/aeo-emit-jsonld.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,oBAAoB,EACpB,KAAK,YAAY,EAClB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC5B;AAMD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,gBAAgB,CAAC,CA8D3B;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getSession } from "../lib/neo4j.js";
|
|
2
|
+
import { buildJsonLd, renderJsonLdScript, MissingPropertyError, } from "../lib/schema-mapping.js";
|
|
3
|
+
function isPropertyBag(v) {
|
|
4
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5
|
+
}
|
|
6
|
+
export async function emitJsonLd(input) {
|
|
7
|
+
if (!input.entityId && !input.label) {
|
|
8
|
+
throw new Error("aeo-emit-jsonld requires either entityId or label+properties");
|
|
9
|
+
}
|
|
10
|
+
if (input.entityId) {
|
|
11
|
+
const session = getSession();
|
|
12
|
+
try {
|
|
13
|
+
const r = await session.run(`MATCH (n) WHERE elementId(n) = $id AND n.accountId = $accountId
|
|
14
|
+
RETURN labels(n) AS labels, properties(n) AS props, elementId(n) AS id LIMIT 1`, { id: input.entityId, accountId: input.accountId });
|
|
15
|
+
if (r.records.length === 0) {
|
|
16
|
+
throw new Error(`aeo-emit-jsonld: no node with elementId=${input.entityId} in account=${input.accountId}`);
|
|
17
|
+
}
|
|
18
|
+
const rec = r.records[0];
|
|
19
|
+
const labels = rec.get("labels");
|
|
20
|
+
const props = rec.get("props");
|
|
21
|
+
const id = rec.get("id");
|
|
22
|
+
const primaryLabel = labels.find((l) => l !== "Resource") ?? labels[0];
|
|
23
|
+
const obj = buildJsonLd({
|
|
24
|
+
label: primaryLabel,
|
|
25
|
+
elementId: id,
|
|
26
|
+
properties: props,
|
|
27
|
+
});
|
|
28
|
+
process.stderr.write(`[aeo-emit-jsonld] entityId=${id} schemaType=${obj["@type"]} source=graph\n`);
|
|
29
|
+
return {
|
|
30
|
+
jsonLd: obj,
|
|
31
|
+
scriptBlock: renderJsonLdScript(obj),
|
|
32
|
+
schemaType: obj["@type"],
|
|
33
|
+
source: "graph",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await session.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!input.label || !isPropertyBag(input.properties)) {
|
|
41
|
+
throw new Error("aeo-emit-jsonld: label requires a properties object");
|
|
42
|
+
}
|
|
43
|
+
const obj = buildJsonLd({
|
|
44
|
+
label: input.label,
|
|
45
|
+
properties: input.properties,
|
|
46
|
+
});
|
|
47
|
+
process.stderr.write(`[aeo-emit-jsonld] label=${input.label} schemaType=${obj["@type"]} source=inline\n`);
|
|
48
|
+
return {
|
|
49
|
+
jsonLd: obj,
|
|
50
|
+
scriptBlock: renderJsonLdScript(obj),
|
|
51
|
+
schemaType: obj["@type"],
|
|
52
|
+
source: "inline",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export { MissingPropertyError };
|
|
56
|
+
//# sourceMappingURL=aeo-emit-jsonld.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aeo-emit-jsonld.js","sourceRoot":"","sources":["../../src/tools/aeo-emit-jsonld.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the `llms.txt` and `llms-full.txt` pair for an account's
|
|
3
|
+
* published content. Format follows the current llmstxt.org draft:
|
|
4
|
+
*
|
|
5
|
+
* # <Site name>
|
|
6
|
+
* > <Short description>
|
|
7
|
+
*
|
|
8
|
+
* ## <Section name>
|
|
9
|
+
* - [<Title>](<URL>): <Summary>
|
|
10
|
+
*
|
|
11
|
+
* `llms-full.txt` concatenates the full text of every listed page in
|
|
12
|
+
* the same order, separated by a blank line and the page's title as an
|
|
13
|
+
* H1.
|
|
14
|
+
*
|
|
15
|
+
* Source: `KnowledgeDocument` nodes for the account that have a `url`
|
|
16
|
+
* property. `summary` and `body` properties feed the index and the
|
|
17
|
+
* concatenated dump respectively. Pages without a `url` are skipped
|
|
18
|
+
* (they aren't reachable from a public site), and the count of skipped
|
|
19
|
+
* pages is returned so the operator can audit completeness.
|
|
20
|
+
*/
|
|
21
|
+
export interface WriteLlmsTxtInput {
|
|
22
|
+
accountId: string;
|
|
23
|
+
siteName: string;
|
|
24
|
+
siteDescription?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface WriteLlmsTxtResult {
|
|
27
|
+
llmsTxt: string;
|
|
28
|
+
llmsFullTxt: string;
|
|
29
|
+
pageCount: number;
|
|
30
|
+
skippedNoUrl: number;
|
|
31
|
+
sizeBytes: {
|
|
32
|
+
index: number;
|
|
33
|
+
full: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare function writeLlmsTxt(input: WriteLlmsTxtInput): Promise<WriteLlmsTxtResult>;
|
|
37
|
+
//# sourceMappingURL=aeo-write-llms-txt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aeo-write-llms-txt.d.ts","sourceRoot":"","sources":["../../src/tools/aeo-write-llms-txt.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AAqBD,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CA4F7B"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getSession } from "../lib/neo4j.js";
|
|
2
|
+
const HEADER_RX = /^#\s/m;
|
|
3
|
+
function escapeMarkdown(text) {
|
|
4
|
+
return text.replace(/[\[\]\(\)\n\r]/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
function summariseBody(body) {
|
|
7
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
8
|
+
if (flat.length <= 200)
|
|
9
|
+
return flat;
|
|
10
|
+
return flat.slice(0, 197).trimEnd() + "…";
|
|
11
|
+
}
|
|
12
|
+
export async function writeLlmsTxt(input) {
|
|
13
|
+
const session = getSession();
|
|
14
|
+
let rows = [];
|
|
15
|
+
let skippedNoUrl = 0;
|
|
16
|
+
try {
|
|
17
|
+
const r = await session.run(`MATCH (d:KnowledgeDocument {accountId: $accountId})
|
|
18
|
+
RETURN d.title AS title, d.url AS url, d.summary AS summary,
|
|
19
|
+
coalesce(d.body, d.abstract, '') AS body
|
|
20
|
+
ORDER BY d.title ASC`, { accountId: input.accountId });
|
|
21
|
+
for (const rec of r.records) {
|
|
22
|
+
const title = rec.get("title") ?? "";
|
|
23
|
+
const url = rec.get("url") ?? "";
|
|
24
|
+
const summary = rec.get("summary") ?? "";
|
|
25
|
+
const body = rec.get("body") ?? "";
|
|
26
|
+
if (!url || !title) {
|
|
27
|
+
skippedNoUrl += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
rows.push({
|
|
31
|
+
title: escapeMarkdown(title),
|
|
32
|
+
url: url.trim(),
|
|
33
|
+
summary: escapeMarkdown(summary) || summariseBody(body) || "(no summary)",
|
|
34
|
+
body: body.trim(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await session.close();
|
|
40
|
+
}
|
|
41
|
+
const indexLines = [];
|
|
42
|
+
indexLines.push(`# ${escapeMarkdown(input.siteName)}`);
|
|
43
|
+
indexLines.push("");
|
|
44
|
+
if (input.siteDescription) {
|
|
45
|
+
indexLines.push(`> ${escapeMarkdown(input.siteDescription)}`);
|
|
46
|
+
indexLines.push("");
|
|
47
|
+
}
|
|
48
|
+
indexLines.push("## Pages");
|
|
49
|
+
indexLines.push("");
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
indexLines.push(`- [${row.title}](${row.url}): ${row.summary}`);
|
|
52
|
+
}
|
|
53
|
+
indexLines.push("");
|
|
54
|
+
indexLines.push("<!-- generated by @maxy/aeo aeo-write-llms-txt; spec: https://llmstxt.org/ -->");
|
|
55
|
+
const llmsTxt = indexLines.join("\n");
|
|
56
|
+
const fullLines = [];
|
|
57
|
+
fullLines.push(`# ${escapeMarkdown(input.siteName)}`);
|
|
58
|
+
fullLines.push("");
|
|
59
|
+
if (input.siteDescription) {
|
|
60
|
+
fullLines.push(`> ${escapeMarkdown(input.siteDescription)}`);
|
|
61
|
+
fullLines.push("");
|
|
62
|
+
}
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
fullLines.push(`# ${row.title}`);
|
|
65
|
+
fullLines.push("");
|
|
66
|
+
fullLines.push(`Source: ${row.url}`);
|
|
67
|
+
fullLines.push("");
|
|
68
|
+
if (row.body) {
|
|
69
|
+
// Demote any in-body H1s to H2 so a single H1 hierarchy holds.
|
|
70
|
+
const safeBody = row.body.replace(HEADER_RX, "## ");
|
|
71
|
+
fullLines.push(safeBody);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
fullLines.push(row.summary);
|
|
75
|
+
}
|
|
76
|
+
fullLines.push("");
|
|
77
|
+
fullLines.push("---");
|
|
78
|
+
fullLines.push("");
|
|
79
|
+
}
|
|
80
|
+
const llmsFullTxt = fullLines.join("\n");
|
|
81
|
+
const sizeBytes = {
|
|
82
|
+
index: Buffer.byteLength(llmsTxt, "utf-8"),
|
|
83
|
+
full: Buffer.byteLength(llmsFullTxt, "utf-8"),
|
|
84
|
+
};
|
|
85
|
+
process.stderr.write(`[aeo-llms-txt] site=${input.siteName} pages=${rows.length} skippedNoUrl=${skippedNoUrl} indexBytes=${sizeBytes.index} fullBytes=${sizeBytes.full}\n`);
|
|
86
|
+
return {
|
|
87
|
+
llmsTxt,
|
|
88
|
+
llmsFullTxt,
|
|
89
|
+
pageCount: rows.length,
|
|
90
|
+
skippedNoUrl,
|
|
91
|
+
sizeBytes,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=aeo-write-llms-txt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aeo-write-llms-txt.js","sourceRoot":"","sources":["../../src/tools/aeo-write-llms-txt.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.
|
|
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
|
|
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.
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"contact-erase.js","sourceRoot":"","sources":["../../src/tools/contact-erase.ts"],"names":[],"mappings":"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"}
|
|
@@ -73,9 +73,9 @@ Every tool emits a single log line per invocation:
|
|
|
73
73
|
|
|
74
74
|
Diagnostic path: `grep -E '^\[aeo-' platform-logs/*.log | grep <urlOrEntityId>`.
|
|
75
75
|
|
|
76
|
-
## What this plugin does not do
|
|
76
|
+
## What this plugin does not do
|
|
77
77
|
|
|
78
|
-
- **No monitor.** Tracking whether your brand is cited by Claude / ChatGPT / Perplexity / Gemini
|
|
78
|
+
- **No citation monitor — out of scope.** Tracking whether your brand is cited by Claude / ChatGPT / Perplexity / Gemini would require multi-engine answer harvesting that doesn't fit maxy-code's no-API-key architecture. Archived without sprinting (Task 363). Check citation manually when needed.
|
|
79
79
|
- **No auto-emission on page render.** `aeo-emit-jsonld` is callable on demand. Wiring it into the platform's page-generator render path is per-renderer work, filed as a follow-up.
|
|
80
80
|
- **No publish-hook regeneration of `llms.txt`.** The tool runs on demand. Hooking it into the publish event is a follow-up.
|
|
81
81
|
|
|
@@ -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:
|