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,273 @@
|
|
|
1
|
+
"""Python subprocess wrapper for the Quartz ``build-partial`` CLI subcommand.
|
|
2
|
+
|
|
3
|
+
Drives the Node.js ``build-partial`` handler (T4) as a one-shot subprocess
|
|
4
|
+
and surfaces structured failure information via :class:`BrainWikiPartialBuildError`
|
|
5
|
+
so the watcher (T6) can route recovery decisions (e.g. force full build on exit 6
|
|
6
|
+
vs. increment a retry counter on exit 5) without string-matching stderr.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from brain.wiki.errors import BrainWikiBuildError
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Maximum number of stderr characters surfaced in error messages.
|
|
24
|
+
# Long emitter stack traces are trimmed to avoid log bloat.
|
|
25
|
+
_STDERR_TAIL_LIMIT = 500
|
|
26
|
+
|
|
27
|
+
# Regex for the success stdout line emitted by build_partial_handler.js:
|
|
28
|
+
# "wiki: build-partial slug=<slug> elapsed=<N>ms"
|
|
29
|
+
_ELAPSED_RE = re.compile(r"elapsed=(\d+)ms")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Failure taxonomy
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PartialBuildFailureKind(Enum):
|
|
38
|
+
"""Distinct failure categories for telemetry and recovery routing.
|
|
39
|
+
|
|
40
|
+
T6 uses this enum to decide whether to fall back to a full build
|
|
41
|
+
immediately (e.g. ``UNSUPPORTED_SLUG_SCOPE``) or to increment a
|
|
42
|
+
partial-failure counter first (e.g. ``EMITTER_FAILED``).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
MISSING_NODE = "missing-node"
|
|
46
|
+
"""``node`` binary not found on PATH."""
|
|
47
|
+
|
|
48
|
+
MISSING_BOOTSTRAP = "missing-bootstrap"
|
|
49
|
+
"""``bootstrap-cli.mjs`` absent in the workspace directory."""
|
|
50
|
+
|
|
51
|
+
MANIFEST_OR_CONTENTMAP_UNREADABLE = "manifest-or-contentmap-unreadable"
|
|
52
|
+
"""Exit 1: manifest or contentmap JSON missing or unparseable."""
|
|
53
|
+
|
|
54
|
+
ENVELOPE_MISMATCH = "envelope-mismatch"
|
|
55
|
+
"""Exit 2: ``version`` or ``parent_build_id`` differs between manifest and contentmap."""
|
|
56
|
+
|
|
57
|
+
SLUG_NOT_IN_MANIFEST = "slug-not-in-manifest"
|
|
58
|
+
"""Exit 3: the requested slug is absent from ``manifest.slugs``."""
|
|
59
|
+
|
|
60
|
+
SLUG_NOT_IN_CONTENTMAP = "slug-not-in-contentmap"
|
|
61
|
+
"""Exit 4: the slug is in the manifest but absent from ``contentmap.entries``."""
|
|
62
|
+
|
|
63
|
+
EMITTER_FAILED = "emitter-failed"
|
|
64
|
+
"""Exit 5: an uncaught exception during the emitter walk (fail-fast)."""
|
|
65
|
+
|
|
66
|
+
UNSUPPORTED_SLUG_SCOPE = "unsupported-slug-scope"
|
|
67
|
+
"""Exit 6: slug is a tag/folder/index page that requires a full build."""
|
|
68
|
+
|
|
69
|
+
UNKNOWN_NONZERO = "unknown-nonzero"
|
|
70
|
+
"""Any nonzero exit code not covered by exit codes 1-6."""
|
|
71
|
+
|
|
72
|
+
SUBPROCESS_TIMEOUT = "subprocess-timeout"
|
|
73
|
+
"""The build-partial subprocess exceeded the configured timeout."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Mapping from Node process exit codes (1-6) to failure kinds.
|
|
77
|
+
_EXIT_CODE_TO_KIND: dict[int, PartialBuildFailureKind] = {
|
|
78
|
+
1: PartialBuildFailureKind.MANIFEST_OR_CONTENTMAP_UNREADABLE,
|
|
79
|
+
2: PartialBuildFailureKind.ENVELOPE_MISMATCH,
|
|
80
|
+
3: PartialBuildFailureKind.SLUG_NOT_IN_MANIFEST,
|
|
81
|
+
4: PartialBuildFailureKind.SLUG_NOT_IN_CONTENTMAP,
|
|
82
|
+
5: PartialBuildFailureKind.EMITTER_FAILED,
|
|
83
|
+
6: PartialBuildFailureKind.UNSUPPORTED_SLUG_SCOPE,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Exception
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BrainWikiPartialBuildError(BrainWikiBuildError):
|
|
93
|
+
"""Raised on any ``build-partial`` failure.
|
|
94
|
+
|
|
95
|
+
Carries a structured :class:`PartialBuildFailureKind` for routing and
|
|
96
|
+
the originating ``slug`` for diagnostics. The human-readable message
|
|
97
|
+
always includes the slug, exit code (where applicable), and the tail of
|
|
98
|
+
stderr so an operator can diagnose without needing the raw subprocess
|
|
99
|
+
output.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
message: str,
|
|
105
|
+
*,
|
|
106
|
+
kind: PartialBuildFailureKind,
|
|
107
|
+
slug: str | None = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
super().__init__(message)
|
|
110
|
+
self.kind = kind
|
|
111
|
+
self.slug = slug
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Result
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class PartialBuildResult:
|
|
121
|
+
"""Summary of a successful ``build-partial`` invocation.
|
|
122
|
+
|
|
123
|
+
``elapsed_ms`` is parsed from the Node process stdout line
|
|
124
|
+
``"wiki: build-partial slug=<slug> elapsed=<N>ms"``. If the pattern
|
|
125
|
+
is not matched (e.g. the handler changed its output format), ``elapsed_ms``
|
|
126
|
+
is ``-1`` and a warning is logged; the result is still considered
|
|
127
|
+
successful because exit code 0 was returned.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
slug: str
|
|
131
|
+
elapsed_ms: int
|
|
132
|
+
stdout: str
|
|
133
|
+
stderr: str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Public API
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run_build_partial(
|
|
142
|
+
*,
|
|
143
|
+
slug: str,
|
|
144
|
+
vault_dir: Path,
|
|
145
|
+
build_dir: Path,
|
|
146
|
+
workspace_dir: Path,
|
|
147
|
+
timeout_s: float = 30.0,
|
|
148
|
+
) -> PartialBuildResult:
|
|
149
|
+
"""Run the Quartz ``build-partial`` CLI subcommand for a single slug.
|
|
150
|
+
|
|
151
|
+
Invokes::
|
|
152
|
+
|
|
153
|
+
node <workspace_dir>/quartz/bootstrap-cli.mjs build-partial \\
|
|
154
|
+
--directory <vault_dir> \\
|
|
155
|
+
--output <build_dir> \\
|
|
156
|
+
--slug <slug>
|
|
157
|
+
|
|
158
|
+
with ``cwd=workspace_dir`` (the workspace ROOT) so Quartz can resolve
|
|
159
|
+
``./package.json`` and ``./quartz/*`` relative paths correctly.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
slug: The Quartz slug to partially rebuild (vault-relative, no ``.md``).
|
|
163
|
+
vault_dir: Vault root directory (``--directory`` argument to Quartz).
|
|
164
|
+
build_dir: Quartz output directory for the active color
|
|
165
|
+
(``--output`` argument; ``.build-id`` is written here on success).
|
|
166
|
+
workspace_dir: **Quartz workspace root** (e.g. ``<vault>/.quartz``).
|
|
167
|
+
Must contain ``quartz/bootstrap-cli.mjs`` and ``package.json``.
|
|
168
|
+
The subprocess runs with ``cwd=workspace_dir`` so Quartz's relative
|
|
169
|
+
paths (``./package.json``, ``./quartz/build.ts``, etc.) resolve
|
|
170
|
+
correctly. Relative paths are resolved to absolute before use.
|
|
171
|
+
timeout_s: Hard wall-clock limit for the subprocess (default: 30 s).
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
:class:`PartialBuildResult` on exit code 0.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
:class:`BrainWikiPartialBuildError`: On any failure. The ``kind``
|
|
178
|
+
attribute is a :class:`PartialBuildFailureKind` for routing;
|
|
179
|
+
the message includes the slug, exit code, and stderr tail.
|
|
180
|
+
"""
|
|
181
|
+
# Resolve workspace_dir to an absolute path up-front so that relative
|
|
182
|
+
# inputs (e.g. Path(".quartz")) work correctly as subprocess cwd.
|
|
183
|
+
workspace = workspace_dir.expanduser().resolve()
|
|
184
|
+
|
|
185
|
+
# Step 1: Resolve node binary — hard-fail rather than falling back to npx.
|
|
186
|
+
node = shutil.which("node")
|
|
187
|
+
if node is None:
|
|
188
|
+
raise BrainWikiPartialBuildError(
|
|
189
|
+
"node binary not found on PATH; install Node.js"
|
|
190
|
+
" (Homebrew: `brew install node`;"
|
|
191
|
+
" Linux: nodejs.org or your distro's package manager)",
|
|
192
|
+
kind=PartialBuildFailureKind.MISSING_NODE,
|
|
193
|
+
slug=slug,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Step 2: Verify bootstrap-cli.mjs exists at <workspace>/quartz/bootstrap-cli.mjs.
|
|
197
|
+
bootstrap = workspace / "quartz" / "bootstrap-cli.mjs"
|
|
198
|
+
if not bootstrap.is_file():
|
|
199
|
+
raise BrainWikiPartialBuildError(
|
|
200
|
+
f"Quartz bootstrap CLI not found at {bootstrap};"
|
|
201
|
+
" run `brain vault render --overlay` to reinstall the workspace overlay",
|
|
202
|
+
kind=PartialBuildFailureKind.MISSING_BOOTSTRAP,
|
|
203
|
+
slug=slug,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Step 3: Build subprocess argv.
|
|
207
|
+
args: list[str] = [
|
|
208
|
+
node,
|
|
209
|
+
str(bootstrap),
|
|
210
|
+
"build-partial",
|
|
211
|
+
"--directory",
|
|
212
|
+
str(vault_dir),
|
|
213
|
+
"--output",
|
|
214
|
+
str(build_dir),
|
|
215
|
+
"--slug",
|
|
216
|
+
slug,
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# Step 4: Run subprocess — capture output, never raise on exit code.
|
|
220
|
+
# cwd = workspace ROOT so Quartz's ./package.json + ./quartz/* paths resolve.
|
|
221
|
+
try:
|
|
222
|
+
proc = subprocess.run( # noqa: S603 — list-form args, no shell injection
|
|
223
|
+
args,
|
|
224
|
+
cwd=str(workspace),
|
|
225
|
+
capture_output=True,
|
|
226
|
+
text=True,
|
|
227
|
+
timeout=timeout_s,
|
|
228
|
+
env=os.environ,
|
|
229
|
+
check=False,
|
|
230
|
+
)
|
|
231
|
+
except subprocess.TimeoutExpired as exc:
|
|
232
|
+
raise BrainWikiPartialBuildError(
|
|
233
|
+
f"build-partial slug={slug} timed out after {timeout_s}s",
|
|
234
|
+
kind=PartialBuildFailureKind.SUBPROCESS_TIMEOUT,
|
|
235
|
+
slug=slug,
|
|
236
|
+
) from exc
|
|
237
|
+
except OSError as exc:
|
|
238
|
+
raise BrainWikiPartialBuildError(
|
|
239
|
+
f"build-partial slug={slug} failed to launch: {exc}",
|
|
240
|
+
kind=PartialBuildFailureKind.UNKNOWN_NONZERO,
|
|
241
|
+
slug=slug,
|
|
242
|
+
) from exc
|
|
243
|
+
|
|
244
|
+
rc = proc.returncode
|
|
245
|
+
|
|
246
|
+
# Step 5 (success): parse elapsed_ms from stdout and return.
|
|
247
|
+
if rc == 0:
|
|
248
|
+
elapsed_ms = -1
|
|
249
|
+
m = _ELAPSED_RE.search(proc.stdout)
|
|
250
|
+
if m:
|
|
251
|
+
elapsed_ms = int(m.group(1))
|
|
252
|
+
else:
|
|
253
|
+
logger.warning(
|
|
254
|
+
"build-partial slug=%s: could not parse elapsed_ms from stdout: %r",
|
|
255
|
+
slug,
|
|
256
|
+
proc.stdout[:200],
|
|
257
|
+
)
|
|
258
|
+
return PartialBuildResult(
|
|
259
|
+
slug=slug,
|
|
260
|
+
elapsed_ms=elapsed_ms,
|
|
261
|
+
stdout=proc.stdout,
|
|
262
|
+
stderr=proc.stderr,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Steps 6-7: Non-zero exit — map to failure kind and raise.
|
|
266
|
+
kind = _EXIT_CODE_TO_KIND.get(rc, PartialBuildFailureKind.UNKNOWN_NONZERO)
|
|
267
|
+
# Trim stderr to avoid bloating structured logs with multi-KB stack traces.
|
|
268
|
+
stderr_tail = proc.stderr[-_STDERR_TAIL_LIMIT:] if proc.stderr else ""
|
|
269
|
+
raise BrainWikiPartialBuildError(
|
|
270
|
+
f"build-partial slug={slug} exited {rc}: {stderr_tail}",
|
|
271
|
+
kind=kind,
|
|
272
|
+
slug=slug,
|
|
273
|
+
)
|