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.
- brain/__init__.py +0 -0
- brain/__main__.py +18 -0
- brain/_capture_command.py +445 -0
- brain/_compose.py +52 -0
- brain/activity.py +206 -0
- brain/ask.py +631 -0
- brain/audio.py +591 -0
- brain/backfill/__init__.py +12 -0
- brain/backfill/search_extras.py +141 -0
- brain/backfill/source_rows.py +101 -0
- brain/bin/__init__.py +1 -0
- brain/bin/_launcher.py +107 -0
- brain/bin/down.py +8 -0
- brain/bin/launchd.py +268 -0
- brain/bin/monitor.py +570 -0
- brain/bin/rebuild.py +8 -0
- brain/bin/status.py +8 -0
- brain/bin/up.py +8 -0
- brain/brief.py +272 -0
- brain/capture.py +49 -0
- brain/chat.py +293 -0
- brain/cli.py +9760 -0
- brain/cli_claude.py +81 -0
- brain/cli_connect.py +285 -0
- brain/cli_demo.py +266 -0
- brain/config.py +1949 -0
- brain/connect.py +925 -0
- brain/db.py +540 -0
- brain/demo/__init__.py +452 -0
- brain/demo/corpus/manifest.json +403 -0
- brain/demo/embedder.py +74 -0
- brain/durations.py +84 -0
- brain/edit_session.py +156 -0
- brain/editor.py +67 -0
- brain/elicit/__init__.py +16 -0
- brain/elicit/detectors.py +250 -0
- brain/elicit/drafter.py +70 -0
- brain/elicit/queue.py +220 -0
- brain/elicit/schema.py +48 -0
- brain/elicit/session.py +445 -0
- brain/embedding_targets.py +54 -0
- brain/embeddings.py +424 -0
- brain/enrichment.py +808 -0
- brain/errors.py +357 -0
- brain/eval/__init__.py +129 -0
- brain/eval/answer_eval.py +281 -0
- brain/eval/baseline.py +265 -0
- brain/eval/concept_extraction.py +378 -0
- brain/eval/corpus.py +152 -0
- brain/eval/errors.py +19 -0
- brain/eval/graph_baseline.py +226 -0
- brain/eval/graph_retrieval.py +202 -0
- brain/eval/graph_runner.py +319 -0
- brain/eval/metrics.py +101 -0
- brain/eval/runner.py +223 -0
- brain/format.py +783 -0
- brain/gaps.py +390 -0
- brain/graph_rag/__init__.py +94 -0
- brain/graph_rag/_retrieval_common.py +113 -0
- brain/graph_rag/aggregates.py +303 -0
- brain/graph_rag/aliases/__init__.py +583 -0
- brain/graph_rag/backends/__init__.py +10 -0
- brain/graph_rag/backends/_age_helpers.py +473 -0
- brain/graph_rag/backends/age.py +782 -0
- brain/graph_rag/backends/base.py +272 -0
- brain/graph_rag/build.py +344 -0
- brain/graph_rag/communities.py +644 -0
- brain/graph_rag/communities_summary.py +437 -0
- brain/graph_rag/concepts.py +202 -0
- brain/graph_rag/cooccur.py +193 -0
- brain/graph_rag/cross_type.py +312 -0
- brain/graph_rag/extract.py +885 -0
- brain/graph_rag/fuse.py +371 -0
- brain/graph_rag/global_.py +412 -0
- brain/graph_rag/grouping.py +372 -0
- brain/graph_rag/person_resolver.py +167 -0
- brain/graph_rag/reconcile.py +792 -0
- brain/graph_rag/relational.py +353 -0
- brain/graph_rag/retrieve.py +526 -0
- brain/graph_rag/router.py +288 -0
- brain/graph_rag/schema.py +320 -0
- brain/graph_rag/sync.py +237 -0
- brain/graph_rag/tenancy.py +43 -0
- brain/graph_rag/themes.py +501 -0
- brain/graph_rag/weighting.py +202 -0
- brain/ingest/__init__.py +1926 -0
- brain/ingest/chunker.py +249 -0
- brain/ingest/docx.py +40 -0
- brain/ingest/gmail.py +621 -0
- brain/ingest/markdown.py +37 -0
- brain/ingest/pdf.py +61 -0
- brain/ingest/stdin.py +22 -0
- brain/ingest/sub_tokens.py +91 -0
- brain/ingest/text.py +16 -0
- brain/interactions.py +205 -0
- brain/maintenance.py +355 -0
- brain/mcp_server.py +3405 -0
- brain/migrations/001_init.sql +43 -0
- brain/migrations/002_qwen3_embedding.sql +17 -0
- brain/migrations/003_vault_model.sql +41 -0
- brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
- brain/migrations/005_derived_links.sql +67 -0
- brain/migrations/006_dedup_file_by_source_path.sql +25 -0
- brain/migrations/007_email_thread_and_draft.sql +15 -0
- brain/migrations/008_gmail_thread_unique.sql +11 -0
- brain/migrations/009_chunks_weighted_tsv.sql +28 -0
- brain/migrations/010_interactions.sql +30 -0
- brain/migrations/011_documents_summary.sql +23 -0
- brain/migrations/012_graphrag.sql +171 -0
- brain/migrations/013_graphrag_communities.sql +125 -0
- brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
- brain/migrations/015_interactions_graph_targets.sql +89 -0
- brain/migrations/016_index_hygiene.sql +61 -0
- brain/migrations/017_elicit.sql +30 -0
- brain/migrations/018_review_gap_signal_kinds.sql +40 -0
- brain/migrations/019_search_queries.sql +35 -0
- brain/migrations/020_link_suggestions.sql +40 -0
- brain/migrations/021_timeline_doc_date.sql +34 -0
- brain/migrations/022_link_suggestions_undirected.sql +84 -0
- brain/migrations/023_search_queries_fts_count.sql +28 -0
- brain/quartz_overrides/__init__.py +8 -0
- brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
- brain/quartz_overrides/quartz/build.ts +568 -0
- brain/quartz_overrides/quartz/cli/args.js +152 -0
- brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
- brain/quartz_overrides/quartz/cli/handlers.js +636 -0
- brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
- brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
- brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
- brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
- brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
- brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
- brain/quartz_overrides/quartz/components/Search.tsx +161 -0
- brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
- brain/quartz_overrides/quartz/components/index.ts +92 -0
- brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
- brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
- brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
- brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
- brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
- brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
- brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
- brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
- brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
- brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
- brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
- brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
- brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
- brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
- brain/quartz_overrides/quartz/processors/parse.ts +371 -0
- brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
- brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
- brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
- brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
- brain/quartz_overrides/quartz/static/emailThread.js +334 -0
- brain/quartz_overrides/quartz/static/favicon.ico +0 -0
- brain/quartz_overrides/quartz/static/icon.png +0 -0
- brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
- brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
- brain/quartz_overrides/quartz/static/reload.js +168 -0
- brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
- brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
- brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
- brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
- brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
- brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
- brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
- brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
- brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
- brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
- brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
- brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
- brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
- brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
- brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
- brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
- brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
- brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
- brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
- brain/quartz_overrides/quartz/styles/custom.scss +89 -0
- brain/quartz_overrides/quartz/styles/graph.scss +505 -0
- brain/quartz_overrides/quartz/util/ctx.ts +92 -0
- brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
- brain/quartz_overrides/quartz/util/path.ts +358 -0
- brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
- brain/quartz_overrides/quartz.config.ts +270 -0
- brain/quartz_overrides/quartz.layout.ts +314 -0
- brain/queries.py +1188 -0
- brain/rank_fusion.py +8 -0
- brain/resurface.py +210 -0
- brain/review/__init__.py +26 -0
- brain/review/emit.py +27 -0
- brain/review/queries.py +436 -0
- brain/review/render.py +196 -0
- brain/review/scans.py +355 -0
- brain/review/weekly.py +413 -0
- brain/search.py +704 -0
- brain/set_similarity.py +15 -0
- brain/setup.py +1205 -0
- brain/tags.py +56 -0
- brain/templates/Caddyfile.j2 +9 -0
- brain/templates/__init__.py +1 -0
- brain/templates/bin/__init__.py +1 -0
- brain/templates/bin/_brain-brief-fg.sh +25 -0
- brain/templates/bin/_brain-build-fg.sh +53 -0
- brain/templates/bin/_brain-watcher-fg.sh +65 -0
- brain/templates/bin/brain-down.sh +89 -0
- brain/templates/bin/brain-status.sh +83 -0
- brain/templates/bin/brain-up.sh +221 -0
- brain/templates/docker/age/Dockerfile +79 -0
- brain/templates/docker-compose.stock.yml.j2 +26 -0
- brain/templates/docker-compose.yml.j2 +34 -0
- brain/templates/env.example +190 -0
- brain/templates/launchd/__init__.py +1 -0
- brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
- brain/templates/launchd/com.brain.build.plist.j2 +46 -0
- brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
- brain/templates/skill/SKILL.md +63 -0
- brain/templates/skill/__init__.py +1 -0
- brain/timeline.py +834 -0
- brain/todo.py +124 -0
- brain/uninstall.py +185 -0
- brain/vault/__init__.py +115 -0
- brain/vault/_atomic.py +25 -0
- brain/vault/daily_index.py +228 -0
- brain/vault/derived_links/__init__.py +50 -0
- brain/vault/derived_links/directory.py +683 -0
- brain/vault/derived_links/fence.py +408 -0
- brain/vault/derived_links/gws.py +64 -0
- brain/vault/derived_links/participants.py +143 -0
- brain/vault/derived_links/pass_runner.py +362 -0
- brain/vault/derived_links/rules.py +137 -0
- brain/vault/export.py +683 -0
- brain/vault/frontmatter.py +165 -0
- brain/vault/graph.py +620 -0
- brain/vault/graph_format.py +388 -0
- brain/vault/link_rewrite.py +235 -0
- brain/vault/links.py +260 -0
- brain/vault/note_builder.py +211 -0
- brain/vault/paths.py +55 -0
- brain/vault/quartz_overlay.py +236 -0
- brain/vault/rename.py +591 -0
- brain/vault/resolver.py +304 -0
- brain/vault/slug.py +127 -0
- brain/vault/sync.py +1513 -0
- brain/vault/sync_summaries.py +264 -0
- brain/vault/templates.py +145 -0
- brain/vault/watch.py +1052 -0
- brain/wiki/__init__.py +6 -0
- brain/wiki/_github_slugger.py +76 -0
- brain/wiki/_person_name.py +314 -0
- brain/wiki/build_homepage.py +541 -0
- brain/wiki/build_partial.py +273 -0
- brain/wiki/build_people.py +934 -0
- brain/wiki/build_related.py +758 -0
- brain/wiki/build_swap.py +585 -0
- brain/wiki/build_watcher.py +975 -0
- brain/wiki/edit_classifier.py +215 -0
- brain/wiki/errors.py +10 -0
- brain/wiki/fastpath_manifest.py +475 -0
- brain/wiki/fastpath_state.py +174 -0
- brain/wiki/install.py +296 -0
- brain/wiki/slug.py +111 -0
- secondbrain_py-0.2.1.dist-info/METADATA +195 -0
- secondbrain_py-0.2.1.dist-info/RECORD +273 -0
- secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
- secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
- secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|