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,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
|
+
}
|