@rubytech/create-maxy-code 0.1.132 → 0.1.140
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/dist/__tests__/installer-specialist-registration.test.js +5 -40
- package/dist/index.js +24 -28
- package/dist/specialist-registration.js +0 -15
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/lib/graph-search/dist/boosts.d.ts +39 -0
- package/payload/platform/lib/graph-search/dist/boosts.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/boosts.js +57 -0
- package/payload/platform/lib/graph-search/dist/boosts.js.map +1 -0
- package/payload/platform/lib/graph-search/dist/dedup.d.ts +29 -0
- package/payload/platform/lib/graph-search/dist/dedup.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/dedup.js +97 -0
- package/payload/platform/lib/graph-search/dist/dedup.js.map +1 -0
- package/payload/platform/lib/graph-search/dist/index.d.ts +70 -8
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-search/dist/index.js +323 -62
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-search/dist/query-expansion.d.ts +37 -0
- package/payload/platform/lib/graph-search/dist/query-expansion.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/query-expansion.js +101 -0
- package/payload/platform/lib/graph-search/dist/query-expansion.js.map +1 -0
- package/payload/platform/lib/graph-search/dist/route.d.ts +29 -0
- package/payload/platform/lib/graph-search/dist/route.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/route.js +53 -0
- package/payload/platform/lib/graph-search/dist/route.js.map +1 -0
- package/payload/platform/lib/graph-search/dist/rrf-fusion.d.ts +31 -0
- package/payload/platform/lib/graph-search/dist/rrf-fusion.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/rrf-fusion.js +57 -0
- package/payload/platform/lib/graph-search/dist/rrf-fusion.js.map +1 -0
- package/payload/platform/lib/graph-search/src/__tests__/boosts.test.ts +59 -0
- package/payload/platform/lib/graph-search/src/__tests__/dedup.test.ts +83 -0
- package/payload/platform/lib/graph-search/src/__tests__/query-expansion.test.ts +96 -0
- package/payload/platform/lib/graph-search/src/__tests__/route.test.ts +62 -0
- package/payload/platform/lib/graph-search/src/__tests__/rrf-fusion.test.ts +39 -0
- package/payload/platform/lib/graph-search/src/boosts.ts +61 -0
- package/payload/platform/lib/graph-search/src/dedup.ts +108 -0
- package/payload/platform/lib/graph-search/src/index.ts +407 -65
- package/payload/platform/lib/graph-search/src/query-expansion.ts +129 -0
- package/payload/platform/lib/graph-search/src/route.ts +70 -0
- package/payload/platform/lib/graph-search/src/rrf-fusion.ts +62 -0
- package/payload/platform/lib/obsidian-parser/dist/index.d.ts +98 -0
- package/payload/platform/lib/obsidian-parser/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/obsidian-parser/dist/index.js +480 -0
- package/payload/platform/lib/obsidian-parser/dist/index.js.map +1 -0
- package/payload/platform/lib/obsidian-parser/src/index.ts +572 -0
- package/payload/platform/lib/obsidian-parser/tsconfig.json +8 -0
- package/payload/platform/neo4j/schema.cypher +86 -1
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/.claude-plugin/marketplace.json +10 -0
- package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +4 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js +6 -4
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +36 -10
- package/payload/platform/plugins/docs/references/neo4j.md +63 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
- package/payload/platform/plugins/docs/references/session-retrospective.md +11 -4
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +2 -2
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +3 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +203 -7
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-property.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-property.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-property.test.js +25 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-property.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js +70 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js +50 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +99 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +21 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +24 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts +13 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js +151 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts +55 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.js +290 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +102 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js +18 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +64 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +258 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +14 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.d.ts +29 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.js +22 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +31 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +182 -50
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +76 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.d.ts +127 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.js +477 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.d.ts +30 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.js +129 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/scripts/backfill-typed-edges.ts +72 -0
- package/payload/platform/plugins/memory/mcp/scripts/generate-edge-docs.ts +75 -0
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +5 -0
- package/payload/platform/plugins/memory/references/schema-base.md +44 -0
- package/payload/platform/plugins/notion-import/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/notion-import/PLUGIN.md +27 -0
- package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +110 -0
- package/payload/platform/plugins/notion-import/skills/notion-import/references/attachments.md +55 -0
- package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +81 -0
- package/payload/platform/plugins/notion-import/skills/notion-import/references/page-tree.md +61 -0
- package/payload/platform/plugins/notion-import/skills/notion-import/references/workspace-export.md +41 -0
- package/payload/platform/plugins/obsidian-import/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/obsidian-import/PLUGIN.md +39 -0
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +92 -0
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/attachments.md +80 -0
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/daily-notes.md +31 -0
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/vault-structure.md +46 -0
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/wikilinks.md +70 -0
- package/payload/platform/scripts/seed-neo4j.sh +49 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +4 -3
- package/payload/platform/templates/specialists/agents/database-operator.md +21 -1
- package/payload/server/{adminuser-self-heal-QAWOZ3JV.js → adminuser-self-heal-YC47O34W.js} +2 -1
- package/payload/server/{chunk-JSBRDJBE.js → chunk-HYQNUVGO.js} +9 -1
- package/payload/server/{chunk-HCYM5FLU.js → chunk-T3DJD5QR.js} +3 -1
- package/payload/server/maxy-edge.js +3 -2
- package/payload/server/public/assets/{admin-FcRHAL-3.js → admin-BM9aXUUh.js} +1 -1
- package/payload/server/public/assets/{data-Ds37mflX.js → data-Ds2ECtfP.js} +1 -1
- package/payload/server/public/assets/{graph-CmWRhaiS.js → graph-BzxObKMM.js} +1 -1
- package/payload/server/public/assets/{graph-labels-Ch2r00Gt.js → graph-labels-Dwgep_nr.js} +1 -1
- package/payload/server/public/assets/{page-BcHhJXUt.js → page-BlkBsjP2.js} +1 -1
- package/payload/server/public/assets/page-h_SA5SVQ.js +51 -0
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +803 -76
- package/payload/server/public/assets/page-BOtNny_4.js +0 -51
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-retrospective-skip-rate.js","sourceRoot":"","sources":["../../src/tools/session-retrospective-skip-rate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAgBH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,IAAyB,EACzB,OAAgB,EAChB,SAAiB;IAEjB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;IAErE,wEAAwE;IACxE,kEAAkE;IAClE,sEAAsE;IACtE,6BAA6B;IAC7B,wEAAwE;IACxE,wEAAwE;IACxE,mEAAmE;IACnE,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;GAuBd,CAAC;IAEF,4EAA4E;IAC5E,oDAAoD;IACpD,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;GAuBtB,CAAC;IAEF,MAAM,WAAW,GAAG;QAClB,SAAS;QACT,MAAM;QACN,SAAS;QACT,OAAO;QACP,MAAM;QACN,MAAM;QACN,MAAM;QACN,QAAQ;KACT,CAAC;IAEF,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;YACvC,SAAS;YACT,UAAU;YACV,WAAW;SACZ,CAAC,CAAC;QACH,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE;YAC/C,SAAS;YACT,UAAU;YACV,WAAW;SACZ,CAAC,CAAC;QACH,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC3B,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,cAAc,EAAE,CAAC;YACjB,uBAAuB,EAAE,CAAC;YAC1B,QAAQ,EAAE,CAAC;YACX,qBAAqB,EAAE,IAAI;SAC5B,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACrB,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,MAAM,uBAAuB,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9E,MAAM,aAAa,GAAG,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,cAAc,GAAG,CAAC;QACjC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,GAAG,uBAAuB,CAAC,GAAG,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC,CAAC;IACN,MAAM,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3C,MAAM,gBAAgB,GAAG,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAE9E,OAAO,CAAC,GAAG,CACT,+CAA+C,YAAY,YAAY,cAAc,cAAc,uBAAuB,cAAc,QAAQ,mBAAmB,gBAAgB,IAAI,MAAM,EAAE,CAChM,CAAC;IAEF,OAAO;QACL,cAAc;QACd,uBAAuB;QACvB,QAAQ;QACR,qBAAqB,EAAE,gBAAgB;KACxC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot backfill of the typed-edge pass over every existing prose-bearing
|
|
3
|
+
* node for the current account. Idempotent — MERGE handles re-runs cleanly,
|
|
4
|
+
* so a second invocation accepts zero edges.
|
|
5
|
+
*
|
|
6
|
+
* Usage (admin-agent invokable via Bash):
|
|
7
|
+
* cd platform/plugins/memory/mcp
|
|
8
|
+
* node --loader ts-node/esm scripts/backfill-typed-edges.ts
|
|
9
|
+
*
|
|
10
|
+
* Reads ACCOUNT_ID from env (same convention as the MCP server). Loops
|
|
11
|
+
* `runTypedEdgePass` with `sinceIso: null` in 500-node pages until the
|
|
12
|
+
* page returns 0 nodes (exhausted). Logs per-batch progress with
|
|
13
|
+
* `[typed-edge-pass:backfill]`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getSession, closeDriver } from "../src/lib/neo4j.js";
|
|
17
|
+
import { runTypedEdgePass } from "../src/lib/typed-edge-pass.js";
|
|
18
|
+
|
|
19
|
+
const PAGE_SIZE = 500;
|
|
20
|
+
|
|
21
|
+
async function main(): Promise<void> {
|
|
22
|
+
const accountId = process.env.ACCOUNT_ID;
|
|
23
|
+
if (!accountId) {
|
|
24
|
+
console.error("ACCOUNT_ID env var is required");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let batch = 0;
|
|
29
|
+
let totalNodes = 0;
|
|
30
|
+
let totalAccepted = 0;
|
|
31
|
+
let totalRejected = 0;
|
|
32
|
+
let totalFailed = 0;
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
const session = getSession();
|
|
36
|
+
try {
|
|
37
|
+
const result = await runTypedEdgePass({
|
|
38
|
+
session,
|
|
39
|
+
accountId,
|
|
40
|
+
sinceIso: null,
|
|
41
|
+
conversationId: null,
|
|
42
|
+
maxNodes: PAGE_SIZE,
|
|
43
|
+
});
|
|
44
|
+
batch += 1;
|
|
45
|
+
totalNodes += result.nodes;
|
|
46
|
+
totalAccepted += result.accepted;
|
|
47
|
+
totalRejected += result.rejected;
|
|
48
|
+
totalFailed += result.failed;
|
|
49
|
+
console.log(
|
|
50
|
+
`[typed-edge-pass:backfill] batch=${batch} done=${totalNodes} accepted=${result.accepted} rejected=${result.rejected} failed=${result.failed} ms=${result.ms}`,
|
|
51
|
+
);
|
|
52
|
+
if (result.nodes < PAGE_SIZE || result.accepted === 0) {
|
|
53
|
+
// Exhausted: either the page came back short, or a re-run page
|
|
54
|
+
// produced no new accepted edges (idempotency convergence).
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
await session.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(
|
|
63
|
+
`[typed-edge-pass:backfill] complete batches=${batch} totalNodes=${totalNodes} totalAccepted=${totalAccepted} totalRejected=${totalRejected} totalFailed=${totalFailed}`,
|
|
64
|
+
);
|
|
65
|
+
await closeDriver();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch(async (err) => {
|
|
69
|
+
console.error(`[typed-edge-pass:backfill] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
+
await closeDriver();
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit the typed-edge enumeration into the public docs from the
|
|
3
|
+
* `TYPED_EDGE_ALLOWLIST` source of truth, so the prompt, the validator,
|
|
4
|
+
* and the docs cannot drift from each other.
|
|
5
|
+
*
|
|
6
|
+
* Targets:
|
|
7
|
+
* - .docs/neo4j.md (parent maxy-code .docs surface)
|
|
8
|
+
* - platform/plugins/docs/references/neo4j.md (bundled into the payload)
|
|
9
|
+
*
|
|
10
|
+
* Replaces the block delimited by:
|
|
11
|
+
* <!-- TYPED-EDGE-TABLE:START -->
|
|
12
|
+
* <!-- TYPED-EDGE-TABLE:END -->
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node --loader ts-node/esm scripts/generate-edge-docs.ts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
19
|
+
import { resolve } from "node:path";
|
|
20
|
+
import { TYPED_EDGE_ALLOWLIST } from "../src/lib/typed-edge-schema.js";
|
|
21
|
+
|
|
22
|
+
const TARGET_PATHS = [
|
|
23
|
+
// maxy-code/.docs/neo4j.md — resolved relative to this script.
|
|
24
|
+
resolve(import.meta.dirname ?? __dirname, "../../../../../.docs/neo4j.md"),
|
|
25
|
+
// bundled docs twin
|
|
26
|
+
resolve(import.meta.dirname ?? __dirname, "../../../docs/references/neo4j.md"),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const START_MARKER = "<!-- TYPED-EDGE-TABLE:START -->";
|
|
30
|
+
const END_MARKER = "<!-- TYPED-EDGE-TABLE:END -->";
|
|
31
|
+
|
|
32
|
+
function buildTable(): string {
|
|
33
|
+
const rows = TYPED_EDGE_ALLOWLIST
|
|
34
|
+
.map(({ sourceLabel, edgeType, targetLabel }) => `| ${sourceLabel} | ${edgeType} | ${targetLabel} |`)
|
|
35
|
+
.join("\n");
|
|
36
|
+
return [
|
|
37
|
+
START_MARKER,
|
|
38
|
+
"",
|
|
39
|
+
"<!-- Generated by platform/plugins/memory/mcp/scripts/generate-edge-docs.ts from TYPED_EDGE_ALLOWLIST. Do not edit by hand. -->",
|
|
40
|
+
"",
|
|
41
|
+
"| Source label | Edge type | Target label |",
|
|
42
|
+
"|---|---|---|",
|
|
43
|
+
rows,
|
|
44
|
+
"",
|
|
45
|
+
END_MARKER,
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function applyToFile(path: string, table: string): "wrote" | "missing" | "no-markers" {
|
|
50
|
+
if (!existsSync(path)) return "missing";
|
|
51
|
+
const current = readFileSync(path, "utf-8");
|
|
52
|
+
const startIdx = current.indexOf(START_MARKER);
|
|
53
|
+
const endIdx = current.indexOf(END_MARKER);
|
|
54
|
+
let next: string;
|
|
55
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
56
|
+
// No markers — append the section under a clear heading rather than
|
|
57
|
+
// failing. Idempotent on subsequent runs because we add the markers.
|
|
58
|
+
next = `${current.trimEnd()}\n\n## Typed-edge allowlist (generated)\n\n${table}\n`;
|
|
59
|
+
writeFileSync(path, next);
|
|
60
|
+
return "wrote";
|
|
61
|
+
}
|
|
62
|
+
next = current.slice(0, startIdx) + table + current.slice(endIdx + END_MARKER.length);
|
|
63
|
+
writeFileSync(path, next);
|
|
64
|
+
return "wrote";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function main(): void {
|
|
68
|
+
const table = buildTable();
|
|
69
|
+
for (const path of TARGET_PATHS) {
|
|
70
|
+
const result = applyToFile(path, table);
|
|
71
|
+
console.log(`[generate-edge-docs] ${path} -> ${result}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main();
|
|
@@ -22,6 +22,11 @@ export default defineConfig({
|
|
|
22
22
|
"src/lib/__tests__/schema-cypher-drift.test.ts",
|
|
23
23
|
// Task 350 — label-origin gate (refuse agent-side LocalBusiness writes).
|
|
24
24
|
"src/lib/__tests__/label-origin-gate.test.ts",
|
|
25
|
+
// Task 305 — typed-edge allowlist schema invariants.
|
|
26
|
+
"src/lib/__tests__/typed-edge-schema.test.ts",
|
|
27
|
+
// Task 306 — compiled-truth + timeline.
|
|
28
|
+
"src/lib/__tests__/timeline-extractor.test.ts",
|
|
29
|
+
"src/lib/__tests__/graph-write-gate-property.test.ts",
|
|
25
30
|
],
|
|
26
31
|
testTimeout: 10_000,
|
|
27
32
|
},
|
|
@@ -46,6 +46,7 @@ When loading this reference, confirm which schema files were consulted by noting
|
|
|
46
46
|
| VoiceEdit | `VoiceEdit` | platform-native (Task 356 — operator edit on an agent draft) | — | `accountId`, `adminUserId`, `originalText`, `editedText`, `intent`, `occurredAt` |
|
|
47
47
|
| AEO Audit | `AEOAudit` | platform-native (AEO audit history) | — | `accountId`, `auditId`, `target`, `score`, `heuristicsJson`, `runAt`, `createdByAgent`, `createdBySession`, `createdAt` |
|
|
48
48
|
| Idea | `Idea` | platform-native (signal-detector original-thinking candidate) | — | `accountId`, `body`, `conversationId`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
|
|
49
|
+
| TimelineEvent | `TimelineEvent` | platform-native (Task 306 — compiled-truth + timeline doctrine) | — | `accountId`, `timelineEventId`, `occurredAt`, `kind`, `summary`, `source` |
|
|
49
50
|
|
|
50
51
|
**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"`.
|
|
51
52
|
|
|
@@ -71,6 +72,49 @@ When loading this reference, confirm which schema files were consulted by noting
|
|
|
71
72
|
|
|
72
73
|
---
|
|
73
74
|
|
|
75
|
+
## Compiled truth + timeline (Task 306)
|
|
76
|
+
|
|
77
|
+
Every `:Person`, `:Organization`, and `:Concept` carries a two-part knowledge shape: a **compiled truth** above the rule (the agent's current best understanding of the subject, rewritten when evidence changes) and an **append-only timeline** below (dated events linked via `:HAS_TIMELINE_EVENT`, never edited).
|
|
78
|
+
|
|
79
|
+
| Property | Type | Purpose |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `compiledTruth` | string ≤2000 chars | The current summary. Empty on first write. |
|
|
82
|
+
| `compiledTruthUpdatedAt` | datetime | Last rewriter or operator write. Epoch on first write. |
|
|
83
|
+
| `compiledTruthSource` | `'rewriter' \| 'operator'` | Last writer. Operator edits get a 7-day cooldown that the rewriter respects unless `forceRewrite=true` is passed. |
|
|
84
|
+
|
|
85
|
+
The compiled truth is system-managed: direct writes to any of the three properties via `memory-update` are **rejected loudly**. The only paths to mutate are:
|
|
86
|
+
|
|
87
|
+
- `memory-update(nodeId, {}, rewriteCompiledTruth: true)` — invokes the OAuth-LLM rewriter against the most recent 10 `:TimelineEvent` rows.
|
|
88
|
+
- `memory-update(nodeId, {}, rewriteCompiledTruth: true, forceRewrite: true)` — same, but bypasses the 7-day operator cooldown.
|
|
89
|
+
- The graph UI's per-property edit surface — stamps `compiledTruthSource: 'operator'` so the rewriter honours the cooldown.
|
|
90
|
+
|
|
91
|
+
### Timeline events
|
|
92
|
+
|
|
93
|
+
`:TimelineEvent` is distinct from the scheduling plugin's `:Event` (which holds `eventId`-unique calendar/reminder rows). Naming chosen to avoid collision.
|
|
94
|
+
|
|
95
|
+
`:HAS_TIMELINE_EVENT` is distinct from visitor-analytics' `:HAS_EVENT` (`Session → PageView/Click/ScrollMilestone`). Naming chosen to avoid collision.
|
|
96
|
+
|
|
97
|
+
`kind` enum (5 values, no catch-all):
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
mentioned | met | quoted | attended | transacted
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The extractor refuses on ambiguity rather than collapsing to "other". Default on write is `mentioned`; the typed-edge post-write hook (Task 305) upgrades the kind asynchronously when it can.
|
|
104
|
+
|
|
105
|
+
### Write doctrine
|
|
106
|
+
|
|
107
|
+
Path-anchored MERGE — never label-only:
|
|
108
|
+
|
|
109
|
+
```cypher
|
|
110
|
+
MATCH (e) WHERE elementId(e) = $entityId AND e.accountId = $accountId
|
|
111
|
+
MERGE (e)-[:HAS_TIMELINE_EVENT]->(t:TimelineEvent {timelineEventId: $id})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`memory-write` runs the deterministic date extractor against the `body` property of any `:Person` / `:Organization` / `:Concept` write and creates one `:TimelineEvent` per parseable dated statement.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
74
118
|
## Forbidden Properties
|
|
75
119
|
|
|
76
120
|
Properties listed here are **rejected at write-time** by the schema validator on the listed label. Rejection runs after the synonym pass, so a `firstName → givenName` rewrite still happens before the forbidden-property check. The rule is per-label: `name` is forbidden on `Person` only; `Organization`, `Event`, `Conversation`, `Service`, etc. retain `name` as a canonical property.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "notion-import",
|
|
3
|
+
"description": "Import a Notion workspace export (markdown + CSV directory) into the Maxy Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Rubytech LLC"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: notion-import
|
|
3
|
+
description: "Import a Notion workspace export (markdown + CSV directory) into the Maxy Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default."
|
|
4
|
+
tools: []
|
|
5
|
+
always: false
|
|
6
|
+
embed: false
|
|
7
|
+
specialist: database-operator
|
|
8
|
+
metadata: {"platform":{"optional":true,"pluginKey":"notion-import"}}
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Notion Import
|
|
12
|
+
|
|
13
|
+
Ingests a Notion workspace export (the native "Export all workspace content" markdown + CSV dump) into the Maxy Neo4j graph. Skill-only plugin — no MCP server, no admin tools added. The skill runs under `specialists:database-operator`, which owns graph-write delegation from admin.
|
|
14
|
+
|
|
15
|
+
## When this applies
|
|
16
|
+
|
|
17
|
+
The admin agent delegates to `specialists:database-operator` when the operator drops a Notion export directory (or zip path) into chat and asks to import it. The specialist runs the skill's import-provenance confirmation flow before any page or row is read, then walks the export.
|
|
18
|
+
|
|
19
|
+
## Intra-plugin growth
|
|
20
|
+
|
|
21
|
+
A Notion export contains pages (`.md`), databases (CSV pairs — schema CSV plus rows CSV), nested page subdirectories, and an `attachments/` tree. The skill ships four references covering the four shapes the export expresses — workspace traversal, database parsing, page-tree hierarchy, attachment copy. Per-database-type references land here as new files under `references/` as operators surface new shapes; they do not become new plugins.
|
|
22
|
+
|
|
23
|
+
## Relationship to other plugins
|
|
24
|
+
|
|
25
|
+
- **memory** — the underlying Cypher-write surface used by the skill (`memory-write`, `memory-update`, `memory-search`). Every node carries `source='notion'` + `createdByAgent='notion-import'` for provenance.
|
|
26
|
+
- **database-operator specialist** — owns execution. See the delegation clause in [database-operator.md](../../templates/specialists/agents/database-operator.md).
|
|
27
|
+
- **linkedin-import** — sibling skill-only import plugin (different specialist owner — `librarian` predates the database-operator delegation doctrine). The two plugins share no code.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: notion-import
|
|
3
|
+
description: Import a Notion workspace export (markdown + CSV) into a {{productName}} Neo4j graph. Triggers when the user asks to import a Notion workspace, ingest a Notion export, or mentions a `Export-*.zip` / unzipped Notion export directory. Pages become `:KnowledgeDocument` nodes, database rows become typed entities (or `:KnowledgeDocument` if no schema mapping fits), database schemas become `:KnowledgeDocument` siblings, hierarchy is preserved via `:HAS_SECTION`, attachments are copied locally and written as `:DigitalDocument`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Notion Import
|
|
7
|
+
|
|
8
|
+
**Invoked from `specialists:database-operator`** when the operator drops a Notion workspace export directory (or `Export-*.zip` path) and asks to import it. The admin agent dispatches the database-operator; the database-operator loads this skill.
|
|
9
|
+
|
|
10
|
+
**Anchor model.** A Notion export does not have a single subject the way a LinkedIn archive does — pages have multiple authors, databases are shared workspaces, mentions span the whole org. The skill anchors **import provenance** on the importing `:AdminUser` via `IMPORTED_BY` (so every node in the import can be traced back to who pushed the button); page authorship is captured separately via `AUTHORED` when the export records an author. The `:AdminUser` is identity, not subject — pages are not "owned" by the importer.
|
|
11
|
+
|
|
12
|
+
Every node this skill writes carries an `importId` property (UUID per skill run) so a future dream-cycle pass can detect orphans or selectively re-ingest.
|
|
13
|
+
|
|
14
|
+
## Import-provenance confirmation (mandatory first step)
|
|
15
|
+
|
|
16
|
+
A Notion export is dropped by one operator on behalf of one workspace. Before any file is read, the skill confirms two things:
|
|
17
|
+
|
|
18
|
+
1. **Importing AdminUser.** The `:AdminUser` who pushed the button. Default is the live operator (the session's userId); confirm by echoing `userId=<id> name=<name>` and accepting yes/no. If the operator is importing on behalf of someone else — a partner, a client whose workspace they're staging — accept either an existing AdminUser userId or a new `:Person` natural-key (`givenName` + `familyName` + at least one of `email` / `telephone`) as the importing identity. Persist as `$importerNodeId`.
|
|
19
|
+
2. **Workspace label.** A short human label (`"Joel's personal workspace"`, `"Rubytech ops"`) used as the value of `:NotionWorkspace.name`. One `:NotionWorkspace` node is MERGEd per `(accountId, name)` natural key; re-imports of the same workspace match it.
|
|
20
|
+
|
|
21
|
+
The skill refuses to read the export directory until both values are echo-confirmed.
|
|
22
|
+
|
|
23
|
+
## Anchor shape
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
(:AdminUser {userId: $importerUserId})
|
|
27
|
+
-[:IMPORTED { importId, source: 'notion', startedAt }]->
|
|
28
|
+
(:NotionWorkspace { accountId, name })
|
|
29
|
+
-[:HAS_SECTION]-> (:KnowledgeDocument { kind: 'notion-page', notionId, importId })
|
|
30
|
+
-[:HAS_SECTION]-> (:KnowledgeDocument { kind: 'notion-database', notionId, importId })
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`:NotionWorkspace` is the only new label the skill needs; everything below it reuses {{productName}} canonical labels (`:KnowledgeDocument`, `:Person`, `:Organization`, `:DigitalDocument`). Page hierarchy from Notion's parent-child relation is written as `:HAS_SECTION` between `:KnowledgeDocument` pages.
|
|
34
|
+
|
|
35
|
+
## Invariants
|
|
36
|
+
|
|
37
|
+
1. **Schema first.** No new constraints or indexes are required; the canonical schema in [`platform/plugins/memory/references/schema-base.md`](../../../../plugins/memory/references/schema-base.md) covers every label this skill writes. If the operator's workspace surfaces a database shape no canonical label fits, the rows are written as `:KnowledgeDocument` with `kind: 'notion-row'` and the gap is reported to admin — the skill does not mint new labels at runtime.
|
|
38
|
+
2. **Provenance confirmed first.** No file is read until `$importerNodeId` (AdminUser or external Person) and `$workspaceName` are persisted and echo-confirmed.
|
|
39
|
+
3. **Closed edge vocabulary.** Edges written are drawn from `schema-base.md` only. Unmapped Notion relations (a custom DB-to-DB link the operator named `sponsored_by`, say) land as `:MENTIONS` from the source page's `:KnowledgeDocument`, with the Notion relation label stored as a property (`mentionContext: '<relation-name>'`). `RELATES_TO` is **not** a schema edge and is not used.
|
|
40
|
+
4. **Account isolation on fuzzy resolves.** Every inline `@person` mention resolves against `:Person {accountId: $accountId}` — never label-only. A "John Smith" mention in the importing account never matches a `:Person` in another account, even if the names are identical. When no in-account match is found, the mention is written as a new `:Person` with `givenName` / `familyName` parsed from the mention text and `source='notion'`.
|
|
41
|
+
5. **Idempotent MERGE on Notion IDs.** Every Notion page and row carries a stable Notion UUID. Pages MERGE on `(accountId, notionId)`; database rows MERGE on `(accountId, notionId)`; attachments MERGE on `(accountId, sha256)`. Re-import of the same export is a property-update no-op on existing nodes — `skipped` counter advances, no duplicates.
|
|
42
|
+
6. **Provenance stamps.** Every new node: `createdByAgent='notion-import'`, `createdBySource='notion'`, `createdBySession=$sessionId` (UUID per skill run), `createdAt=datetime()`, `importId=$importId`, plus `source='notion'`.
|
|
43
|
+
7. **No raw Cypher, no bash improvisation.** Writes route through `mcp__plugin_memory_memory__memory-write` and `mcp__plugin_memory_memory__memory-update` (per-node), plus `mcp__plugin_memory_memory__memory-search` for parent / mention resolution. If a write shape the skill needs is missing from the tool surface, the gap is filed as a follow-up task — never improvised via Bash.
|
|
44
|
+
|
|
45
|
+
## Execution model
|
|
46
|
+
|
|
47
|
+
1. Run the provenance-confirmation flow. Persist `$importerNodeId`, `$workspaceName`, `$importId` (fresh UUID), `$sessionId`.
|
|
48
|
+
2. MERGE the `:NotionWorkspace { accountId, name }` and the `(:AdminUser)-[:IMPORTED]->(:NotionWorkspace)` edge.
|
|
49
|
+
3. Walk the export directory per [workspace-export.md](references/workspace-export.md) — classify each entry as page (`.md`), database schema (`<db-name>.csv` paired with row CSVs), database rows, attachment subdirectory, or nested page subdirectory.
|
|
50
|
+
4. For each shape, follow the matching reference:
|
|
51
|
+
- Pages → [page-tree.md](references/page-tree.md). Resolves parent page, MERGEs the `:KnowledgeDocument`, writes `:HAS_SECTION` from parent. Inline `@person` mentions resolved per Invariant 4.
|
|
52
|
+
- Databases → [databases.md](references/databases.md). Database itself becomes a `:KnowledgeDocument {kind: 'notion-database'}`. Each row becomes a typed entity if the database's name + schema fits a canonical label (a "People" DB → `:Person`; a "Projects" DB → `:Project`); otherwise the row lands as `:KnowledgeDocument {kind: 'notion-row'}` with the schema properties preserved on the node. Notion's typed relation columns become canonical edges where schema allows; unmapped relations are `:MENTIONS`.
|
|
53
|
+
- Attachments → [attachments.md](references/attachments.md). Copied to `{accountDir}/archive/notion/<importId>/`, content-hashed, written as `:DigitalDocument {kind: 'notion-attachment', sha256}`. Referenced from pages via `:HAS_ATTACHMENT`.
|
|
54
|
+
5. Emit `[notion-import] importId=<id> pages=<n> rows=<n> relations=<n> mentions=<n> attachments=<n> skipped=<n> ms=<elapsed>` at completion. Per-file errors emit `[notion-import:error] file=<path> reason=<…>`; the skill continues past parse failures so a single bad file does not abort the entire import.
|
|
55
|
+
|
|
56
|
+
## Selective-ingest gate (bulk imports)
|
|
57
|
+
|
|
58
|
+
A Notion workspace typically contains 200–5,000 pages. Writing all of them in one shot defeats compression-on-write — most pages will never be queried, and the noise compounds with every subsequent import.
|
|
59
|
+
|
|
60
|
+
**Threshold:** when the export walk discovers more than **200 pages or 500 database rows**, pause and ask the operator to filter before any write begins. Natural filter axes for Notion:
|
|
61
|
+
|
|
62
|
+
- **Top-level page subtree** — "only pages under `Engineering/`", "only `Personal/Journal/`".
|
|
63
|
+
- **Database name** — "only the `People` and `Projects` DBs", "skip the `Daily Standup` DB".
|
|
64
|
+
- **Last-edited date** — "only pages edited since 2024-01-01".
|
|
65
|
+
|
|
66
|
+
The operator picks one axis or a combination. The skill applies the filter to the walk's candidate set before issuing any write. Re-importing later with a wider filter hits the same `(accountId, notionId)` natural keys; existing nodes are property-updated, only the new-only delta is created.
|
|
67
|
+
|
|
68
|
+
## File-shape roster
|
|
69
|
+
|
|
70
|
+
The Notion export is a directory of:
|
|
71
|
+
|
|
72
|
+
| Shape | Detection | Reference |
|
|
73
|
+
|-------|-----------|-----------|
|
|
74
|
+
| Page (markdown) | `*.md` file at any depth, with a Notion UUID suffix in the filename | [page-tree.md](references/page-tree.md) |
|
|
75
|
+
| Database schema | `<name>.csv` with a sibling directory of the same name containing per-row `.md` files | [databases.md](references/databases.md) |
|
|
76
|
+
| Database row | `<row-name>.md` inside a database subdirectory, with the parent CSV's column set as front-matter properties | [databases.md](references/databases.md) |
|
|
77
|
+
| Attachment | Any file under an `<image>/`, `<pdf>/`, or other media-bearing subdirectory adjacent to its referencing page | [attachments.md](references/attachments.md) |
|
|
78
|
+
| Nested workspace subtree | A directory whose name carries a Notion UUID and contains both `.md` pages and CSV databases | [workspace-export.md](references/workspace-export.md) |
|
|
79
|
+
|
|
80
|
+
## Explicitly out of scope
|
|
81
|
+
|
|
82
|
+
- **Notion API live sync.** This is one-shot from a manual export only. Live API sync (incremental delta, webhook listening, two-way write-back) is a separate task if commissioned.
|
|
83
|
+
- **Formulas, rollups, synced blocks.** The export materialises computed values as text; the skill imports the materialised text but not the formula source. Re-computing rollups against the graph is downstream work.
|
|
84
|
+
- **Notion Web Clipper saved pages.** Clipper pages live in a different export shape (HTML, not markdown) and have different semantics (third-party source ingest). They land in a future task, not here.
|
|
85
|
+
- **Back-sync to Notion.** Writing changes from the graph back into the Notion workspace is not in scope.
|
|
86
|
+
- **Bulk archiveType handler.** Per-node `memory-write` issues one Cypher transaction per node. For a 5,000-page workspace this is ~5,000 round-trips with no per-transaction atomicity. A `notion-pages` archiveType handler in `memory-archive-write` would batch this; the follow-up is tracked at `.tasks/395-notion-bulk-archive-handler.md`.
|
|
87
|
+
|
|
88
|
+
## Schema-mapping table
|
|
89
|
+
|
|
90
|
+
The skill maps Notion shapes to canonical {{productName}} labels and edges. Schema additions are last resort — every row below uses an existing label from `schema-base.md`.
|
|
91
|
+
|
|
92
|
+
| Notion shape | Canonical label | Notes |
|
|
93
|
+
|--------------|-----------------|-------|
|
|
94
|
+
| Workspace | `:NotionWorkspace` | New label, single property `name` plus `accountId`. Only new label introduced by this skill. |
|
|
95
|
+
| Page | `:KnowledgeDocument { kind: 'notion-page' }` | Body markdown stored as `text`. Hierarchy via `:HAS_SECTION`. |
|
|
96
|
+
| Page authored-by | `(:Person)-[:AUTHORED]->(:KnowledgeDocument)` | When the export records an author, resolve to in-account `:Person`. |
|
|
97
|
+
| Database (the table itself) | `:KnowledgeDocument { kind: 'notion-database' }` | Schema preserved as `schema` (JSON-stringified property list). |
|
|
98
|
+
| Database row, canonical-fit | `:Person`, `:Organization`, `:Project`, `:Task`, `:Event` | Database name + columns drive label selection. A "People" DB with `email`/`telephone` columns → `:Person`. |
|
|
99
|
+
| Database row, no fit | `:KnowledgeDocument { kind: 'notion-row' }` | Properties preserved on the node; surface to admin in the completion log if any DB lands here entirely. |
|
|
100
|
+
| Notion relation, schema-mapped | Canonical edge from `schema-base.md` | A relation column named `works_at` between People and Companies → `(:Person)-[:WORKS_FOR]->(:Organization)`. |
|
|
101
|
+
| Notion relation, unmapped | `:MENTIONS { mentionContext: '<relation-name>' }` | Source page `:KnowledgeDocument` → target. Preserves the Notion relation name for later audit. |
|
|
102
|
+
| Inline `@person` mention | `(:KnowledgeDocument)-[:MENTIONS]->(:Person)` | Account-filtered fuzzy resolve per Invariant 4. |
|
|
103
|
+
| Attachment | `:DigitalDocument { kind: 'notion-attachment', sha256 }` | Files copied to `{accountDir}/archive/notion/<importId>/`. |
|
|
104
|
+
| Page hierarchy | `(:KnowledgeDocument)-[:HAS_SECTION]->(:KnowledgeDocument)` | Parent-of-child per Notion's nesting. |
|
|
105
|
+
|
|
106
|
+
If the operator's workspace introduces a database that fits none of the canonical labels and is not satisfied by the `:KnowledgeDocument` fallback, surface the gap to admin in the completion summary — never mint a new label at runtime.
|
|
107
|
+
|
|
108
|
+
## Natural-edge discipline (why this matters)
|
|
109
|
+
|
|
110
|
+
Every edge written corresponds to a relationship the Notion export actually expresses. `:HAS_SECTION` is real — Notion explicitly records page parents. `:AUTHORED` is real when the export records it. `:WORKS_FOR` is real when a People DB has a Companies-relation column. Synthetic "attach-everything-to-the-importer" edges are forbidden — the importer is provenance (via `IMPORTED`), not the subject of every page. A workspace with thousands of unrelated pages does not get thousands of edges manufactured to "make it reachable"; the workspace-level `IMPORTED` edge already makes them reachable through the importer.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# attachments
|
|
2
|
+
|
|
3
|
+
Reference for copying Notion attachments — images, PDFs, and other media — into the account's local archive and recording them as graph nodes.
|
|
4
|
+
|
|
5
|
+
## Shape on disk
|
|
6
|
+
|
|
7
|
+
Notion exports media files alongside the page that references them. The export's directory layout varies — small media may live in the same directory as the referencing `.md`, larger media in a sibling subdirectory named after the page. The skill does not assume a single layout; it follows attachment references **from the page markdown**, not by enumerating media files.
|
|
8
|
+
|
|
9
|
+
Referenced shapes:
|
|
10
|
+
|
|
11
|
+
- `` — image embed.
|
|
12
|
+
- `[Doc](Doc%20Name%20<32-hex-notionId>.pdf)` — file link.
|
|
13
|
+
- HTML `<img src="...">` tags inside the markdown (older exports) — also captured.
|
|
14
|
+
|
|
15
|
+
The referenced filename is URL-decoded to get the relative path on disk.
|
|
16
|
+
|
|
17
|
+
## Where they go
|
|
18
|
+
|
|
19
|
+
Attachments copy to `{accountDir}/archive/notion/<importId>/<sha256>.<ext>`:
|
|
20
|
+
|
|
21
|
+
- `{accountDir}` is the account's directory (the same directory that holds `account.json`), resolved per the platform's existing `accountDir` convention used by `platform/lib/admins-write` and elsewhere.
|
|
22
|
+
- `<importId>` is the UUID for this skill run — keeps a single import's attachments grouped on disk.
|
|
23
|
+
- `<sha256>` is the content hash of the file, computed at copy time. Filename collisions on the same hash skip the copy (already present from a prior import of the same file).
|
|
24
|
+
- `<ext>` is the original extension preserved from the source filename.
|
|
25
|
+
|
|
26
|
+
The path scheme is deliberately content-hash-keyed so multiple pages referencing the same file (a logo embedded across many pages, say) share one copy on disk and one `:DigitalDocument` node in the graph.
|
|
27
|
+
|
|
28
|
+
## What each attachment becomes
|
|
29
|
+
|
|
30
|
+
One `:DigitalDocument { kind: 'notion-attachment' }` node per content hash:
|
|
31
|
+
|
|
32
|
+
- `sha256` — the content hash; the MERGE natural key together with `accountId`.
|
|
33
|
+
- `name` — original filename (without the Notion ID suffix where present).
|
|
34
|
+
- `mimeType` — inferred from extension.
|
|
35
|
+
- `bytes` — file size in bytes.
|
|
36
|
+
- `localPath` — absolute path under `{accountDir}/archive/notion/<importId>/`.
|
|
37
|
+
- `accountId`, `importId`, `source='notion'`, plus standard provenance per SKILL invariant 6.
|
|
38
|
+
|
|
39
|
+
Page-to-attachment edge: `(:KnowledgeDocument)-[:HAS_ATTACHMENT]->(:DigitalDocument)`. One edge per referencing page; if a single page references the same attachment three times, one edge is MERGEd (idempotent).
|
|
40
|
+
|
|
41
|
+
## Idempotency
|
|
42
|
+
|
|
43
|
+
`:DigitalDocument` MERGEs on `(accountId, sha256)`. Re-import of the same file by content hash matches the existing node — `localPath` updates if the file was copied into a fresh `<importId>` directory, every other property stays. The `:HAS_ATTACHMENT` edge also MERGEs, so re-imports do not duplicate.
|
|
44
|
+
|
|
45
|
+
## What the attachment step does NOT do
|
|
46
|
+
|
|
47
|
+
- **Does not** OCR scanned PDFs or extract text from images. That belongs to a separate ingest step (a future task can chain `memory-ingest` to derive text from `:DigitalDocument` nodes by mime type).
|
|
48
|
+
- **Does not** fetch from Notion's CDN. Notion exports are self-contained — every referenced file is in the zip. If a markdown reference points at an absolute Notion URL (not a relative export path), it is treated as an external link and a `:MENTIONS` edge to a `:KnowledgeDocument { kind: 'external-link', url }` is written instead, with no file copy.
|
|
49
|
+
- **Does not** delete attachments on re-import when the page no longer references the file. Orphan attachment detection is a graph-prune concern, not an importer concern.
|
|
50
|
+
|
|
51
|
+
## Failure modes
|
|
52
|
+
|
|
53
|
+
- `[notion-import:error] reason=attachment-not-on-disk path=<rel-path>` — the page markdown references a file the walker cannot locate. Skip the copy, skip the edge, log.
|
|
54
|
+
- `[notion-import:error] reason=copy-failed path=<rel-path> errno=<…>` — filesystem error during copy. Log and continue; the page itself is still written, only the attachment is missing.
|
|
55
|
+
- `[notion-import:error] reason=hash-collision-different-content sha256=<hash>` — two files in the same import produced the same hash but the filenames disagree on extension. Should be impossible in practice; if observed, log and write both as separate `:DigitalDocument` nodes with disambiguated extensions.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# databases
|
|
2
|
+
|
|
3
|
+
Reference for ingesting Notion databases — both the database (schema) and its rows.
|
|
4
|
+
|
|
5
|
+
## Shape on disk
|
|
6
|
+
|
|
7
|
+
A Notion database lands as a `.csv` file plus a sibling directory of the same basename. The CSV is the table-view export; the directory contains one `.md` per row, with the row's properties as YAML front-matter and the row body as markdown below.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Projects 7a1b2c…/
|
|
11
|
+
Projects 7a1b2c….csv ← schema + row table
|
|
12
|
+
Projects 7a1b2c…/ ← row markdowns
|
|
13
|
+
Phoenix Migration 9f3a…md
|
|
14
|
+
Q3 Strategy d4e5…md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CSV columns are the database's properties. Column types are not encoded in the CSV directly — they are inferred from the values (date-shaped, URL-shaped, person-mention-shaped, relation-shaped). A relation column's cells contain the human title plus Notion ID of the linked row.
|
|
18
|
+
|
|
19
|
+
## What the database itself becomes
|
|
20
|
+
|
|
21
|
+
Each Notion database is written as one `:KnowledgeDocument { kind: 'notion-database' }` node:
|
|
22
|
+
|
|
23
|
+
- `notionId` — the 32-hex ID from the CSV basename.
|
|
24
|
+
- `name` — human title from the CSV basename.
|
|
25
|
+
- `schema` — JSON-stringified list of `{ name, inferredType }` per column.
|
|
26
|
+
- `parentPageNotionId` — the page that contains the database, if any.
|
|
27
|
+
|
|
28
|
+
The database is attached to the workspace root via `(:NotionWorkspace)-[:HAS_SECTION]->(:KnowledgeDocument)` and, when nested under a page, also via `(:KnowledgeDocument {page-parent})-[:HAS_SECTION]->(:KnowledgeDocument {database})`.
|
|
29
|
+
|
|
30
|
+
## Label selection for rows
|
|
31
|
+
|
|
32
|
+
Per-row label is decided by the database's name and column set together — not by either alone.
|
|
33
|
+
|
|
34
|
+
- **`:Person`** when the database name matches `^(people|contacts|crm|team|members?)$` (case-insensitive) AND the columns contain at least one of `email`, `phone`, `telephone`, `linkedin`. Properties map onto schema-base camelCase: `givenName` / `familyName` from a single `name` column split on first space (preserve `name` only if no canonical Person property fits); `email`, `telephone`, `jobTitle`, etc.
|
|
35
|
+
- **`:Organization`** when the name matches `^(companies|orgs|organizations?|accounts)$` AND the columns contain `website` or `domain`.
|
|
36
|
+
- **`:Project`** when the name matches `^projects?$` AND the columns contain at least one of `status`, `startDate`, `dueDate`, `owner`.
|
|
37
|
+
- **`:Task`** when the name matches `^(tasks?|todo|to-?dos?|action items?)$` AND the columns contain at least one of `done`, `status`, `assignee`, `dueDate`.
|
|
38
|
+
- **`:Event`** when the name matches `^(events?|meetings?|calendar)$` AND the columns contain `startDate` / `start` / `date`.
|
|
39
|
+
- **`:KnowledgeDocument { kind: 'notion-row' }`** — fallback. The CSV columns are preserved on the node as properties. Surface to admin in the completion log if any database falls entirely into this bucket — it usually means the workspace has a custom DB shape worth handling explicitly in a future iteration.
|
|
40
|
+
|
|
41
|
+
When admin's brief is ambiguous about which label a database should land in, the database-operator's `{ambiguity, proposed}` protocol applies — ask, don't guess.
|
|
42
|
+
|
|
43
|
+
## Row write recipe
|
|
44
|
+
|
|
45
|
+
For each row in the parsed CSV:
|
|
46
|
+
|
|
47
|
+
1. **Resolve the parent.** Every row's parent is its database (the `:KnowledgeDocument { kind: 'notion-database' }`). Edge: `(:KnowledgeDocument {database})-[:HAS_SECTION]->(row)`.
|
|
48
|
+
2. **Select the label** per the rules above.
|
|
49
|
+
3. **Map CSV columns to schema-base properties.** Use canonical camelCase. Unknown columns land as properties on the node with their original column name preserved.
|
|
50
|
+
4. **Resolve relation columns.** A relation column cell is `"Linked Row Title <32-hex-notionId>"`. Look up `:KnowledgeDocument {notionId}` (any kind — the linked row may have already been imported, or may land later in the same import). When the target is found and the relation name maps to a canonical edge (table below), write the canonical edge; otherwise write `:MENTIONS` with `mentionContext: '<relation-column-name>'`.
|
|
51
|
+
5. **Issue the write** via `mcp__plugin_memory_memory__memory-write` with the selected label, properties, and edges.
|
|
52
|
+
|
|
53
|
+
## Notion-relation → canonical-edge map
|
|
54
|
+
|
|
55
|
+
Examples — the table is extensible per workspace, but the principle is fixed: a relation maps to a canonical edge only when the schema vocabulary in `schema-base.md` (Relationship Patterns + Edges introduced by document-ingest, plus any vertical-schema edges loaded at runtime) names a matching one.
|
|
56
|
+
|
|
57
|
+
| Notion relation column name | Canonical edge |
|
|
58
|
+
|-----------------------------|----------------|
|
|
59
|
+
| `works_at`, `company`, `employer` | `(:Person)-[:WORKS_FOR]->(:Organization)` |
|
|
60
|
+
| `assignee`, `owner` | `(:Person)-[:OWNS]->(:Project)` / `(:Person)-[:ASSIGNED_TO]->(:Task)` per row label |
|
|
61
|
+
| `project`, `belongs_to_project` | `(:Task)-[:PART_OF]->(:Project)` |
|
|
62
|
+
| `attended`, `participants` | `(:Person)-[:ATTENDED]->(:Event)` |
|
|
63
|
+
| `mentions` (Notion's own typed mention relation) | `:MENTIONS` |
|
|
64
|
+
| anything else | `:MENTIONS { mentionContext: '<relation-column-name>' }` |
|
|
65
|
+
|
|
66
|
+
The mapping table lives in this reference — not in a server-side enum — so a workspace with custom relation names is handled by extending this file, not by code change.
|
|
67
|
+
|
|
68
|
+
## Forward-reference resolution
|
|
69
|
+
|
|
70
|
+
A row's relation column may point at a row not yet written (later in the walk). The write loop runs in two passes:
|
|
71
|
+
|
|
72
|
+
- **Pass 1** — write every node, deferring relation columns. Skip the relation step but keep the relation cell text alongside the manifest entry.
|
|
73
|
+
- **Pass 2** — walk the manifest again with every Notion node now MERGEd, resolve each deferred relation by `notionId`, write the edge.
|
|
74
|
+
|
|
75
|
+
Two passes are deterministic and keep the per-row write idempotent. The completion log reports the `relations` count from pass 2.
|
|
76
|
+
|
|
77
|
+
## Failure modes
|
|
78
|
+
|
|
79
|
+
- `[notion-import:error] reason=row-schema-mismatch file=<path>` — the row's front-matter has properties not in the parent CSV's schema. Write the row with the union of properties; log the mismatch; continue.
|
|
80
|
+
- `[notion-import:error] reason=relation-target-not-found notionId=<id>` — pass 2 saw a relation cell pointing at a Notion ID that is not in the import (a row outside the operator's filter, or an externally-linked workspace row). Skip the edge; log; continue.
|
|
81
|
+
- `[notion-import:error] reason=label-ambiguous db=<name>` — the name + column heuristic matched no canonical label and the operator's brief did not specify. Fall back to `:KnowledgeDocument { kind: 'notion-row' }` and surface the gap in the completion log.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# page-tree
|
|
2
|
+
|
|
3
|
+
Reference for ingesting Notion pages — the `.md` files at every depth of the export.
|
|
4
|
+
|
|
5
|
+
## Shape on disk
|
|
6
|
+
|
|
7
|
+
A Notion page is a markdown file with a 32-hex Notion ID suffix in its basename. Front-matter at the top (when present) carries page-level properties: `Created`, `Created by`, `Last edited`, `Last edited by`, tags. The body is markdown — headings, paragraphs, lists, embedded media references, inline `@person` mentions, and inline `@page` mentions (page-to-page links).
|
|
8
|
+
|
|
9
|
+
A page with children lives alongside a directory of the same basename containing the child pages and their own subdirectories.
|
|
10
|
+
|
|
11
|
+
## What each page becomes
|
|
12
|
+
|
|
13
|
+
One `:KnowledgeDocument { kind: 'notion-page' }` node per page:
|
|
14
|
+
|
|
15
|
+
- `notionId` — 32-hex from the basename.
|
|
16
|
+
- `title` — human title from the basename.
|
|
17
|
+
- `text` — full markdown body (front-matter stripped; the front-matter fields are mapped to properties below).
|
|
18
|
+
- `notionCreatedAt`, `notionLastEditedAt` — front-matter date fields parsed to `datetime()`.
|
|
19
|
+
- `accountId`, `importId`, `source='notion'`, `createdByAgent='notion-import'`, `createdBySession`, `createdAt` — standard provenance per SKILL invariant 6.
|
|
20
|
+
|
|
21
|
+
## Hierarchy
|
|
22
|
+
|
|
23
|
+
Page hierarchy is written as `:HAS_SECTION` between `:KnowledgeDocument` nodes:
|
|
24
|
+
|
|
25
|
+
- **Top-level pages** — parent is the `:NotionWorkspace`. Edge: `(:NotionWorkspace)-[:HAS_SECTION]->(:KnowledgeDocument)`.
|
|
26
|
+
- **Nested pages** — parent is the enclosing page's `:KnowledgeDocument`. Edge: `(:KnowledgeDocument {parent})-[:HAS_SECTION]->(:KnowledgeDocument {child})`.
|
|
27
|
+
|
|
28
|
+
The parent's `notionId` is the basename of the enclosing directory; resolve via `mcp__plugin_memory_memory__memory-search` keyed on `(accountId, notionId)`.
|
|
29
|
+
|
|
30
|
+
## Authorship
|
|
31
|
+
|
|
32
|
+
When the front-matter records `Created by` (or `Author`), resolve the named user against in-account `:Person` per SKILL invariant 4 (account-filtered fuzzy match on `givenName + familyName`). Write `(:Person)-[:AUTHORED]->(:KnowledgeDocument)` when a match is found or a new `:Person` is created. If the front-matter only carries a Notion display name that resolves to no in-account `:Person` and the import flow already created a `:Person` for an earlier mention with the same display name, MERGE on `(accountId, displayName)` so a Notion user named once in front-matter and once inline becomes one node, not two.
|
|
33
|
+
|
|
34
|
+
`Last edited by` is not currently written as an edge — page-level edit provenance is downstream work (a future task can add `:LAST_EDITED_BY` if needed).
|
|
35
|
+
|
|
36
|
+
## Inline `@person` mentions
|
|
37
|
+
|
|
38
|
+
Notion exports inline person mentions as plain text in the markdown body — typically the user's display name without an `@` prefix in the markdown source, but operators sometimes preserve `@`-prefixed text. The skill matches both shapes:
|
|
39
|
+
|
|
40
|
+
1. Detect candidate mentions — bracketed names from Notion's link syntax `[Person Name](https://www.notion.so/...)`, plain `@`-prefixed tokens, and front-matter `Mentioned` lists (when present).
|
|
41
|
+
2. For each candidate, run `mcp__plugin_memory_memory__memory-search` with `accountId` filter and the candidate's display name as the search term. Use the search's existing fuzzy semantics — do not invent a new matcher.
|
|
42
|
+
3. On match, write `(:KnowledgeDocument)-[:MENTIONS]->(:Person)`. On miss, write a new `:Person { givenName, familyName, source='notion', accountId }` and the same edge — the mention is preserved either way.
|
|
43
|
+
|
|
44
|
+
The account-filter step is load-bearing. A "John Smith" mention in the importing account never matches a `:Person` in another account, even if the names collide. See SKILL invariant 4.
|
|
45
|
+
|
|
46
|
+
## Inline `@page` mentions (page-to-page links)
|
|
47
|
+
|
|
48
|
+
A page-to-page link in Notion exports as `[Other Page Title](Other%20Page%20Title%20<32-hex-notionId>.md)`. Resolve by `notionId`:
|
|
49
|
+
|
|
50
|
+
- If the target is in the current import (`:KnowledgeDocument {notionId}` exists or will exist after this pass), defer to pass 2 and write `(:KnowledgeDocument)-[:MENTIONS]->(:KnowledgeDocument)` with `mentionContext: 'page-link'`.
|
|
51
|
+
- If the target is outside the import (an externally-linked page the operator's filter excluded, or a page in a different workspace), skip the edge and log `[notion-import:error] reason=page-link-target-not-found notionId=<id>`.
|
|
52
|
+
|
|
53
|
+
## Idempotency
|
|
54
|
+
|
|
55
|
+
Pages MERGE on `(accountId, notionId)`. Re-importing the same export with no changes is a no-op — every property update writes the same value back, every edge MERGE matches existing. The `skipped` counter advances on each unchanged page. Property-level updates (a page edited in Notion between exports) overwrite without versioning; the previous body is lost. A future task could write `:KnowledgeDocumentRevision` audit nodes if version history becomes a need.
|
|
56
|
+
|
|
57
|
+
## Failure modes
|
|
58
|
+
|
|
59
|
+
- `[notion-import:error] reason=front-matter-parse file=<path>` — front-matter is malformed YAML. Treat as no front-matter; write the page with body-only.
|
|
60
|
+
- `[notion-import:error] reason=mention-search-failed name=<name>` — `memory-search` raised. Log and continue; the mention is dropped (no fallback Person creation when the search itself failed, only when it returned no match).
|
|
61
|
+
- `[notion-import:error] reason=parent-not-found notionId=<id>` — the enclosing directory's `notionId` did not resolve to a `:KnowledgeDocument`. Anchor the orphan page to the `:NotionWorkspace` and log.
|
package/payload/platform/plugins/notion-import/skills/notion-import/references/workspace-export.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# workspace-export
|
|
2
|
+
|
|
3
|
+
Reference for walking a Notion "Export all workspace content" directory.
|
|
4
|
+
|
|
5
|
+
## What a Notion export looks like
|
|
6
|
+
|
|
7
|
+
Notion's native export ("Settings → Workspace → Export all workspace content" with "Everything", "Markdown & CSV" format) produces a zip. Unzipped, the top level is a single directory named after the workspace plus a Notion UUID; nested inside is the page tree as the operator structured it.
|
|
8
|
+
|
|
9
|
+
Two file shapes occur at every depth:
|
|
10
|
+
|
|
11
|
+
- **Pages** — `Page Title <32-hex-notionId>.md`. The basename is the human title followed by the 32-character hex Notion ID (no dashes). The file body is markdown.
|
|
12
|
+
- **Databases** — `Database Name <32-hex-notionId>.csv` paired with `Database Name <32-hex-notionId>/` directory. The CSV is the table view; the directory contains one `.md` per row plus an attachments tree.
|
|
13
|
+
|
|
14
|
+
Nested page subtrees: a page with children becomes a directory `Page Title <notionId>/` alongside its `.md` file; the directory holds the child pages and their own subdirectories recursively.
|
|
15
|
+
|
|
16
|
+
## What the walker does
|
|
17
|
+
|
|
18
|
+
1. **Read the root.** First-level entries are the top-level pages and databases of the workspace.
|
|
19
|
+
2. **Recurse depth-first** into each subdirectory, classifying entries on each pass.
|
|
20
|
+
3. **Build the candidate manifest** — a flat list of `{kind, path, notionId, parentNotionId, depth}` records, one per page or row. This is the manifest the selective-ingest gate runs against, and the manifest the write loop consumes.
|
|
21
|
+
4. **Apply the operator's filter** (top-level subtree, database name, last-edited date) by predicate-matching the manifest before any write begins.
|
|
22
|
+
5. **Hand off** the filtered manifest to [page-tree.md](page-tree.md) for pages and [databases.md](databases.md) for database tables and rows. Attachments are handed off as referenced from pages, not walked separately — see [attachments.md](attachments.md).
|
|
23
|
+
|
|
24
|
+
## Notion ID extraction
|
|
25
|
+
|
|
26
|
+
The Notion ID is the last 32 hex characters of the basename, after the trailing space. Strip the `.md` / `.csv` suffix and the trailing space, then take the last 32 chars; the remainder is the human title. Notion IDs are stable across exports; they are the natural key the skill MERGEs on.
|
|
27
|
+
|
|
28
|
+
## What the walker does NOT do
|
|
29
|
+
|
|
30
|
+
- **Does not** read page or row body content. That happens at write time, per the page/database references.
|
|
31
|
+
- **Does not** decide labels. Label selection happens in [databases.md](databases.md).
|
|
32
|
+
- **Does not** open attachments. Hashing happens at the attachment-copy step.
|
|
33
|
+
- **Does not** retry on partial exports. If a referenced child directory is missing, the parent page is still imported and the gap is logged.
|
|
34
|
+
|
|
35
|
+
## Failure modes
|
|
36
|
+
|
|
37
|
+
- `[notion-import:error] reason=basename-no-notion-id file=<path>` — basename does not end in a 32-hex Notion ID. Skip the file, continue the walk.
|
|
38
|
+
- `[notion-import:error] reason=orphan-row file=<path>` — a row markdown lives inside a directory whose parent CSV is missing. Skip; the row cannot be typed without the schema.
|
|
39
|
+
- `[notion-import:error] reason=zip-not-extracted path=<path>` — the operator pointed at a `.zip`. Unzip first; the skill does not call `unzip`.
|
|
40
|
+
|
|
41
|
+
The walker never aborts on a single file's failure. Counts of skipped files appear in the completion log.
|