secondbrain-py 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. brain/__init__.py +0 -0
  2. brain/__main__.py +18 -0
  3. brain/_capture_command.py +445 -0
  4. brain/_compose.py +52 -0
  5. brain/activity.py +206 -0
  6. brain/ask.py +631 -0
  7. brain/audio.py +591 -0
  8. brain/backfill/__init__.py +12 -0
  9. brain/backfill/search_extras.py +141 -0
  10. brain/backfill/source_rows.py +101 -0
  11. brain/bin/__init__.py +1 -0
  12. brain/bin/_launcher.py +107 -0
  13. brain/bin/down.py +8 -0
  14. brain/bin/launchd.py +268 -0
  15. brain/bin/monitor.py +570 -0
  16. brain/bin/rebuild.py +8 -0
  17. brain/bin/status.py +8 -0
  18. brain/bin/up.py +8 -0
  19. brain/brief.py +272 -0
  20. brain/capture.py +49 -0
  21. brain/chat.py +293 -0
  22. brain/cli.py +9760 -0
  23. brain/cli_claude.py +81 -0
  24. brain/cli_connect.py +285 -0
  25. brain/cli_demo.py +266 -0
  26. brain/config.py +1949 -0
  27. brain/connect.py +925 -0
  28. brain/db.py +540 -0
  29. brain/demo/__init__.py +452 -0
  30. brain/demo/corpus/manifest.json +403 -0
  31. brain/demo/embedder.py +74 -0
  32. brain/durations.py +84 -0
  33. brain/edit_session.py +156 -0
  34. brain/editor.py +67 -0
  35. brain/elicit/__init__.py +16 -0
  36. brain/elicit/detectors.py +250 -0
  37. brain/elicit/drafter.py +70 -0
  38. brain/elicit/queue.py +220 -0
  39. brain/elicit/schema.py +48 -0
  40. brain/elicit/session.py +445 -0
  41. brain/embedding_targets.py +54 -0
  42. brain/embeddings.py +424 -0
  43. brain/enrichment.py +808 -0
  44. brain/errors.py +357 -0
  45. brain/eval/__init__.py +129 -0
  46. brain/eval/answer_eval.py +281 -0
  47. brain/eval/baseline.py +265 -0
  48. brain/eval/concept_extraction.py +378 -0
  49. brain/eval/corpus.py +152 -0
  50. brain/eval/errors.py +19 -0
  51. brain/eval/graph_baseline.py +226 -0
  52. brain/eval/graph_retrieval.py +202 -0
  53. brain/eval/graph_runner.py +319 -0
  54. brain/eval/metrics.py +101 -0
  55. brain/eval/runner.py +223 -0
  56. brain/format.py +783 -0
  57. brain/gaps.py +390 -0
  58. brain/graph_rag/__init__.py +94 -0
  59. brain/graph_rag/_retrieval_common.py +113 -0
  60. brain/graph_rag/aggregates.py +303 -0
  61. brain/graph_rag/aliases/__init__.py +583 -0
  62. brain/graph_rag/backends/__init__.py +10 -0
  63. brain/graph_rag/backends/_age_helpers.py +473 -0
  64. brain/graph_rag/backends/age.py +782 -0
  65. brain/graph_rag/backends/base.py +272 -0
  66. brain/graph_rag/build.py +344 -0
  67. brain/graph_rag/communities.py +644 -0
  68. brain/graph_rag/communities_summary.py +437 -0
  69. brain/graph_rag/concepts.py +202 -0
  70. brain/graph_rag/cooccur.py +193 -0
  71. brain/graph_rag/cross_type.py +312 -0
  72. brain/graph_rag/extract.py +885 -0
  73. brain/graph_rag/fuse.py +371 -0
  74. brain/graph_rag/global_.py +412 -0
  75. brain/graph_rag/grouping.py +372 -0
  76. brain/graph_rag/person_resolver.py +167 -0
  77. brain/graph_rag/reconcile.py +792 -0
  78. brain/graph_rag/relational.py +353 -0
  79. brain/graph_rag/retrieve.py +526 -0
  80. brain/graph_rag/router.py +288 -0
  81. brain/graph_rag/schema.py +320 -0
  82. brain/graph_rag/sync.py +237 -0
  83. brain/graph_rag/tenancy.py +43 -0
  84. brain/graph_rag/themes.py +501 -0
  85. brain/graph_rag/weighting.py +202 -0
  86. brain/ingest/__init__.py +1926 -0
  87. brain/ingest/chunker.py +249 -0
  88. brain/ingest/docx.py +40 -0
  89. brain/ingest/gmail.py +621 -0
  90. brain/ingest/markdown.py +37 -0
  91. brain/ingest/pdf.py +61 -0
  92. brain/ingest/stdin.py +22 -0
  93. brain/ingest/sub_tokens.py +91 -0
  94. brain/ingest/text.py +16 -0
  95. brain/interactions.py +205 -0
  96. brain/maintenance.py +355 -0
  97. brain/mcp_server.py +3405 -0
  98. brain/migrations/001_init.sql +43 -0
  99. brain/migrations/002_qwen3_embedding.sql +17 -0
  100. brain/migrations/003_vault_model.sql +41 -0
  101. brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
  102. brain/migrations/005_derived_links.sql +67 -0
  103. brain/migrations/006_dedup_file_by_source_path.sql +25 -0
  104. brain/migrations/007_email_thread_and_draft.sql +15 -0
  105. brain/migrations/008_gmail_thread_unique.sql +11 -0
  106. brain/migrations/009_chunks_weighted_tsv.sql +28 -0
  107. brain/migrations/010_interactions.sql +30 -0
  108. brain/migrations/011_documents_summary.sql +23 -0
  109. brain/migrations/012_graphrag.sql +171 -0
  110. brain/migrations/013_graphrag_communities.sql +125 -0
  111. brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
  112. brain/migrations/015_interactions_graph_targets.sql +89 -0
  113. brain/migrations/016_index_hygiene.sql +61 -0
  114. brain/migrations/017_elicit.sql +30 -0
  115. brain/migrations/018_review_gap_signal_kinds.sql +40 -0
  116. brain/migrations/019_search_queries.sql +35 -0
  117. brain/migrations/020_link_suggestions.sql +40 -0
  118. brain/migrations/021_timeline_doc_date.sql +34 -0
  119. brain/migrations/022_link_suggestions_undirected.sql +84 -0
  120. brain/migrations/023_search_queries_fts_count.sql +28 -0
  121. brain/quartz_overrides/__init__.py +8 -0
  122. brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
  123. brain/quartz_overrides/quartz/build.ts +568 -0
  124. brain/quartz_overrides/quartz/cli/args.js +152 -0
  125. brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
  126. brain/quartz_overrides/quartz/cli/handlers.js +636 -0
  127. brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
  128. brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
  129. brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
  130. brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
  131. brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
  132. brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
  133. brain/quartz_overrides/quartz/components/Search.tsx +161 -0
  134. brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
  135. brain/quartz_overrides/quartz/components/index.ts +92 -0
  136. brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
  137. brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
  138. brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
  139. brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
  140. brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
  141. brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
  142. brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
  143. brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
  144. brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
  145. brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
  146. brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
  147. brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
  148. brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
  149. brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
  150. brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
  151. brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
  152. brain/quartz_overrides/quartz/processors/parse.ts +371 -0
  153. brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
  154. brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
  155. brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
  156. brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
  157. brain/quartz_overrides/quartz/static/emailThread.js +334 -0
  158. brain/quartz_overrides/quartz/static/favicon.ico +0 -0
  159. brain/quartz_overrides/quartz/static/icon.png +0 -0
  160. brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
  161. brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
  162. brain/quartz_overrides/quartz/static/reload.js +168 -0
  163. brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
  164. brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
  165. brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
  166. brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
  167. brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
  168. brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
  169. brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
  170. brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
  171. brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
  172. brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
  173. brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
  174. brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
  175. brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
  176. brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
  177. brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
  178. brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
  179. brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
  180. brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
  181. brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
  182. brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
  183. brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
  184. brain/quartz_overrides/quartz/styles/custom.scss +89 -0
  185. brain/quartz_overrides/quartz/styles/graph.scss +505 -0
  186. brain/quartz_overrides/quartz/util/ctx.ts +92 -0
  187. brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
  188. brain/quartz_overrides/quartz/util/path.ts +358 -0
  189. brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
  190. brain/quartz_overrides/quartz.config.ts +270 -0
  191. brain/quartz_overrides/quartz.layout.ts +314 -0
  192. brain/queries.py +1188 -0
  193. brain/rank_fusion.py +8 -0
  194. brain/resurface.py +210 -0
  195. brain/review/__init__.py +26 -0
  196. brain/review/emit.py +27 -0
  197. brain/review/queries.py +436 -0
  198. brain/review/render.py +196 -0
  199. brain/review/scans.py +355 -0
  200. brain/review/weekly.py +413 -0
  201. brain/search.py +704 -0
  202. brain/set_similarity.py +15 -0
  203. brain/setup.py +1205 -0
  204. brain/tags.py +56 -0
  205. brain/templates/Caddyfile.j2 +9 -0
  206. brain/templates/__init__.py +1 -0
  207. brain/templates/bin/__init__.py +1 -0
  208. brain/templates/bin/_brain-brief-fg.sh +25 -0
  209. brain/templates/bin/_brain-build-fg.sh +53 -0
  210. brain/templates/bin/_brain-watcher-fg.sh +65 -0
  211. brain/templates/bin/brain-down.sh +89 -0
  212. brain/templates/bin/brain-status.sh +83 -0
  213. brain/templates/bin/brain-up.sh +221 -0
  214. brain/templates/docker/age/Dockerfile +79 -0
  215. brain/templates/docker-compose.stock.yml.j2 +26 -0
  216. brain/templates/docker-compose.yml.j2 +34 -0
  217. brain/templates/env.example +190 -0
  218. brain/templates/launchd/__init__.py +1 -0
  219. brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
  220. brain/templates/launchd/com.brain.build.plist.j2 +46 -0
  221. brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
  222. brain/templates/skill/SKILL.md +63 -0
  223. brain/templates/skill/__init__.py +1 -0
  224. brain/timeline.py +834 -0
  225. brain/todo.py +124 -0
  226. brain/uninstall.py +185 -0
  227. brain/vault/__init__.py +115 -0
  228. brain/vault/_atomic.py +25 -0
  229. brain/vault/daily_index.py +228 -0
  230. brain/vault/derived_links/__init__.py +50 -0
  231. brain/vault/derived_links/directory.py +683 -0
  232. brain/vault/derived_links/fence.py +408 -0
  233. brain/vault/derived_links/gws.py +64 -0
  234. brain/vault/derived_links/participants.py +143 -0
  235. brain/vault/derived_links/pass_runner.py +362 -0
  236. brain/vault/derived_links/rules.py +137 -0
  237. brain/vault/export.py +683 -0
  238. brain/vault/frontmatter.py +165 -0
  239. brain/vault/graph.py +620 -0
  240. brain/vault/graph_format.py +388 -0
  241. brain/vault/link_rewrite.py +235 -0
  242. brain/vault/links.py +260 -0
  243. brain/vault/note_builder.py +211 -0
  244. brain/vault/paths.py +55 -0
  245. brain/vault/quartz_overlay.py +236 -0
  246. brain/vault/rename.py +591 -0
  247. brain/vault/resolver.py +304 -0
  248. brain/vault/slug.py +127 -0
  249. brain/vault/sync.py +1513 -0
  250. brain/vault/sync_summaries.py +264 -0
  251. brain/vault/templates.py +145 -0
  252. brain/vault/watch.py +1052 -0
  253. brain/wiki/__init__.py +6 -0
  254. brain/wiki/_github_slugger.py +76 -0
  255. brain/wiki/_person_name.py +314 -0
  256. brain/wiki/build_homepage.py +541 -0
  257. brain/wiki/build_partial.py +273 -0
  258. brain/wiki/build_people.py +934 -0
  259. brain/wiki/build_related.py +758 -0
  260. brain/wiki/build_swap.py +585 -0
  261. brain/wiki/build_watcher.py +975 -0
  262. brain/wiki/edit_classifier.py +215 -0
  263. brain/wiki/errors.py +10 -0
  264. brain/wiki/fastpath_manifest.py +475 -0
  265. brain/wiki/fastpath_state.py +174 -0
  266. brain/wiki/install.py +296 -0
  267. brain/wiki/slug.py +111 -0
  268. secondbrain_py-0.2.1.dist-info/METADATA +195 -0
  269. secondbrain_py-0.2.1.dist-info/RECORD +273 -0
  270. secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
  271. secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
  272. secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
  273. secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,568 @@
1
+ // Brain wiki overlay — quartz/build.ts
2
+ //
3
+ // This file shadows the upstream ``~/brain-vault/.quartz/quartz/build.ts``.
4
+ // All upstream functionality is preserved verbatim. The only addition is the
5
+ // ``writeFastpathArtifacts`` hook at the end of ``buildQuartz``, which writes
6
+ // ``manifest.json`` + ``contentmap.json`` under
7
+ // ``<vault>/.quartz/.cache/fastpath/`` after a successful full build.
8
+ //
9
+ // T2 additions (Plan B v3 — per-file emit fast path):
10
+ // - ``_deriveOutputPath(slug)`` — uniform slug→html output path rule.
11
+ // - ``writeFastpathArtifacts(ctx, filteredContent)`` — writes manifest +
12
+ // contentmap (atomic: write tmp → rename). Called after ``emitContent``
13
+ // succeeds, wrapped in try/catch so artifact failure NEVER crashes the build.
14
+ // - ``QUARTZ_PARENT_BUILD_ID`` env var (Strategy A from T0): Python's
15
+ // ``build_swap`` passes this; the overlay reads it and stamps both artifacts.
16
+ // If the env var is absent, artifact write is skipped (non-fatal warning only).
17
+ //
18
+ // Spec: docs/plans/2026-05-09-plan-b-per-file-emit.md (T2 section)
19
+ // Companion: quartz_overrides/quartz/util/fastpath_manifest.ts
20
+
21
+ import sourceMapSupport from "source-map-support"
22
+ sourceMapSupport.install(options)
23
+ import path from "path"
24
+ import { PerfTimer } from "./util/perf"
25
+ import { rm } from "fs/promises"
26
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs"
27
+ import { randomUUID } from "node:crypto"
28
+ import { GlobbyFilterFunction, isGitIgnored } from "globby"
29
+ import { styleText } from "util"
30
+ import { parseMarkdown } from "./processors/parse"
31
+ import { filterContent } from "./processors/filter"
32
+ import { emitContent } from "./processors/emit"
33
+ import cfg from "../quartz.config"
34
+ import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
35
+ import chokidar from "chokidar"
36
+ import { ProcessedContent } from "./plugins/vfile"
37
+ import { Argv, BuildCtx } from "./util/ctx"
38
+ import { glob, toPosixPath } from "./util/glob"
39
+ import { trace } from "./util/trace"
40
+ import { options } from "./util/sourcemap"
41
+ import { Mutex } from "async-mutex"
42
+ import { getStaticResourcesFromPlugins } from "./plugins"
43
+ import { randomIdNonSecure } from "./util/random"
44
+ import { ChangeEvent } from "./plugins/types"
45
+ import { minimatch } from "minimatch"
46
+ import {
47
+ _atomicWriteJson,
48
+ computeFingerprint,
49
+ writeManifest,
50
+ FINGERPRINT_VERSION,
51
+ } from "./util/fastpath_manifest"
52
+ import type { Manifest, SlugEntry } from "./util/fastpath_manifest"
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Shared types (same as upstream)
56
+ // ---------------------------------------------------------------------------
57
+
58
+ type ContentMap = Map<
59
+ FilePath,
60
+ | {
61
+ type: "markdown"
62
+ content: ProcessedContent
63
+ }
64
+ | {
65
+ type: "other"
66
+ }
67
+ >
68
+
69
+ type BuildData = {
70
+ ctx: BuildCtx
71
+ ignored: GlobbyFilterFunction
72
+ mut: Mutex
73
+ contentMap: ContentMap
74
+ changesSinceLastBuild: Record<FilePath, ChangeEvent["type"]>
75
+ lastBuildMs: number
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // T2 additions — fastpath artifact helpers
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /**
83
+ * Derive the HTML output path for a Quartz slug.
84
+ *
85
+ * All Quartz pages use ``slug + ".html"`` as their output path, confirmed by
86
+ * ``plugins/emitters/helpers.ts:write()`` which calls
87
+ * ``joinSegments(ctx.argv.output, slug + ext)`` where ``ext = ".html"``.
88
+ *
89
+ * Examples:
90
+ * - ``"index"`` → ``"index.html"`` (root page)
91
+ * - ``"notes/my-doc"`` → ``"notes/my-doc.html"`` (regular page)
92
+ * - ``"folder/index"`` → ``"folder/index.html"`` (folder index)
93
+ * - ``"tags/foo"`` → ``"tags/foo.html"`` (tag page)
94
+ */
95
+ function _deriveOutputPath(slug: string): string {
96
+ return slug + ".html"
97
+ }
98
+
99
+ /**
100
+ * Serialise a Date instance (or string/null) to an ISO string or null.
101
+ *
102
+ * Mirrors the rehydration done by ``parse.ts:rehydrateDates`` in reverse:
103
+ * Date → string so the contentmap is fully JSON-safe.
104
+ */
105
+ function _serializeDate(v: unknown): string | null {
106
+ if (v instanceof Date) return v.toISOString()
107
+ if (typeof v === "string" && v) return v
108
+ return null
109
+ }
110
+
111
+ /**
112
+ * Write fastpath artifacts (manifest.json + contentmap.json) after a
113
+ * successful full build.
114
+ *
115
+ * Reads ``QUARTZ_PARENT_BUILD_ID`` env var (Strategy A from T0). If the
116
+ * var is absent or empty, logs a warning and returns without writing —
117
+ * the build itself is unaffected; the next edit will trigger a full build.
118
+ *
119
+ * Both files are written atomically (write-tmp + renameSync).
120
+ *
121
+ * @param ctx The build context (provides argv.directory for vault path).
122
+ * @param filteredContent The filtered markdown content from the full build.
123
+ */
124
+ async function writeFastpathArtifacts(
125
+ ctx: BuildCtx,
126
+ filteredContent: ProcessedContent[],
127
+ ): Promise<void> {
128
+ const parentBuildId = process.env["QUARTZ_PARENT_BUILD_ID"] ?? ""
129
+ if (!parentBuildId) {
130
+ console.warn(
131
+ "wiki: QUARTZ_PARENT_BUILD_ID not set — skipping fastpath artifact write " +
132
+ "(next edit will trigger a full build)",
133
+ )
134
+ return
135
+ }
136
+
137
+ const { argv } = ctx
138
+
139
+ // Fastpath dir lives under the vault's .quartz directory (NOT the build dir).
140
+ // argv.directory = vault root; argv.output = build output dir.
141
+ // Spec: docs/audits/2026-05-09-fastpath-t0.md F5.3.
142
+ const fastpathDir = path.join(argv.directory, ".quartz", ".cache", "fastpath")
143
+ mkdirSync(fastpathDir, { recursive: true })
144
+
145
+ // -------------------------------------------------------------------------
146
+ // 1. Build manifest.json — per-slug fingerprint + output/source paths.
147
+ // -------------------------------------------------------------------------
148
+
149
+ const slugEntries: Record<string, SlugEntry> = {}
150
+ for (const pc of filteredContent) {
151
+ const [, vfile] = pc
152
+ const slug = vfile.data.slug!
153
+ const sourcePath = String(vfile.data.relativePath ?? "")
154
+ const outputPath = _deriveOutputPath(slug)
155
+ slugEntries[slug] = {
156
+ fingerprint: computeFingerprint(pc, { sourcePath, outputPath }),
157
+ output_path: outputPath,
158
+ source_path: sourcePath,
159
+ }
160
+ }
161
+
162
+ const manifest: Manifest = {
163
+ version: FINGERPRINT_VERSION,
164
+ parent_build_id: parentBuildId,
165
+ built_at_ms: Date.now(),
166
+ slugs: slugEntries,
167
+ }
168
+ // manifest.json is written LAST (after contentmap.json) so it acts as a
169
+ // commit marker — see write-order comment below.
170
+
171
+ // -------------------------------------------------------------------------
172
+ // 2. Build contentmap.json — metadata-only (hastRoot = null, htmlAst excluded).
173
+ //
174
+ // Per T0 Amendment 1: full HAST trees are 273 MB on a 1100-doc vault.
175
+ // Metadata-only (no hastRoot, no htmlAst) drops this to ~1.3 MB and
176
+ // round-trips in <5 ms. The fast path re-parses the changed file's HAST
177
+ // from source; unchanged files don't need HAST because the emitter only
178
+ // accesses ``vfile.data`` (frontmatter, links, blocks, dates, etc.).
179
+ //
180
+ // blocks MUST be included: block-ref transclusions are resolved via
181
+ // ``allFiles[target].data.blocks[blockId]`` at render time (M3 confirmed).
182
+ // -------------------------------------------------------------------------
183
+
184
+ type SerializedDates = {
185
+ created: string | null
186
+ modified: string | null
187
+ published: string | null
188
+ }
189
+
190
+ type ContentMapVFileData = {
191
+ frontmatter: Record<string, unknown> | null
192
+ links: string[] | null
193
+ text: string | null
194
+ blocks: Record<string, unknown>
195
+ dates: SerializedDates
196
+ filePath: string
197
+ relativePath: string
198
+ slug: string
199
+ // Extended metadata (Fix #3 — T3/T4 consumers need these without re-parsing).
200
+ description: string | null
201
+ toc: unknown
202
+ collapseToc: boolean | null
203
+ aliases: string[] | null
204
+ hasMermaidDiagram: boolean | null
205
+ }
206
+
207
+ type ContentMapEntry = {
208
+ type: "markdown"
209
+ filePath: string
210
+ hastRoot: null
211
+ vfileData: ContentMapVFileData
212
+ }
213
+
214
+ type ContentmapEnvelope = {
215
+ /** Must equal FINGERPRINT_VERSION — lets T3/T4 detect stale contentmap. */
216
+ version: number
217
+ /** Matches manifest.parent_build_id — cross-artifact consistency check. */
218
+ parent_build_id: string
219
+ /** Unix epoch milliseconds when this contentmap was written. */
220
+ built_at_ms: number
221
+ /** Per-file content metadata entries (no hastRoot, no htmlAst). */
222
+ entries: ContentMapEntry[]
223
+ }
224
+
225
+ const contentMapEntries: ContentMapEntry[] = []
226
+ for (const [, vfile] of filteredContent) {
227
+ const data = vfile.data as Record<string, unknown>
228
+
229
+ const rawDates = data["dates"] as Record<string, unknown> | undefined
230
+ const serializedDates: SerializedDates = {
231
+ created: _serializeDate(rawDates?.["created"]),
232
+ modified: _serializeDate(rawDates?.["modified"]),
233
+ published: _serializeDate(rawDates?.["published"]),
234
+ }
235
+
236
+ // blocks: Record<string, Element> — HAST Elements are plain objects, JSON-safe.
237
+ // Include even though hastRoot is null (block-ref transclusion requirement, M3).
238
+ const blocks = (data["blocks"] as Record<string, unknown> | undefined) ?? {}
239
+
240
+ contentMapEntries.push({
241
+ type: "markdown",
242
+ filePath: String(vfile.data.relativePath ?? ""),
243
+ hastRoot: null,
244
+ vfileData: {
245
+ frontmatter: (data["frontmatter"] as Record<string, unknown> | undefined) ?? null,
246
+ links: (data["links"] as string[] | undefined) ?? null,
247
+ text: (data["text"] as string | undefined) ?? null,
248
+ blocks,
249
+ dates: serializedDates,
250
+ filePath: String(data["filePath"] ?? ""),
251
+ relativePath: String(data["relativePath"] ?? ""),
252
+ slug: String(data["slug"] ?? ""),
253
+ // Extended metadata — T3/T4 consumers read these from the contentmap
254
+ // instead of re-parsing the source file.
255
+ description: typeof data["description"] === "string" ? data["description"] : null,
256
+ toc: data["toc"] ?? null,
257
+ collapseToc: typeof data["collapseToc"] === "boolean" ? data["collapseToc"] : null,
258
+ aliases: Array.isArray(data["aliases"])
259
+ ? (data["aliases"] as unknown[]).map(String)
260
+ : null,
261
+ hasMermaidDiagram:
262
+ typeof data["hasMermaidDiagram"] === "boolean" ? data["hasMermaidDiagram"] : null,
263
+ },
264
+ })
265
+ }
266
+
267
+ // -------------------------------------------------------------------------
268
+ // Write order: contentmap FIRST (as content), manifest LAST (as commit marker).
269
+ // T3/T4 must observe: if manifest.json exists, contentmap.json is guaranteed
270
+ // to already be present and consistent. Writing manifest last enforces this
271
+ // invariant atomically.
272
+ // -------------------------------------------------------------------------
273
+
274
+ const contentmapEnvelope: ContentmapEnvelope = {
275
+ version: FINGERPRINT_VERSION,
276
+ parent_build_id: parentBuildId,
277
+ built_at_ms: Date.now(),
278
+ entries: contentMapEntries,
279
+ }
280
+ // contentmap written FIRST — content is available before the commit marker.
281
+ _atomicWriteJson(fastpathDir, "contentmap.json", contentmapEnvelope)
282
+
283
+ // manifest written LAST — acts as the commit marker (T3/T4 invariant above).
284
+ writeManifest(fastpathDir, manifest)
285
+
286
+ console.log(
287
+ `wiki: fastpath manifest + contentmap written ` +
288
+ `(parent_build_id=${parentBuildId}, slugs=${filteredContent.length})`,
289
+ )
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Upstream buildQuartz (with T2 hook added after emitContent)
294
+ // ---------------------------------------------------------------------------
295
+
296
+ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
297
+ const ctx: BuildCtx = {
298
+ buildId: randomIdNonSecure(),
299
+ argv,
300
+ cfg,
301
+ allSlugs: [],
302
+ allFiles: [],
303
+ incremental: false,
304
+ }
305
+
306
+ const perf = new PerfTimer()
307
+ const output = argv.output
308
+
309
+ const pluginCount = Object.values(cfg.plugins).flat().length
310
+ const pluginNames = (key: "transformers" | "filters" | "emitters") =>
311
+ cfg.plugins[key].map((plugin) => plugin.name)
312
+ if (argv.verbose) {
313
+ console.log(`Loaded ${pluginCount} plugins`)
314
+ console.log(` Transformers: ${pluginNames("transformers").join(", ")}`)
315
+ console.log(` Filters: ${pluginNames("filters").join(", ")}`)
316
+ console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
317
+ }
318
+
319
+ const release = await mut.acquire()
320
+ perf.addEvent("clean")
321
+ await rm(output, { recursive: true, force: true })
322
+ console.log(`Cleaned output directory \`${output}\` in ${perf.timeSince("clean")}`)
323
+
324
+ perf.addEvent("glob")
325
+ const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
326
+ const markdownPaths = allFiles.filter((fp) => fp.endsWith(".md")).sort()
327
+ console.log(
328
+ `Found ${markdownPaths.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
329
+ )
330
+
331
+ const filePaths = markdownPaths.map((fp) => joinSegments(argv.directory, fp) as FilePath)
332
+ ctx.allFiles = allFiles
333
+ ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
334
+
335
+ const parsedFiles = await parseMarkdown(ctx, filePaths)
336
+ const filteredContent = filterContent(ctx, parsedFiles)
337
+
338
+ await emitContent(ctx, filteredContent)
339
+ console.log(
340
+ styleText("green", `Done processing ${markdownPaths.length} files in ${perf.timeSince()}`),
341
+ )
342
+
343
+ // brain T2: write fastpath artifacts (manifest.json + contentmap.json).
344
+ // Non-fatal: any failure logs a warning but does NOT abort the build or swap.
345
+ // QUARTZ_PARENT_BUILD_ID (Strategy A) must be set by Python's build_swap
346
+ // before invoking node; if absent, artifact write is silently skipped.
347
+ try {
348
+ await writeFastpathArtifacts(ctx, filteredContent)
349
+ } catch (err) {
350
+ console.warn("wiki: fastpath artifact write failed (non-fatal):", err)
351
+ }
352
+
353
+ release()
354
+
355
+ if (argv.watch) {
356
+ ctx.incremental = true
357
+ return startWatching(ctx, mut, parsedFiles, clientRefresh)
358
+ }
359
+ }
360
+
361
+ // setup watcher for rebuilds
362
+ async function startWatching(
363
+ ctx: BuildCtx,
364
+ mut: Mutex,
365
+ initialContent: ProcessedContent[],
366
+ clientRefresh: () => void,
367
+ ) {
368
+ const { argv, allFiles } = ctx
369
+
370
+ const contentMap: ContentMap = new Map()
371
+ for (const filePath of allFiles) {
372
+ contentMap.set(filePath, {
373
+ type: "other",
374
+ })
375
+ }
376
+
377
+ for (const content of initialContent) {
378
+ const [_tree, vfile] = content
379
+ contentMap.set(vfile.data.relativePath!, {
380
+ type: "markdown",
381
+ content,
382
+ })
383
+ }
384
+
385
+ const gitIgnoredMatcher = await isGitIgnored()
386
+ const buildData: BuildData = {
387
+ ctx,
388
+ mut,
389
+ contentMap,
390
+ ignored: (fp) => {
391
+ const pathStr = toPosixPath(fp.toString())
392
+ if (pathStr.startsWith(".git/")) return true
393
+ if (gitIgnoredMatcher(pathStr)) return true
394
+ for (const pattern of cfg.configuration.ignorePatterns) {
395
+ if (minimatch(pathStr, pattern)) {
396
+ return true
397
+ }
398
+ }
399
+
400
+ return false
401
+ },
402
+
403
+ changesSinceLastBuild: {},
404
+ lastBuildMs: 0,
405
+ }
406
+
407
+ const watcher = chokidar.watch(".", {
408
+ awaitWriteFinish: { stabilityThreshold: 250 },
409
+ persistent: true,
410
+ cwd: argv.directory,
411
+ ignoreInitial: true,
412
+ })
413
+
414
+ const changes: ChangeEvent[] = []
415
+ watcher
416
+ .on("add", (fp) => {
417
+ fp = toPosixPath(fp)
418
+ if (buildData.ignored(fp)) return
419
+ changes.push({ path: fp as FilePath, type: "add" })
420
+ void rebuild(changes, clientRefresh, buildData)
421
+ })
422
+ .on("change", (fp) => {
423
+ fp = toPosixPath(fp)
424
+ if (buildData.ignored(fp)) return
425
+ changes.push({ path: fp as FilePath, type: "change" })
426
+ void rebuild(changes, clientRefresh, buildData)
427
+ })
428
+ .on("unlink", (fp) => {
429
+ fp = toPosixPath(fp)
430
+ if (buildData.ignored(fp)) return
431
+ changes.push({ path: fp as FilePath, type: "delete" })
432
+ void rebuild(changes, clientRefresh, buildData)
433
+ })
434
+
435
+ return async () => {
436
+ await watcher.close()
437
+ }
438
+ }
439
+
440
+ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildData: BuildData) {
441
+ const { ctx, contentMap, mut, changesSinceLastBuild } = buildData
442
+ const { argv, cfg } = ctx
443
+
444
+ const buildId = randomIdNonSecure()
445
+ ctx.buildId = buildId
446
+ buildData.lastBuildMs = new Date().getTime()
447
+ const numChangesInBuild = changes.length
448
+ const release = await mut.acquire()
449
+
450
+ // if there's another build after us, release and let them do it
451
+ if (ctx.buildId !== buildId) {
452
+ release()
453
+ return
454
+ }
455
+
456
+ const perf = new PerfTimer()
457
+ perf.addEvent("rebuild")
458
+ console.log(styleText("yellow", "Detected change, rebuilding..."))
459
+
460
+ // update changesSinceLastBuild
461
+ for (const change of changes) {
462
+ changesSinceLastBuild[change.path] = change.type
463
+ }
464
+
465
+ const staticResources = getStaticResourcesFromPlugins(ctx)
466
+ const pathsToParse: FilePath[] = []
467
+ for (const [fp, type] of Object.entries(changesSinceLastBuild)) {
468
+ if (type === "delete" || path.extname(fp) !== ".md") continue
469
+ const fullPath = joinSegments(argv.directory, toPosixPath(fp)) as FilePath
470
+ pathsToParse.push(fullPath)
471
+ }
472
+
473
+ const parsed = await parseMarkdown(ctx, pathsToParse)
474
+ for (const content of parsed) {
475
+ contentMap.set(content[1].data.relativePath!, {
476
+ type: "markdown",
477
+ content,
478
+ })
479
+ }
480
+
481
+ // update state using changesSinceLastBuild
482
+ // we do this weird play of add => compute change events => remove
483
+ // so that partialEmitters can do appropriate cleanup based on the content of deleted files
484
+ for (const [file, change] of Object.entries(changesSinceLastBuild)) {
485
+ if (change === "delete") {
486
+ // universal delete case
487
+ contentMap.delete(file as FilePath)
488
+ }
489
+
490
+ // manually track non-markdown files as processed files only
491
+ // contains markdown files
492
+ if (change === "add" && path.extname(file) !== ".md") {
493
+ contentMap.set(file as FilePath, {
494
+ type: "other",
495
+ })
496
+ }
497
+ }
498
+
499
+ const changeEvents: ChangeEvent[] = Object.entries(changesSinceLastBuild).map(([fp, type]) => {
500
+ const path = fp as FilePath
501
+ const processedContent = contentMap.get(path)
502
+ if (processedContent?.type === "markdown") {
503
+ const [_tree, file] = processedContent.content
504
+ return {
505
+ type,
506
+ path,
507
+ file,
508
+ }
509
+ }
510
+
511
+ return {
512
+ type,
513
+ path,
514
+ }
515
+ })
516
+
517
+ // update allFiles and then allSlugs with the consistent view of content map
518
+ ctx.allFiles = Array.from(contentMap.keys())
519
+ ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
520
+ let processedFiles = filterContent(
521
+ ctx,
522
+ Array.from(contentMap.values())
523
+ .filter((file) => file.type === "markdown")
524
+ .map((file) => file.content),
525
+ )
526
+
527
+ let emittedFiles = 0
528
+ for (const emitter of cfg.plugins.emitters) {
529
+ // Try to use partialEmit if available, otherwise assume the output is static
530
+ const emitFn = emitter.partialEmit ?? emitter.emit
531
+ const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents)
532
+ if (emitted === null) {
533
+ continue
534
+ }
535
+
536
+ if (Symbol.asyncIterator in emitted) {
537
+ // Async generator case
538
+ for await (const file of emitted) {
539
+ emittedFiles++
540
+ if (ctx.argv.verbose) {
541
+ console.log(`[emit:${emitter.name}] ${file}`)
542
+ }
543
+ }
544
+ } else {
545
+ // Array case
546
+ emittedFiles += emitted.length
547
+ if (ctx.argv.verbose) {
548
+ for (const file of emitted) {
549
+ console.log(`[emit:${emitter.name}] ${file}`)
550
+ }
551
+ }
552
+ }
553
+ }
554
+
555
+ console.log(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince("rebuild")}`)
556
+ console.log(styleText("green", `Done rebuilding in ${perf.timeSince()}`))
557
+ changes.splice(0, numChangesInBuild)
558
+ clientRefresh()
559
+ release()
560
+ }
561
+
562
+ export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {
563
+ try {
564
+ return await buildQuartz(argv, mut, clientRefresh)
565
+ } catch (err) {
566
+ trace("\nExiting Quartz due to a fatal error", err as Error)
567
+ }
568
+ }