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,544 @@
1
+ // Brain wiki — partial-build handler for the ``build-partial`` CLI subcommand.
2
+ //
3
+ // This file is a TEMPLATE. It is installed at
4
+ // ``<vault>/.quartz/quartz/cli/build_partial_handler.js`` by
5
+ // ``brain vault render --overlay``.
6
+ //
7
+ // Algorithm (per docs/plans/2026-05-09-plan-b-per-file-emit.md T4 section +
8
+ // Codex pre-flight contract):
9
+ //
10
+ // 1. Parse --slug from argv (required).
11
+ // 2. Compute fastpathDir = <argv.directory>/.quartz/.cache/fastpath/.
12
+ // 3. Load manifest.json (exit 1 if missing/unparseable).
13
+ // 4. Load contentmap.json (exit 1 if missing/unparseable).
14
+ // 5. Envelope cross-check — enforce BOTH:
15
+ // manifest.version === contentmap.version
16
+ // manifest.parent_build_id === contentmap.parent_build_id
17
+ // Mismatch → exit 2, stderr "envelope mismatch: manifest=<v>/<id> contentmap=<v>/<id>".
18
+ // 6. Look up slugEntry = manifest.slugs[slug]. Absent → exit 3.
19
+ // 6b. Unsupported-slug guard: slugs starting with "tags/", ending with "/index",
20
+ // or equal to "index" cannot be partially built — ContentPage.partialEmit skips
21
+ // them (contentPage.tsx:112-117) and TagPage/FolderPage are in the deny-list.
22
+ // → exit 6, stderr "scope: full build required for slug=<slug>".
23
+ // 7. Find contentEntry in contentmap.entries. Absent → exit 4.
24
+ // 8. Compile + run the TypeScript executor (_PARTIAL_BUILD_TS via esbuild stdin).
25
+ // The executor uses the Quartz parse + emit machinery to re-parse the changed
26
+ // file and run eligible emitters (skipping ContentIndex — Option C exclusion).
27
+ // Emitter exception → exit 5 (written inside the executor TS).
28
+ // 9. Atomic write order: contentmap.json FIRST, manifest.json SECOND, .build-id LAST.
29
+ // This mirrors the T2 commit-marker pattern (manifest is the commit marker).
30
+ // 10. Print success line: "wiki: build-partial slug=<slug> elapsed=<ms>".
31
+ //
32
+ // Exit codes:
33
+ // 1 — manifest.json or contentmap.json missing/unparseable; --slug missing.
34
+ // 2 — envelope mismatch (version or parent_build_id differs).
35
+ // 3 — slug absent from manifest.slugs.
36
+ // 4 — slug absent from contentmap.entries.
37
+ // 5 — emitter exception (fail-fast; no artifact written).
38
+ // 6 — unsupported-slug scope violation (tag/folder/index pages need full build).
39
+ //
40
+ // Emitter exclusion (Option C from T0 benchmark M2):
41
+ // ContentIndex, TagPage, and FolderPage are excluded from the partial emitter walk.
42
+ // They are tracked in _PARTIAL_EMIT_EXCLUDED (a Set) inside the executor.
43
+ // No .emit call is made for any excluded emitter.
44
+ //
45
+ // _atomicWriteJson is defined here in plain JS (mirrors the function in
46
+ // util/fastpath_manifest.ts) for the artifact writes performed directly by this handler.
47
+ //
48
+ // The TypeScript executor (_PARTIAL_BUILD_TS) imports from ../util/fastpath_manifest
49
+ // (the T1+T2 helper) for computeFingerprint and FINGERPRINT_VERSION.
50
+
51
+ import { readFileSync, writeFileSync, mkdirSync, renameSync, statSync } from "node:fs"
52
+ import { promises as fsPromises } from "node:fs"
53
+ import { join } from "node:path"
54
+ import { randomUUID } from "node:crypto"
55
+ import path from "path"
56
+ import esbuild from "esbuild"
57
+ import { sassPlugin } from "esbuild-sass-plugin"
58
+ import { cwd } from "./constants.js"
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Atomic JSON write (mirrors _atomicWriteJson from util/fastpath_manifest.ts).
62
+ // Used for contentmap.json and manifest.json writes in this handler.
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function _atomicWriteJson(dir, filename, data) {
66
+ mkdirSync(dir, { recursive: true })
67
+ const final = join(dir, filename)
68
+ const tmp = join(dir, filename + "." + process.pid + "." + randomUUID() + ".tmp")
69
+ writeFileSync(tmp, JSON.stringify(data), "utf8")
70
+ renameSync(tmp, final)
71
+ }
72
+
73
+ // Atomic write for plain-text content (used for .build-id).
74
+ function _atomicWriteText(dir, filename, text) {
75
+ mkdirSync(dir, { recursive: true })
76
+ const final = join(dir, filename)
77
+ const tmp = join(dir, filename + "." + process.pid + "." + randomUUID() + ".tmp")
78
+ writeFileSync(tmp, text, "utf8")
79
+ renameSync(tmp, final)
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // TypeScript executor source — compiled at runtime by esbuild stdin API.
84
+ //
85
+ // This source is "virtually" located at quartz/cli/build_partial_inline.ts,
86
+ // so esbuild resolves imports with resolveDir = path.join(cwd, "quartz", "cli"):
87
+ //
88
+ // ../util/fastpath_manifest → quartz/util/fastpath_manifest.ts (T1 helper)
89
+ // ../processors/parse → quartz/processors/parse.ts
90
+ // ../../quartz.config → quartz.config.ts
91
+ // ../plugins → quartz/plugins/index.ts
92
+ // ../util/ctx → quartz/util/ctx.ts
93
+ // ../util/path → quartz/util/path.ts
94
+ // ../util/random → quartz/util/random.ts
95
+ //
96
+ // Option C exclusion: ContentIndex, TagPage, and FolderPage are skipped in the emitter walk.
97
+ // _PARTIAL_EMIT_EXCLUDED.has(emitter.name) triggers a continue; no .emit call is made.
98
+ // ---------------------------------------------------------------------------
99
+
100
+ const _PARTIAL_BUILD_TS = `
101
+ import { computeFingerprint, FINGERPRINT_VERSION } from "../util/fastpath_manifest"
102
+ import { parseMarkdown } from "../processors/parse"
103
+ import cfg from "../../quartz.config"
104
+ import { getStaticResourcesFromPlugins } from "../plugins"
105
+ import { trieFromAllFiles } from "../util/ctx"
106
+ import { joinSegments, FilePath } from "../util/path"
107
+ import { randomIdNonSecure } from "../util/random"
108
+
109
+ export async function runPartialBuild(params) {
110
+ const { argv, slug, slugEntry, contentmapEntries } = params
111
+
112
+ // Reconstruct allFiles/allSlugs from contentmap for the BuildCtx.
113
+ // This gives emitters the full cross-file view (backlinks, breadcrumbs, etc.)
114
+ // even though we only re-parse and emit one file.
115
+ const allFiles = contentmapEntries
116
+ .filter(function(e) { return e.type === "markdown" && e.filePath })
117
+ .map(function(e) { return e.filePath })
118
+ const allSlugs = contentmapEntries
119
+ .filter(function(e) { return e.type === "markdown" && e.vfileData && e.vfileData.slug })
120
+ .map(function(e) { return e.vfileData.slug })
121
+
122
+ const ctx = {
123
+ buildId: randomIdNonSecure(),
124
+ argv: argv,
125
+ cfg: cfg,
126
+ allSlugs: allSlugs,
127
+ allFiles: allFiles,
128
+ incremental: true,
129
+ cacheDir: undefined,
130
+ }
131
+
132
+ // Re-parse the single changed file. Uses Plan A parser cache on hit.
133
+ const absoluteSourcePath = joinSegments(argv.directory, slugEntry.source_path)
134
+ const parsed = await parseMarkdown(ctx, [absoluteSourcePath])
135
+
136
+ if (parsed.length === 0) {
137
+ throw new Error(
138
+ "build-partial: parseMarkdown returned empty result for slug=" + slug +
139
+ " source=" + absoluteSourcePath
140
+ )
141
+ }
142
+
143
+ // Extract vfile early so it can be attached to changeEvents.
144
+ // Every upstream partialEmit starts with: if (!changeEvent.file) continue
145
+ // Without file attached, the emitter walk emits 0 files (ship-blocker HIGH-1 fix).
146
+ const [_tree, vfile0] = parsed[0]
147
+
148
+ // Rebuild ctx.trie from contentmap entries so breadcrumbs, backlinks, and
149
+ // the Explorer component all see the full vault structure.
150
+ const allVFileData = contentmapEntries
151
+ .filter(function(e) { return e.type === "markdown" && e.vfileData })
152
+ .map(function(e) {
153
+ const d = e.vfileData
154
+ return Object.assign({}, d, {
155
+ filePath: d.filePath || e.filePath,
156
+ dates: {
157
+ created: d.dates && d.dates.created ? new Date(d.dates.created) : null,
158
+ modified: d.dates && d.dates.modified ? new Date(d.dates.modified) : null,
159
+ published: d.dates && d.dates.published ? new Date(d.dates.published) : null,
160
+ },
161
+ })
162
+ })
163
+ ctx.trie = trieFromAllFiles(allVFileData)
164
+
165
+ const staticResources = getStaticResourcesFromPlugins(ctx)
166
+
167
+ // Fix #1 (full-corpus reconstruction): build a ProcessedContent-equivalent array
168
+ // containing ALL files, not just the changed one. Emitters like ContentPage,
169
+ // Backlinks, TagPage, and FolderPage derive allFiles from
170
+ // content.map(c => c[1].data), so without the full set:
171
+ // - Backlinks loses cross-file sources (unchanged pages linking to this slug
172
+ // are invisible — they disappear from the rendered backlinks section).
173
+ // - Transclusion resolution (renderPage.tsx) cannot find target page blocks
174
+ // referenced in the changed file, causing broken transclusions.
175
+ // - TagPage / FolderPage see a 1-entry corpus and emit incorrect tag/folder pages.
176
+ //
177
+ // CONTRACT: synthesized unchanged entries have AST=null (index 0) and no
178
+ // rawSource/value because contentmap is metadata-only by T2 design.
179
+ // Emitters that dereference c[0] (HAST AST) for unchanged corpus entries MUST be
180
+ // excluded from the emitter walk. Currently excluded (see _PARTIAL_EMIT_EXCLUDED):
181
+ // - ContentIndex (full-build search index regeneration, Option C / T0 M2)
182
+ // - TagPage (computeTagInfo reads unchanged custom tags/<tag>.md trees)
183
+ // - FolderPage (computeFolderInfo reads unchanged folder description trees)
184
+ // Verified by reading upstream emitters at tagPage.tsx:48-55 and folderPage.tsx:81-85.
185
+ // Adding a new emitter that reads c[0] for non-changed slugs requires updating this set.
186
+ const changedSlug = slug
187
+ const synthesizedUnchanged = contentmapEntries
188
+ .filter(function(e) {
189
+ return e.type === "markdown" && e.vfileData &&
190
+ (e.vfileData.slug ?? null) !== changedSlug
191
+ })
192
+ .map(function(e) {
193
+ const d = e.vfileData
194
+ const rehydratedData = Object.assign({}, d, {
195
+ filePath: d.filePath || e.filePath,
196
+ // Rehydrate Date strings -> Date instances (mirrors parse.ts rehydrateDates).
197
+ dates: {
198
+ created: d.dates && d.dates.created ? new Date(d.dates.created) : null,
199
+ modified: d.dates && d.dates.modified ? new Date(d.dates.modified) : null,
200
+ published: d.dates && d.dates.published ? new Date(d.dates.published) : null,
201
+ },
202
+ })
203
+ // AST (index 0) is null for unchanged entries — only c[1].data is used by emitters.
204
+ return [null, { data: rehydratedData, value: undefined }]
205
+ })
206
+ // filteredContent[0] = changed file with full HAST + vfile (real parse result)
207
+ // filteredContent[1..] = unchanged files with null AST and metadata-only vfile data
208
+ const filteredContent = [parsed[0], ...synthesizedUnchanged]
209
+ const emittedFiles = []
210
+
211
+ // Build ChangeEvent with file attached. Without file, every emitter's partialEmit
212
+ // does: if (!changeEvent.file) continue — emitting 0 files total.
213
+ // Fix #3 (path shape): use vault-relative source_path, NOT absolute path.
214
+ // Upstream chokidar watcher records vault-relative paths (build.ts:416-425) and
215
+ // rebuild() emits those same relative keys as ChangeEvent.path (build.ts:499-507).
216
+ const changeEvents = [{
217
+ type: "change" as const,
218
+ path: slugEntry.source_path as FilePath,
219
+ file: vfile0,
220
+ }]
221
+
222
+ // Walk emitters — Option C: skip excluded emitters (see _PARTIAL_EMIT_EXCLUDED below).
223
+ // emitter.partialEmit is preferred when available; falls back to emitter.emit.
224
+ //
225
+ // Fix #2 (fail-fast): a SINGLE try/catch wraps the ENTIRE emitter walk.
226
+ // Any emitter exception aborts via process.exit(5) BEFORE the artifact write block
227
+ // below (contentmap.json / manifest.json / .build-id writes at step 14).
228
+ // This enforces the Plan B contract: partial emit writes NOTHING on failure so the
229
+ // classifier's "successful fingerprint" invariant is never corrupted by stale HTML.
230
+ let _currentEmitterName = "unknown"
231
+ try {
232
+ // Option C exclusions: emitters that dereference c[0] (HAST AST) for unchanged
233
+ // content entries cannot operate safely on the synthesized [null, vfile] tuples.
234
+ // All three exclusions accept staleness until the next full build:
235
+ // - ContentIndex: full-corpus search index regeneration is full-only by T0/M2.
236
+ // - TagPage: computeTagInfo at tagPage.tsx:48-55 dereferences unchanged
237
+ // custom tags/<tag>.md entries' tree -> TagContent.tsx:111
238
+ // crashes on null.children.length.
239
+ // - FolderPage: computeFolderInfo at folderPage.tsx:81-85 same pattern;
240
+ // FolderContent.tsx:99 crashes on null.children.length.
241
+ // Trivial edits don't change tags or folder structure (canonical-blob spec
242
+ // routes those to NON_TRIVIAL -> full build via T3 classifier), so tag/folder
243
+ // pages don't need regeneration on the fast path.
244
+ const _PARTIAL_EMIT_EXCLUDED = new Set(["ContentIndex", "TagPage", "FolderPage"])
245
+ for (const emitter of cfg.plugins.emitters) {
246
+ if (_PARTIAL_EMIT_EXCLUDED.has(emitter.name)) {
247
+ if (argv.verbose) {
248
+ console.log("[build-partial] skipping " + emitter.name + " (Option C exclusion)")
249
+ }
250
+ continue
251
+ }
252
+
253
+ _currentEmitterName = emitter.name
254
+ const emitFn = emitter.partialEmit || emitter.emit
255
+ const result = await emitFn(ctx, filteredContent, staticResources, changeEvents)
256
+ if (result == null) continue
257
+ if (typeof result === "object" && Symbol.asyncIterator in result) {
258
+ for await (const f of result) {
259
+ emittedFiles.push(f)
260
+ if (argv.verbose) {
261
+ console.log("[build-partial emit:" + emitter.name + "] " + f)
262
+ }
263
+ }
264
+ } else if (Array.isArray(result)) {
265
+ for (const f of result) {
266
+ emittedFiles.push(f)
267
+ if (argv.verbose) {
268
+ console.log("[build-partial emit:" + emitter.name + "] " + f)
269
+ }
270
+ }
271
+ }
272
+ }
273
+ } catch (err) {
274
+ // Fail-fast: abort before artifact writes so stale HTML is never committed
275
+ // with a fresh fingerprint (Plan B contract violation if we continued).
276
+ process.stderr.write(
277
+ "partial emit failed in " + _currentEmitterName + ": " +
278
+ (err instanceof Error ? err.message : String(err)) + "\\n"
279
+ )
280
+ process.exit(5)
281
+ }
282
+
283
+ // Compute updated fingerprint for this slug (using T1's computeFingerprint).
284
+ // vfile0 was already extracted above (before emitter walk); parsed0 alias for computeFingerprint.
285
+ const parsed0 = parsed[0]
286
+ const newFingerprint = computeFingerprint(parsed0, {
287
+ sourcePath: slugEntry.source_path,
288
+ outputPath: slugEntry.output_path,
289
+ })
290
+
291
+ // Serialize updated vfile.data for contentmap entry replacement.
292
+ const data = vfile0.data
293
+ const rawDates = data.dates
294
+ const serializedDates = {
295
+ created: rawDates && rawDates.created instanceof Date
296
+ ? rawDates.created.toISOString()
297
+ : (rawDates && rawDates.created != null ? rawDates.created : null),
298
+ modified: rawDates && rawDates.modified instanceof Date
299
+ ? rawDates.modified.toISOString()
300
+ : (rawDates && rawDates.modified != null ? rawDates.modified : null),
301
+ published: rawDates && rawDates.published instanceof Date
302
+ ? rawDates.published.toISOString()
303
+ : (rawDates && rawDates.published != null ? rawDates.published : null),
304
+ }
305
+
306
+ return {
307
+ newFingerprint: newFingerprint,
308
+ emittedFiles: emittedFiles,
309
+ newVFileData: {
310
+ frontmatter: data.frontmatter || null,
311
+ links: data.links || null,
312
+ text: data.text || null,
313
+ blocks: data.blocks || {},
314
+ dates: serializedDates,
315
+ filePath: String(data.filePath || ""),
316
+ relativePath: String(data.relativePath || ""),
317
+ slug: String(data.slug || ""),
318
+ description: typeof data.description === "string" ? data.description : null,
319
+ toc: data.toc != null ? data.toc : null,
320
+ collapseToc: typeof data.collapseToc === "boolean" ? data.collapseToc : null,
321
+ aliases: Array.isArray(data.aliases) ? data.aliases.map(String) : null,
322
+ hasMermaidDiagram: typeof data.hasMermaidDiagram === "boolean"
323
+ ? data.hasMermaidDiagram
324
+ : null,
325
+ },
326
+ }
327
+ }
328
+ `
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // esbuild compilation of the TypeScript executor
332
+ //
333
+ // The compiled output is cached at _PARTIAL_CACHE_FILE. Freshness check:
334
+ // skip recompilation if the file exists AND is <5 minutes old (300 000 ms).
335
+ // This keeps "warm" partial builds fast (~100-300ms) while ensuring overlay
336
+ // changes (which require re-installation anyway) are picked up promptly.
337
+ // ---------------------------------------------------------------------------
338
+
339
+ const _PARTIAL_CACHE_FILE = path.join(cwd, "quartz", ".quartz-cache", "transpiled-build-partial.mjs")
340
+
341
+ async function _compilePartialBuildTs() {
342
+ // Freshness check — skip recompilation if cache is warm (<5 min old).
343
+ let needsCompile = true
344
+ try {
345
+ const { mtimeMs } = statSync(_PARTIAL_CACHE_FILE)
346
+ needsCompile = (Date.now() - mtimeMs) > 300_000
347
+ } catch (_e) {
348
+ // File absent — must compile.
349
+ }
350
+
351
+ if (needsCompile) {
352
+ // Ensure cache directory exists.
353
+ mkdirSync(path.join(cwd, "quartz", ".quartz-cache"), { recursive: true })
354
+
355
+ await esbuild.build({
356
+ stdin: {
357
+ contents: _PARTIAL_BUILD_TS,
358
+ resolveDir: path.join(cwd, "quartz", "cli"),
359
+ loader: "ts",
360
+ sourcefile: "build_partial_inline.ts",
361
+ },
362
+ outfile: _PARTIAL_CACHE_FILE,
363
+ bundle: true,
364
+ keepNames: true,
365
+ minifyWhitespace: true,
366
+ minifySyntax: true,
367
+ platform: "node",
368
+ format: "esm",
369
+ packages: "external",
370
+ sourcemap: false,
371
+ plugins: [
372
+ sassPlugin({ type: "css-text", cssImports: true }),
373
+ sassPlugin({ filter: /\.inline\.scss$/, type: "css", cssImports: true }),
374
+ {
375
+ name: "inline-script-loader",
376
+ setup(build) {
377
+ build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
378
+ let text = await fsPromises.readFile(args.path, "utf8")
379
+ text = text.replace("export default", "")
380
+ text = text.replace("export", "")
381
+ const sourcefile = path.relative(path.resolve("."), args.path)
382
+ const resolveDir = path.dirname(sourcefile)
383
+ const transpiled = await esbuild.build({
384
+ stdin: { contents: text, loader: "ts", resolveDir, sourcefile },
385
+ write: false,
386
+ bundle: true,
387
+ minify: true,
388
+ platform: "browser",
389
+ format: "esm",
390
+ })
391
+ return { contents: transpiled.outputFiles[0].text, loader: "text" }
392
+ })
393
+ },
394
+ },
395
+ ],
396
+ })
397
+ }
398
+
399
+ // Bypass module cache with unique query string (same pattern as upstream handleBuild).
400
+ const { runPartialBuild } = await import(_PARTIAL_CACHE_FILE + "?update=" + randomUUID())
401
+ return runPartialBuild
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Main exported handler — called by handlers.js as ``partialBuildContent``
406
+ // ---------------------------------------------------------------------------
407
+
408
+ export async function handlePartialBuild(argv) {
409
+ const startMs = Date.now()
410
+ const slug = argv.slug
411
+
412
+ if (!slug) {
413
+ process.stderr.write("build-partial: --slug is required\n")
414
+ process.exit(1)
415
+ }
416
+
417
+ // Step 2: fastpath dir lives under the vault root (argv.directory), NOT argv.output.
418
+ // Per T0 F5.3: <vault>/.quartz/.cache/fastpath/
419
+ const fastpathDir = path.join(argv.directory, ".quartz", ".cache", "fastpath")
420
+
421
+ // Step 3: Load manifest.json (the commit marker written by T2 full-build hook).
422
+ let manifest
423
+ try {
424
+ const raw = readFileSync(join(fastpathDir, "manifest.json"), "utf8")
425
+ manifest = JSON.parse(raw)
426
+ } catch (_err) {
427
+ process.stderr.write("manifest not found — full build required\n")
428
+ process.exit(1)
429
+ }
430
+
431
+ // Step 4: Load contentmap.json (written atomically before manifest by T2).
432
+ let contentmap
433
+ try {
434
+ const raw = readFileSync(join(fastpathDir, "contentmap.json"), "utf8")
435
+ contentmap = JSON.parse(raw)
436
+ } catch (_err) {
437
+ process.stderr.write("contentmap not found — full build required\n")
438
+ process.exit(1)
439
+ }
440
+
441
+ // Step 5: Envelope cross-check (Codex pre-flight contract — non-negotiable).
442
+ // Both version and parent_build_id must match across manifest and contentmap.
443
+ if (manifest.version !== contentmap.version ||
444
+ manifest.parent_build_id !== contentmap.parent_build_id) {
445
+ process.stderr.write(
446
+ "envelope mismatch: manifest=" + manifest.version + "/" + manifest.parent_build_id +
447
+ " contentmap=" + contentmap.version + "/" + contentmap.parent_build_id + "\n"
448
+ )
449
+ process.exit(2)
450
+ }
451
+
452
+ // Step 6: Look up slug in manifest.slugs.
453
+ // Output paths come from manifest.slugs[slug].output_path — not re-derived.
454
+ const slugEntry = manifest.slugs[slug]
455
+ if (!slugEntry) {
456
+ process.stderr.write("slug not in manifest — full build required: " + slug + "\n")
457
+ process.exit(3)
458
+ }
459
+
460
+ // Step 6b: Unsupported fast-path slug guard (exit 6).
461
+ // ContentPage.partialEmit skips slugs starting with "tags/" or ending with "/index"
462
+ // (contentPage.tsx:112-117). Our deny-list also skips TagPage/FolderPage which own
463
+ // those outputs. So a fast-path edit to such a slug would advance
464
+ // manifest/contentmap/.build-id while leaving the rendered HTML stale.
465
+ // Refuse here and let T5/T6 fall back to full build.
466
+ if (slug.startsWith("tags/") || slug.endsWith("/index") || slug === "index") {
467
+ process.stderr.write(
468
+ "scope: full build required for slug=" + slug +
469
+ " (tag/folder pages must be refreshed by full build)\n"
470
+ )
471
+ process.exit(6)
472
+ }
473
+
474
+ // Step 7: Look up slug in contentmap.entries (envelope-style array, NOT bare array).
475
+ const contentEntryIdx = (contentmap.entries || []).findIndex(
476
+ function(e) { return e.vfileData && e.vfileData.slug === slug }
477
+ )
478
+ if (contentEntryIdx === -1) {
479
+ process.stderr.write("slug not in contentmap — full build required: " + slug + "\n")
480
+ process.exit(4)
481
+ }
482
+
483
+ // Steps 8-10: Compile + run the TypeScript executor via esbuild stdin.
484
+ let result
485
+ try {
486
+ const runPartialBuild = await _compilePartialBuildTs()
487
+ result = await runPartialBuild({
488
+ argv: argv,
489
+ slug: slug,
490
+ slugEntry: slugEntry,
491
+ contentmapEntries: contentmap.entries,
492
+ })
493
+ } catch (err) {
494
+ process.stderr.write("build-partial: executor failed: " + err + "\n")
495
+ process.exit(1)
496
+ }
497
+
498
+ const { newFingerprint, newVFileData, emittedFiles } = result
499
+
500
+ if (argv.verbose) {
501
+ console.log("wiki: build-partial emitted " + emittedFiles.length + " file(s):")
502
+ for (const f of emittedFiles) console.log(" " + f)
503
+ }
504
+
505
+ // Step 12: Update manifest.slugs for this slug only.
506
+ // KEEP manifest.parent_build_id unchanged — partial emit inherits it from the full build.
507
+ const updatedManifest = Object.assign({}, manifest, {
508
+ built_at_ms: Date.now(),
509
+ slugs: Object.assign({}, manifest.slugs, {
510
+ [slug]: Object.assign({}, slugEntry, { fingerprint: newFingerprint }),
511
+ }),
512
+ })
513
+
514
+ // Step 13: Replace contentmap entry for this slug.
515
+ // KEEP contentmap.parent_build_id unchanged.
516
+ const updatedEntries = (contentmap.entries || []).slice()
517
+ updatedEntries[contentEntryIdx] = Object.assign(
518
+ {},
519
+ updatedEntries[contentEntryIdx],
520
+ {
521
+ hastRoot: null,
522
+ vfileData: newVFileData,
523
+ }
524
+ )
525
+ const updatedContentmap = Object.assign({}, contentmap, {
526
+ built_at_ms: Date.now(),
527
+ entries: updatedEntries,
528
+ })
529
+
530
+ // Step 14: Write order — contentmap FIRST, manifest SECOND, .build-id LAST.
531
+ // This preserves the T2 commit-marker invariant: if manifest.json exists,
532
+ // contentmap.json is guaranteed present and consistent.
533
+ _atomicWriteJson(fastpathDir, "contentmap.json", updatedContentmap)
534
+ _atomicWriteJson(fastpathDir, "manifest.json", updatedManifest)
535
+
536
+ // .build-id lives at <argv.output>/.build-id — the watcher's reload trigger.
537
+ // Emit a millisecond-suffixed fast-path id matching the plan spec.
538
+ const buildId = "fastpath-" + Date.now() + "-" + randomUUID().slice(0, 8)
539
+ _atomicWriteText(argv.output, ".build-id", buildId)
540
+
541
+ const elapsedMs = Date.now() - startMs
542
+ console.log("wiki: build-partial slug=" + slug + " elapsed=" + elapsedMs + "ms")
543
+ process.exit(0)
544
+ }