diff --git a/CHANGELOG.md b/CHANGELOG.md index ac7df61..044c40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,64 @@ for tagged releases. ### Added -- Initial Knowledge Agent and Digital TeamMate framework. -- Allowlist-only publication boundary and repeatable installer. -- Sanitized automation definitions and empty-vault example. +- Marp slide-deck support, with a canonical Markdown deck template, a SecondBrain theme, deck-aware validation, and managed PDF, PowerPoint, and speaker-note exports. +- Separate voice-pack profiles for public writing and internal documents, with shared protected anti-AI style rules and a rollback copy for each generated profile. + +### Changed + +- Document and slide-deck workflows now keep Markdown as the canonical source throughout drafting, approval, and delivery; exported files remain generated deliverables. +- DOCX generation now supports standard Markdown image links and preserves a more natural document structure in exported Word files. +- Daily Note rollover now replaces earlier previous-note pointers cleanly, keeping a single current link to the preceding note. + +## [2026-07-27] + +### Added + - `resolve-document-items` skill for closing open questions, unowned actions, assumptions, and pending decisions in documents through a calm one-question-at-a-time workflow. - DOCX deliverable generation now renders Markdown pipe tables as Word tables. + +### Changed + +- Daily Notes are retained together in the main workspace, and DTM validation checks that referenced projects, working notes, documents, and writing items have a same-day activity-log record. +- Framework validation now detects out-of-order system-log entries. + +## [2026-07-19] + +### Added + +- `thinking-interview` skill for working through a question with structured prompts before committing to a conclusion. +- Lifecycle validation for archived writing, including the former publication URL, publication date, archive date, and archive reason. + +### Changed + +- Daily Note and working-note workflows were refined around stable focus commitments, durable working-note status, and clearer operational continuity. + +## [2026-07-12] + +### Added + +- Markdown-to-PDF skill and managed Markdown-to-DOCX deliverable generation. +- Working-note template and workspace conventions for retained operational research, plans, and decision support. + +### Changed + +- Daily Note rollover and framework export checks were strengthened to preserve continuity and public-safe framework output. + +## [2026-07-06] + +### Added + +- Managed `writing/` and `documents/` workspaces, including draft, ready, final, published, and deliverable lifecycle rules. +- DTM and voice skills, writing and document lint tools, reusable writing/document templates, and scheduled drafting and publishing automation definitions. +- Thread-scoped DTM sessions and a durable queue for wiki open questions. + +### Changed + +- The publication boundary now excludes personal writing and documents as well as Daily Notes, wiki content, and other live-vault material. + +## [2026-06-23] + +### Added + +- Initial Knowledge Agent and Digital TeamMate framework. +- Allowlist-only publication boundary, repeatable installer, sanitized automation definitions, and empty-vault example. diff --git a/framework/AGENTS.md b/framework/AGENTS.md index f51760a..ba02787 100644 --- a/framework/AGENTS.md +++ b/framework/AGENTS.md @@ -158,6 +158,16 @@ compound across sessions. documents, such as `.pptx`, `.docx`, and `.xlsx`. Keep deliverables grouped under a folder named for the document slug where practical, for example `documents/deliverables/data-strategy-for-social-impact/`. +- Markdown remains the canonical source of truth for every authored document + and slide deck throughout its lifecycle. Draft and final Markdown files are + the maintained copies. Exported `.pptx`, PDF, HTML, image, Word, spreadsheet, + and speaker-note files are generated deliverables only; promoting a Markdown + source to final status does not make an exported file primary. +- Marp slide decks are canonical Markdown sources. Keep active deck sources + under `documents/drafts//.marp.md` and approved final + deck sources under `documents/final//.marp.md`. + Generate slide outputs under `documents/deliverables//exports/` + only when the user asks for a deliverable pass. - When a Markdown document needs a managed Word copy, prefer `python3 tools/document_deliverables.py docx ` and visually QA the output by rendering page images before treating the deliverable as ready @@ -166,13 +176,16 @@ compound across sessions. document is also used as wiki evidence, retain the raw-source copy under `raw/` or `raw/processed/` as immutable provenance and manage the authored working copy separately under `documents/`. -- `writing/voice/voice-pack.md` is also the private style model for internal - document drafting. Voice analysis may read only reader-facing prose in - `writing/ready/`, `writing/published/`, and `documents/final/`; it must never - inspect document drafts or use chats, Daily Notes, wiki pages, sources, - projects, or scratch material as style evidence. -- Before creating or materially rewriting a document draft, read the voice pack - when it exists and apply its guidance without caricaturing the user. +- `documents/voice/document-voice-pack.md` is the private style model for + internal document drafting. `writing/voice/anti-ai-style-rules.md` is the + shared protected avoidance layer for both writing and documents. Voice + analysis may read only reader-facing prose in `writing/ready/`, + `writing/published/`, and `documents/final/`; it must never inspect document + drafts or use chats, Daily Notes, wiki pages, sources, projects, or scratch + material as style evidence. +- Before creating or materially rewriting a document draft, read the document + voice pack and anti-AI style rules when they exist and apply their guidance + without caricaturing the user. - Run `python3 tools/documents.py lint` after structural or status changes. ### `writing/` — collaborative writing workspace @@ -205,14 +218,17 @@ compound across sessions. - Publish only the content between `` and ``. Editorial briefs, source notes, and revision history remain private working metadata unless the user explicitly includes them. -- `writing/voice/voice-pack.md` is a private style model maintained by the - `$voice` skill. Voice analysis may read only reader-facing prose in - `writing/ready/`, `writing/published/`, and `documents/final/`; it must never - inspect drafts or use chats, Daily Notes, wiki pages, sources, projects, or - scratch material as style evidence. -- Before creating or materially rewriting a draft, read the voice pack when it - exists and apply its guidance without caricaturing the user. Preserve the - user's current brief and explicit instructions over inferred style rules. +- `writing/voice/blog-voice-pack.md` is the private style model for public + writing maintained by the `$voice` skill. `writing/voice/voice-pack.md` is a + compatibility mirror for older artefacts. `writing/voice/anti-ai-style-rules.md` + is the shared protected avoidance layer. Voice analysis may read only + reader-facing prose in `writing/ready/`, `writing/published/`, and + `documents/final/`; it must never inspect drafts or use chats, Daily Notes, + wiki pages, sources, projects, or scratch material as style evidence. +- Before creating or materially rewriting a draft, read the blog voice pack and + anti-AI style rules when they exist and apply their guidance without + caricaturing the user. Preserve the user's current brief and explicit + instructions over inferred style rules. - Run `python3 tools/writing.py lint` after structural or status changes. ### `AGENTS.md` — shared schema diff --git a/framework/DTM.md b/framework/DTM.md index 4add3ef..c6a3472 100644 --- a/framework/DTM.md +++ b/framework/DTM.md @@ -137,10 +137,10 @@ only after confirmed success. Apply all detailed status and metadata rules in `writing/README.md`. Before creating or materially rewriting reader-facing prose, read -`writing/voice/voice-pack.md` when present. Treat it as guidance rather than a -formula: the current brief and direct user instructions take precedence. Only -the `$voice` workflow may learn style from the approved corpus, and it must -exclude drafts completely. +`writing/voice/blog-voice-pack.md` and `writing/voice/anti-ai-style-rules.md` +when present. Treat them as guidance rather than a formula: the current brief +and direct user instructions take precedence. Only the `$voice` workflow may +learn style from the approved corpus, and it must exclude drafts completely. ## Collaborative internal documents @@ -166,6 +166,12 @@ document workspace item. Treat the Markdown file in `documents/drafts/` or `documents/final/` as the editable canonical source when one exists, and treat the `.pptx`, `.docx`, or `.xlsx` file as the distributed artefact. +The same rule applies to slide decks. A Marp `.marp.md` deck remains the +canonical source in draft and final states. Generate `.pptx`, PDF, HTML, +image, and speaker-note exports only when the user asks for a deliverable pass; +do not create or promote exported slide files merely because a Markdown deck +has been created or revised. + When generating a managed `.docx` deliverable from a Markdown source, prefer `python3 tools/document_deliverables.py docx `, then render the result and visually inspect the page PNGs before treating it as ready to share. @@ -175,8 +181,9 @@ in `raw/` or `raw/processed/` unchanged as provenance and maintain the authored copy separately under `documents/`. Before creating or materially rewriting an internal document, read -`writing/voice/voice-pack.md` when present. The `$voice` workflow may learn -from `documents/final/` as part of the approved corpus, but must never inspect +`documents/voice/document-voice-pack.md` and +`writing/voice/anti-ai-style-rules.md` when present. The `$voice` workflow may +learn from `documents/final/` for the document pack, but must never inspect `documents/drafts/`. If the user says they have edited a managed document directly in Obsidian or diff --git a/framework/automation-definitions/weekly-diary-draft.json b/framework/automation-definitions/weekly-diary-draft.json index e3af5ad..7c7fe62 100644 --- a/framework/automation-definitions/weekly-diary-draft.json +++ b/framework/automation-definitions/weekly-diary-draft.json @@ -4,5 +4,5 @@ "schedule": "01:30 every Monday in the user's local timezone", "execution_environment": "local", "workspace": "{{VAULT_PATH}}", - "prompt": "Read AGENTS.md, writing/README.md, and writing/voice/voice-pack.md. Determine the most recent complete Monday-Sunday week from daily/YYYY-MM-DD.md notes. Use every Daily Note from that week as the primary source of truth. Review the previous week's diary post in writing/published/ if one exists; if none exists, proceed without continuity review. Consult wiki/index.md only when extra context would materially improve one or two chosen themes, and only load the minimum relevant wiki pages. Write a conversational, reflective weekly diary post that summarises the week, explores one or two interesting developments in more depth, and closes by looking ahead. Save or update the draft at writing/drafts/diary--week-.md using the writing template conventions, update writing/index.md so the draft is listed exactly once, run python3 tools/writing.py lint, and report any unresolved blocker without touching writing/ready/ or writing/published/." + "prompt": "Read AGENTS.md, writing/README.md, writing/voice/blog-voice-pack.md, and writing/voice/anti-ai-style-rules.md. Determine the most recent complete Monday-Sunday week from daily/YYYY-MM-DD.md notes. Use every Daily Note from that week as the primary source of truth. Review the previous week's diary post in writing/published/ if one exists; if none exists, proceed without continuity review. Consult wiki/index.md only when extra context would materially improve one or two chosen themes, and only load the minimum relevant wiki pages. Write a conversational, reflective weekly diary post that summarises the week, explores one or two interesting developments in more depth, and closes by looking ahead. Save or update the draft at writing/drafts/diary--week-.md using the writing template conventions, update writing/index.md so the draft is listed exactly once, run python3 tools/writing.py lint, and report any unresolved blocker without touching writing/ready/ or writing/published/." } diff --git a/framework/defaults/documents/README.md b/framework/defaults/documents/README.md index 090086b..f187f7f 100644 --- a/framework/defaults/documents/README.md +++ b/framework/defaults/documents/README.md @@ -13,10 +13,15 @@ here under `documents/`. ## Voice pack -`../writing/voice/voice-pack.md` is the private style guide shared across -writing and internal documents. Invoke `$voice` to learn from approved prose in -`../writing/ready/`, `../writing/published/`, and `final/`. The skill never -reads `drafts/`. +`voice/document-voice-pack.md` is the canonical private guide to the user's +internal document style. It is generated from approved reader-facing prose in +`final/`. `../writing/voice/blog-voice-pack.md` is generated separately from +public writing, and `../writing/voice/anti-ai-style-rules.md` supplies the +protected anti-AI avoidance layer shared by both writing modes. + +Invoke `$voice` to refresh the blog and document voice packs. The skill never +reads `drafts/` or `../writing/drafts/`, and it must not learn from chats, +Daily Notes, wiki pages, raw sources, projects, or scratch material. ## Lifecycle @@ -73,7 +78,7 @@ updated: YYYY-MM-DD audience: human_author: ai_assistance: true -voice_pack: writing/voice/voice-pack.md +voice_pack: documents/voice/document-voice-pack.md origin_path: tags: - document @@ -119,4 +124,63 @@ The current supported path is: - Markdown document to Word `.docx` +## Marp slide decks + +Use Marp Markdown as the canonical source for slide decks that should remain easy to edit in Obsidian and revise with Codex. This remains true throughout the whole lifecycle: draft, final, published, and later revised deck sources are Markdown-first. Exported `.pptx`, PDF, HTML, image, and speaker-note files are generated deliverables only; promoting a deck to a final version does not make an exported file the primary copy. + +The preferred Obsidian plugin is `Marp Slides Presenter` (`marp-slides-presenter`), configured to use the local Marp CLI, Chrome launch wrapper, and `templates/marp-themes` as the custom theme folder. + +Keep active draft deck sources under: + +```text +documents/drafts//.marp.md +``` + +When a deck is approved as final, move the Markdown source folder to: + +```text +documents/final//.marp.md +``` + +Create exported files only when the Markdown source has been reviewed and the user asks for a deliverable pass. Generated PDFs, PPTX files, speaker-note exports, and deck-local export artefacts belong under: + +```text +documents/deliverables//exports/ +``` + +Keep deck-local images, diagrams, and style overrides beside the Markdown source while the deck is still being edited: + +```text +documents/drafts//assets/ +documents/final//assets/ +``` + +Start new decks from `templates/marp-deck.md`. Use `templates/marp-themes/secondbrain.css` as the default theme unless a branded or event-specific deck needs a different visual treatment. + +Speaker notes belong in HTML comments on the slide they support: + +```markdown + +``` + +Prefer standard Markdown image links over Obsidian embeds inside decks, for example: + +```markdown +![Delegation modes](assets/diagrams/delegation-modes.svg) +``` + +Use SVG or PNG assets for important visuals. Mermaid is acceptable for rough drafting, but important conference visuals should be exported to SVG or PNG before final delivery so Obsidian preview, PDF export, and PowerPoint export all use the same asset. Keep `MarkdownIt Plugins` disabled in the presenter plugin unless there is a deliberate need for Kroki-backed diagram rendering. + +Deck export: + +```sh +python3 tools/decks.py check +python3 tools/decks.py export documents/drafts/example-deck/example-deck.marp.md +``` + +The helper exports PDF, image-based PPTX, and speaker notes by default. Editable PPTX remains a special-case path because Marp's editable export has lower fidelity and does not preserve presenter notes reliably. + After generating a deliverable, render it through the bundled document-skill renderer and visually inspect the PNG output before treating it as ready to share. The current operating note for this workflow lives in `../work/document-deliverables-workflow.md`. diff --git a/framework/publication-manifest.json b/framework/publication-manifest.json index 3feb21b..111c8be 100644 --- a/framework/publication-manifest.json +++ b/framework/publication-manifest.json @@ -10,6 +10,8 @@ {"source": "tools/dtm.py", "destination": "framework/tools/dtm.py"}, {"source": "tools/documents.py", "destination": "framework/tools/documents.py"}, {"source": "tools/document_deliverables.py", "destination": "framework/tools/document_deliverables.py"}, + {"source": "tools/decks.py", "destination": "framework/tools/decks.py"}, + {"source": "tools/marp-chrome", "destination": "framework/tools/marp-chrome"}, {"source": "tools/writing.py", "destination": "framework/tools/writing.py"}, {"source": "tools/export_framework.py", "destination": "framework/tools/export_framework.py"}, {"source": "tools/publish_framework.py", "destination": "framework/tools/publish_framework.py"}, diff --git a/framework/skills/voice/SKILL.md b/framework/skills/voice/SKILL.md index e690f2b..d4f42b2 100644 --- a/framework/skills/voice/SKILL.md +++ b/framework/skills/voice/SKILL.md @@ -1,50 +1,89 @@ --- name: voice -description: Analyse the user's approved writing and finished internal documents and create or update the private SecondBrain writing voice pack. Use only when the user invokes `$voice` or explicitly asks to learn, refresh, or inspect their writing style from `writing/ready/`, `writing/published/`, and `documents/final/`; never learn from drafts, chats, notes, wiki pages, sources, or other vault content. +description: Analyse the user's approved writing and finished internal documents and create or update private SecondBrain voice packs. Use only when the user invokes `$voice` or explicitly asks to learn, refresh, or inspect their writing style from `writing/ready/`, `writing/published/`, and `documents/final/`; never learn from drafts, chats, notes, wiki pages, sources, or other vault content. --- -# Writing voice analysis +# Voice analysis -Build an evidence-based style model at `writing/voice/voice-pack.md` without -reading `writing/drafts/` or `documents/drafts/`. +Build evidence-based style models without reading `writing/drafts/` or +`documents/drafts/`. + +Generated outputs: + +- `writing/voice/blog-voice-pack.md` — public/blog writing voice learned from + `writing/ready/` and `writing/published/`. +- `documents/voice/document-voice-pack.md` — internal document voice learned + from `documents/final/`. +- `writing/voice/voice-pack.md` — compatibility mirror of the blog voice pack + for older templates and artefacts that still point at this path. + +Protected input: + +- `writing/voice/anti-ai-style-rules.md` — shared anti-AI avoidance rules that + apply to both generated packs and must not be rewritten by `$voice` unless + the user explicitly asks to change that file. ## Analyse the corpus -1. Confirm the working directory contains `AGENTS.md`, `writing/README.md`, and - the writing workspace. Read those contracts first. Never inspect - `scratch.md`. -2. Read the existing voice pack when present. Preserve its **Declared - preferences** section verbatim unless the user explicitly asks to change it. -3. Enumerate Markdown files only under `writing/ready/`, +1. Confirm the working directory contains `AGENTS.md`, `writing/README.md`, + `documents/README.md`, and the writing/document workspaces. Read those + contracts first. Never inspect `scratch.md`. +2. Read `writing/voice/anti-ai-style-rules.md` when present. Treat it as a + protected rule input, not as style evidence. Its avoidance rules have higher + authority than learned patterns from AI-assisted approved prose. +3. Read the existing generated voice packs when present. Preserve useful + historical nuance and any **Declared preferences** section verbatim unless + the user explicitly asks to change it. +4. Enumerate Markdown files only under `writing/ready/`, `writing/published/`, and `documents/final/`. Do not list, search, count, or read files under `writing/drafts/` or `documents/drafts/`. -4. Analyse only reader-facing prose. For writing pieces, use only content +5. Analyse only reader-facing prose. For writing pieces, use only content between `` and ``. For internal documents in `documents/final/`, analyse the reader-facing body while ignoring private notes, metadata, and drafting residue. -5. Weight published work more strongly than ready work, and weight approved - final internal documents between those two levels unless the user asks - otherwise. Separate stable habits seen across pieces from topic, format, or - genre-specific choices. Do not infer a confident rule from a single example. -6. Extract actionable patterns for tone, stance, syntax and rhythm, diction, - structure, transitions, openings, closings, rhetorical habits, formatting, - and characteristic things to avoid. Use short examples only when they add - precision; link them to the eligible writing file. - -## Update the voice pack - -- Create the pack from `templates/voice-pack.md` if absent; otherwise update it - in place without erasing declared preferences or useful historical nuance. +6. Analyse blog and document corpora separately. Weight published blog work + more strongly than ready blog work. Use final internal documents only for + the document pack. Separate stable habits seen across pieces from topic, + format, or genre-specific choices. Do not infer a confident rule from a + single example. +7. Extract actionable positive style patterns for each pack: tone, stance, + syntax and rhythm, diction, structure, transitions, openings, closings, + rhetorical habits, formatting, and context variations. Use short examples + only when they add precision; link them to eligible writing or final + document files. +8. Incorporate the shared anti-AI rules into both generated packs. If corpus + evidence appears to conflict with those rules, report the tension and keep + the learned pattern only when it is clearly purposeful, specific, and + consistent with the user's authored judgement. + +## Update the voice packs + +- Create missing packs from `templates/voice-pack.md` as a structural starting + point; otherwise update them in place without erasing useful historical + nuance. +- Before writing a newly generated pack over an existing pack, save the current + file as the corresponding single rollback copy: + `writing/voice/blog-voice-pack.backup.md`, + `documents/voice/document-voice-pack.backup.md`, or + `writing/voice/voice-pack.backup.md` for the compatibility mirror. Keep only + one rollback copy per generated output. If the backup already exists, replace + it; do not keep a chain of historical backups. Treat each new `$voice` run as + confirmation that the current pack is satisfactory enough to become the new + rollback point. +- Regenerate `writing/voice/voice-pack.md` as a compatibility mirror of + `writing/voice/blog-voice-pack.md` until all writing templates and artefacts + have moved to the canonical blog pack path. - Keep observations concrete enough to guide drafting. Prefer “Use X under Y conditions” over vague labels such as “engaging” or “authentic.” - Record coverage, confidence, eligible source counts, an evidence ledger, and - what changed in this analysis pass. -- Mark the pack `provisional` when evidence is sparse or narrow. Never invent a + what changed in this analysis pass for each generated pack. +- Mark a pack `provisional` when evidence is sparse or narrow. Never invent a style to fill gaps; state what cannot yet be learned. - Append a `voice` entry to `writing/log.md`. If the current thread is an active - DTM session, also add the updated pack to today's Daily Note References and + DTM session, also add the updated packs to today's Daily Note References and record the action once. -- Run `python3 tools/writing.py lint` before reporting completion. +- Run `python3 tools/writing.py lint` and `python3 tools/documents.py lint` + before reporting completion. If there is no eligible corpus, create or retain a provisional empty pack, report that no style was inferred, and suggest moving representative human- diff --git a/framework/templates/document-piece.md b/framework/templates/document-piece.md index 9e3e7fe..9f55e12 100644 --- a/framework/templates/document-piece.md +++ b/framework/templates/document-piece.md @@ -7,7 +7,7 @@ updated: {{date:YYYY-MM-DD}} audience: human_author: ai_assistance: true -voice_pack: writing/voice/voice-pack.md +voice_pack: documents/voice/document-voice-pack.md origin_path: tags: - document diff --git a/framework/templates/marp-deck.md b/framework/templates/marp-deck.md new file mode 100644 index 0000000..677fe5e --- /dev/null +++ b/framework/templates/marp-deck.md @@ -0,0 +1,66 @@ +--- +marp: true +theme: secondbrain +paginate: true +size: 16:9 +title: Deck title +description: +author: +--- + + + +# Deck Title + +Short subtitle or event context + + + +--- + +## Section Or Slide Title + +- One clear point +- One supporting detail +- One next thought + + + +--- + +## Visual Slide + +Use a real image, diagram, or SVG asset here when the slide needs a visual. + + + + + +--- + +## Closing Thought + +What should the audience do differently after this talk? + + diff --git a/framework/templates/marp-themes/secondbrain.css b/framework/templates/marp-themes/secondbrain.css new file mode 100644 index 0000000..92b9209 --- /dev/null +++ b/framework/templates/marp-themes/secondbrain.css @@ -0,0 +1,506 @@ +/*! + * @theme secondbrain + * + * A restrained Marp theme for SecondBrain-authored conference and internal decks. + */ + +@import "default"; + +:root { + --sb-ink: #17202a; + --sb-muted: #5b6470; + --sb-bg: #f8f7f3; + --sb-panel: #ffffff; + --sb-rule: #d6d3cb; + --sb-accent: #246b73; + --sb-accent-2: #a23e48; + --sb-warm: #d49a3a; +} + +section { + background: var(--sb-bg); + color: var(--sb-ink); + font-family: "Aptos", "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-size: 29px; + letter-spacing: 0; + padding: 58px 68px; +} + +section::after { + color: var(--sb-muted); + font-size: 18px; + right: 34px; + bottom: 24px; +} + +h1, +h2, +h3 { + color: var(--sb-ink); + font-weight: 720; + letter-spacing: 0; + line-height: 1.08; +} + +h1 { + font-size: 58px; + max-width: 900px; +} + +h2 { + font-size: 43px; + margin-bottom: 24px; +} + +h3 { + font-size: 31px; + margin-top: 20px; +} + +p, +li { + line-height: 1.28; +} + +p { + max-width: 940px; +} + +strong { + color: var(--sb-accent); +} + +blockquote { + border-left: 8px solid var(--sb-accent); + color: var(--sb-ink); + font-size: 34px; + margin: 0; + padding: 12px 0 12px 28px; +} + +table { + border-collapse: collapse; + font-size: 24px; + width: 100%; +} + +th { + color: var(--sb-ink); + font-weight: 700; +} + +td, +th { + border-bottom: 1px solid var(--sb-rule); + padding: 12px 10px; +} + +code { + background: #ebe8df; + color: var(--sb-ink); + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} + +img { + max-height: 520px; +} + +section.lead { + background: var(--sb-ink); + color: var(--sb-bg); +} + +section.lead h1, +section.lead h2, +section.lead h3, +section.lead strong { + color: var(--sb-bg); +} + +section.lead p { + color: #e5e0d6; +} + +section.section { + background: var(--sb-accent); + color: #ffffff; +} + +section.section h1, +section.section h2, +section.section h3, +section.section strong { + color: #ffffff; +} + +section.two-column { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 48px; + align-items: start; +} + +section.two-column h2 { + grid-column: 1 / -1; +} + +section.visual h2 { + margin-bottom: 18px; +} + +.sb-flow { + align-items: center; + display: grid; + gap: 14px; + grid-template-columns: repeat(5, 1fr); + margin-top: 38px; +} + +.sb-flow--wide { + grid-template-columns: repeat(4, 1fr); +} + +.sb-step, +.sb-card, +.sb-tile, +.sb-question, +.sb-example-card { + background: var(--sb-panel); + border: 2px solid var(--sb-rule); + border-radius: 8px; + box-shadow: 0 8px 22px rgba(23, 32, 42, 0.06); + padding: 18px; +} + +.sb-step { + min-height: 96px; + text-align: center; +} + +.sb-step strong, +.sb-card strong, +.sb-tile strong { + color: var(--sb-ink); + display: block; + font-size: 26px; + margin-bottom: 8px; +} + +.sb-step span, +.sb-card span, +.sb-tile span, +.sb-question span, +.sb-example-card span { + color: var(--sb-muted); + display: block; + font-size: 18px; + line-height: 1.22; +} + +.sb-step--active { + background: #e2f0f1; + border-color: var(--sb-accent); +} + +.sb-step--risk { + background: #faeee7; + border-color: #dc7c63; +} + +.sb-arrow { + color: var(--sb-muted); + font-size: 34px; + text-align: center; +} + +.sb-contrast { + display: grid; + gap: 30px; + grid-template-columns: 0.82fr 1.18fr; + margin-top: 34px; +} + +.sb-contrast .sb-card { + min-height: 210px; +} + +.sb-contrast + p { + margin-top: 28px; +} + +.sb-card--weak { + border-color: #dc7c63; +} + +.sb-card--strong { + border-color: var(--sb-accent); +} + +.sb-card__label { + color: var(--sb-muted); + font-size: 20px; + font-weight: 700; + letter-spacing: 0.04em; + margin-bottom: 16px; + text-transform: uppercase; +} + +.sb-card__question { + color: var(--sb-ink); + font-size: 34px; + font-weight: 720; + line-height: 1.14; +} + +.sb-bars { + display: grid; + gap: 28px; + margin-top: 34px; +} + +.sb-bar { + align-items: center; + display: grid; + gap: 18px; + grid-template-columns: 230px 1fr 190px; +} + +.sb-bar__label, +.sb-bar__note { + color: var(--sb-ink); + font-size: 23px; + font-weight: 700; +} + +.sb-bar__track { + background: #ebe8df; + border-radius: 999px; + height: 34px; + overflow: hidden; +} + +.sb-bar__fill { + background: var(--sb-accent); + border-radius: 999px; + height: 100%; +} + +.sb-bar__fill--warm { + background: var(--sb-warm); +} + +.sb-bar__fill--risk { + background: #dc7c63; +} + +.sb-fill-38 { + width: 38%; +} + +.sb-fill-74 { + width: 74%; +} + +.sb-fill-92 { + width: 92%; +} + +.sb-canvas { + display: grid; + gap: 18px; + grid-template-columns: repeat(2, 1fr); + margin-top: 26px; +} + +.sb-question { + border-left: 10px solid var(--sb-accent); + min-height: 118px; +} + +.sb-question strong { + color: var(--sb-ink); + display: block; + font-size: 27px; + line-height: 1.14; +} + +.sb-number { + color: var(--sb-accent); + display: block; + font-size: 18px; + font-weight: 800; + margin-bottom: 8px; +} + +.sb-brief { + display: grid; + gap: 16px; + grid-template-columns: repeat(4, 1fr); + margin-top: 30px; +} + +.sb-brief .sb-tile { + min-height: 138px; +} + +.sb-before-after { + align-items: stretch; + display: grid; + gap: 20px; + grid-template-columns: 1fr 84px 1.25fr; + margin-top: 34px; +} + +.sb-before-after .sb-arrow { + align-self: center; +} + +.sb-check-grid { + display: grid; + gap: 18px; + grid-template-columns: repeat(5, 1fr); + margin-top: 32px; +} + +.sb-check-grid .sb-tile { + min-height: 150px; +} + +.sb-scale { + display: grid; + gap: 18px; + grid-template-columns: repeat(4, 1fr); + margin-top: 28px; +} + +.sb-scale .sb-card { + min-height: 190px; +} + +.sb-card--go { + border-top: 10px solid #4f8f65; +} + +.sb-card--checkpoint { + border-top: 10px solid var(--sb-warm); +} + +.sb-card--support { + border-top: 10px solid var(--sb-accent); +} + +.sb-card--stop { + border-top: 10px solid #a23e48; +} + +.sb-examples { + display: grid; + gap: 18px; + grid-template-columns: repeat(4, 1fr); + margin-top: 28px; +} + +.sb-example-card { + min-height: 230px; +} + +.sb-example-card strong { + color: var(--sb-ink); + display: block; + font-size: 25px; + margin-bottom: 12px; +} + +.sb-badge { + border-radius: 999px; + color: #ffffff; + display: inline-block; + font-size: 16px; + font-weight: 800; + margin-top: 18px; + padding: 7px 11px; +} + +.sb-example-card .sb-badge { + color: #ffffff; + display: inline-block; + font-size: 16px; +} + +.sb-badge--go { + background: #4f8f65; +} + +.sb-badge--checkpoint { + background: var(--sb-warm); +} + +.sb-badge--support { + background: var(--sb-accent); +} + +.sb-badge--stop { + background: #a23e48; +} + +.sb-network { + display: grid; + gap: 16px; + grid-template-columns: repeat(5, 1fr); + margin-top: 34px; +} + +.sb-network .sb-tile { + min-height: 155px; + position: relative; +} + +.sb-network .sb-tile::after { + color: var(--sb-muted); + content: "->"; + font-size: 28px; + position: absolute; + right: -26px; + top: 62px; +} + +.sb-network .sb-tile:last-child::after { + content: ""; +} + +.sb-split { + display: grid; + gap: 34px; + grid-template-columns: 1fr 1fr; + margin-top: 28px; +} + +.sb-list-card { + background: var(--sb-panel); + border: 2px solid var(--sb-rule); + border-radius: 8px; + padding: 24px 28px; +} + +.sb-list-card h3 { + margin-top: 0; +} + +.sb-list-card li { + font-size: 22px; +} + +.sb-pull { + color: var(--sb-ink); + font-size: 45px; + font-weight: 760; + line-height: 1.12; + margin-top: 72px; + max-width: 950px; +} + +.sb-caption { + color: var(--sb-muted); + font-size: 21px; + margin-top: 18px; +} diff --git a/framework/templates/voice-pack.md b/framework/templates/voice-pack.md index 0c1861f..1f6851d 100644 --- a/framework/templates/voice-pack.md +++ b/framework/templates/voice-pack.md @@ -4,9 +4,11 @@ type: voice-pack status: provisional created: YYYY-MM-DD updated: YYYY-MM-DD +profile: generic ready_sources: 0 published_sources: 0 document_sources: 0 +anti_ai_rules: writing/voice/anti-ai-style-rules.md tags: - writing - voice @@ -20,11 +22,18 @@ Apply these patterns as flexible guidance. The current editorial brief and direct user instructions take precedence. Do not exaggerate traits into a caricature. +Read the shared anti-AI style rules before drafting or regenerating this pack. +Those rules are protected inputs, not learned style evidence. + ## Confidence and coverage No approved writing or final internal documents have been analysed yet. The pack is provisional. +Before overwriting an existing generated voice pack, copy it to the single +rollback backup for that profile. Replace the previous backup; do not keep a +chain of historical backups. + ## Declared preferences @@ -57,7 +66,7 @@ Insufficient evidence. ## Avoid -No evidence-based avoidances identified yet. +Apply `writing/voice/anti-ai-style-rules.md` as the shared avoidance layer. ## Context variations @@ -66,6 +75,8 @@ No genre-specific variations identified yet. ## Drafting checklist - Follow the current brief before inferred tendencies. +- Preserve declared preferences verbatim during regeneration. +- Keep anti-AI rules as durable constraints, not as positive style evidence. - Recheck this pack after representative approved writing is added. ## Evidence ledger diff --git a/framework/templates/writing-piece.md b/framework/templates/writing-piece.md index 14b26a2..a6db70a 100644 --- a/framework/templates/writing-piece.md +++ b/framework/templates/writing-piece.md @@ -10,7 +10,7 @@ canonical_url: published_at: human_author: ai_assistance: true -voice_pack: writing/voice/voice-pack.md +voice_pack: writing/voice/blog-voice-pack.md tags: - writing --- diff --git a/framework/tools/decks.py b/framework/tools/decks.py new file mode 100644 index 0000000..6a3f810 --- /dev/null +++ b/framework/tools/decks.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Export and check Marp slide decks managed inside the vault.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_THEME = ROOT / "templates" / "marp-themes" / "secondbrain.css" +DEFAULT_CHROME_CANDIDATES = [ + ROOT / "tools" / "marp-chrome", + Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"), + Path("/opt/homebrew/bin/chromium"), + Path("/usr/local/bin/chromium"), + Path("/Applications/Chromium.app/Contents/MacOS/Chromium"), +] + + +def relative(path: Path) -> str: + try: + return path.resolve().relative_to(ROOT).as_posix() + except ValueError: + return path.as_posix() + + +def command_path(name: str) -> str | None: + return shutil.which(name) + + +def default_chrome_path() -> str | None: + configured = os.environ.get("CHROME_PATH") + if configured and Path(configured).exists(): + return configured + for candidate in DEFAULT_CHROME_CANDIDATES: + if candidate.exists(): + return str(candidate) + return None + + +def default_export_dir(source: Path) -> Path: + resolved = source.resolve() + try: + rel = resolved.relative_to(ROOT / "documents" / "deliverables") + except ValueError: + rel = None + + if rel and len(rel.parts) >= 2: + return ROOT / "documents" / "deliverables" / rel.parts[0] / "exports" + + slug = source.stem.removesuffix(".marp") + return ROOT / "documents" / "deliverables" / slug / "exports" + + +def marp_command(source: Path, output: Path, theme: Path, extra: list[str] | None = None) -> list[str]: + chrome_path = default_chrome_path() + command = [ + "marp", + str(source), + "--allow-local-files", + "--theme-set", + str(theme), + "-o", + str(output), + ] + if chrome_path: + command[1:1] = ["--browser", "chrome", "--browser-path", chrome_path] + if extra: + command[1:1] = extra + return command + + +def run(command: list[str]) -> None: + print("$ " + " ".join(command)) + env = os.environ.copy() + chrome_path = default_chrome_path() + if chrome_path and "CHROME_PATH" not in env: + env["CHROME_PATH"] = chrome_path + subprocess.run(command, cwd=ROOT, check=True, env=env) + + +def check(_args: argparse.Namespace) -> int: + checks = { + "node": command_path("node"), + "npm": command_path("npm"), + "marp": command_path("marp"), + "chromium": command_path("chromium"), + "google-chrome-cli": command_path("google-chrome"), + "google-chrome-app": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + if Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome").exists() + else None, + "soffice": command_path("soffice"), + "libreoffice": command_path("libreoffice"), + } + for name, path in checks.items(): + print(f"{name}: {path or 'missing'}") + print(f"CHROME_PATH: {default_chrome_path() or 'missing'}") + if not checks["marp"]: + print("ERROR marp CLI is missing; install @marp-team/marp-cli before exporting decks.") + return 1 + return 0 + + +def export(args: argparse.Namespace) -> int: + source = (ROOT / args.source).resolve() if not Path(args.source).is_absolute() else Path(args.source).resolve() + if not source.exists(): + print(f"ERROR source not found: {relative(source)}") + return 1 + if command_path("marp") is None: + print("ERROR marp CLI is missing; install @marp-team/marp-cli before exporting decks.") + return 1 + + theme = (ROOT / args.theme).resolve() if args.theme else DEFAULT_THEME + if not theme.exists(): + print(f"ERROR theme not found: {relative(theme)}") + return 1 + + output_dir = (ROOT / args.output_dir).resolve() if args.output_dir else default_export_dir(source) + output_dir.mkdir(parents=True, exist_ok=True) + slug = source.stem.removesuffix(".marp") + + formats = args.formats or ["pdf", "pptx", "notes"] + if "pdf" in formats: + run(marp_command(source, output_dir / f"{slug}.pdf", theme)) + if "pptx" in formats: + run(marp_command(source, output_dir / f"{slug}.pptx", theme)) + if "notes" in formats: + run(marp_command(source, output_dir / f"{slug}-speaker-notes.txt", theme, ["--notes"])) + + print(f"Exported to {relative(output_dir)}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + check_parser = sub.add_parser("check", help="check host dependencies for Marp deck export") + check_parser.set_defaults(func=check) + + export_parser = sub.add_parser("export", help="export a Marp deck to managed deliverables") + export_parser.add_argument("source", help="path to a .marp.md or Marp-enabled Markdown source") + export_parser.add_argument("--output-dir", help="override export output directory") + export_parser.add_argument("--theme", help="override Marp theme CSS path") + export_parser.add_argument( + "--formats", + nargs="+", + choices=["pdf", "pptx", "notes"], + help="formats to export; default: pdf pptx notes", + ) + export_parser.set_defaults(func=export) + + args = parser.parse_args() + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/framework/tools/document_deliverables.py b/framework/tools/document_deliverables.py index 1fc0bd0..0269cce 100644 --- a/framework/tools/document_deliverables.py +++ b/framework/tools/document_deliverables.py @@ -58,6 +58,7 @@ def preferred_runtime_site_packages() -> Path: OBSIDIAN_EMBED_RE = re.compile(r"!\[\[([^\]]+)\]\]") OBSIDIAN_LINK_RE = re.compile(r"(?]+)>|([^)]+))\)$") @dataclass @@ -343,6 +344,35 @@ def is_markdown_table(block: str) -> bool: return bool(separator_cells) and all(re.match(r"^:?-{3,}:?$", cell) for cell in separator_cells) +def markdown_image_target(block: str) -> str | None: + match = MARKDOWN_IMAGE_RE.match(block.strip()) + if not match: + return None + return (match.group(2) or match.group(3) or "").strip() + + +def resolve_markdown_image(raw_target: str, source: Path) -> Path | None: + target = raw_target.split("#", 1)[0].strip() + if not target: + return None + candidate = (source.parent / target).resolve() + if candidate.exists(): + return candidate + root_candidate = (ROOT / target).resolve() + if root_candidate.exists(): + return root_candidate + return None + + +def docx_image_path(path: Path) -> Path: + if path.suffix.lower() != ".svg": + return path + png_candidate = path.parent / "docx-png" / f"{path.stem}.png" + if png_candidate.exists(): + return png_candidate + return path + + def parse_markdown_table(block: str) -> tuple[list[str], list[list[str]]]: lines = [line.strip() for line in block.splitlines() if line.strip()] rows = [[cell.strip() for cell in line.strip("|").split("|")] for line in lines] @@ -473,17 +503,18 @@ def add_footer(doc: Document, title: str) -> None: set_font(page_run, "Calibri", 9, color="666666") -def add_title_block(doc: Document, title: str, audience: str, updated: str) -> None: +def add_title_block(doc: Document, title: str, subtitle: str = "") -> None: title_p = doc.add_paragraph(style="Title") title_p.alignment = WD_ALIGN_PARAGRAPH.LEFT style_paragraph(title_p, after=6, line=240) set_font(title_p.add_run(title), "Calibri", 18, bold=True, color="1F1F1F") - meta = doc.add_paragraph() - style_paragraph(meta, after=12, line=240) - meta.alignment = WD_ALIGN_PARAGRAPH.LEFT - meta_run = meta.add_run(f"Audience: {audience} | Updated: {updated}") - set_font(meta_run, "Calibri", 10, color="666666") + if subtitle: + subtitle_p = doc.add_paragraph() + style_paragraph(subtitle_p, after=14, line=252) + subtitle_p.alignment = WD_ALIGN_PARAGRAPH.LEFT + subtitle_run = subtitle_p.add_run(clean_inline(subtitle)) + set_font(subtitle_run, "Calibri", 11, italic=True, color="666666") def add_heading(doc: Document, title: str, level: int) -> None: @@ -511,13 +542,14 @@ def add_paragraph(doc: Document, text: str) -> None: def add_list(doc: Document, block: str, ordered: bool) -> None: - for line in block.splitlines(): + for idx, line in enumerate(block.splitlines(), start=1): item = re.sub(r"^(- |\d+\. )", "", line).strip() - para = doc.add_paragraph(style="List Number" if ordered else "List Bullet") + para = doc.add_paragraph(style="List Bullet" if not ordered else None) style_paragraph(para, after=6, line=280) para.paragraph_format.left_indent = Inches(0.25) para.paragraph_format.first_line_indent = Inches(-0.25) - run = para.add_run(clean_inline(item)) + prefix = f"{idx}. " if ordered else "" + run = para.add_run(prefix + clean_inline(item)) set_font(run, "Calibri", 11, color="000000") @@ -555,6 +587,20 @@ def add_markdown_table(doc: Document, block: str) -> None: style_paragraph(spacer, after=8, line=240) +def add_markdown_image(doc: Document, path: Path, alt_text: str = "") -> None: + paragraph = doc.add_paragraph() + paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + style_paragraph(paragraph, before=6, after=10, line=240) + run = paragraph.add_run() + run.add_picture(str(path), width=Inches(CONTENT_WIDTH_IN)) + if alt_text: + drawing = run._element.xpath(".//wp:inline") + if drawing: + doc_pr = drawing[0].find(qn("wp:docPr")) + if doc_pr is not None: + doc_pr.set("descr", alt_text) + + def add_summary_table(doc: Document, title: str, summary: str, recommendation: str) -> None: table = doc.add_table(rows=3, cols=2) table.style = "Table Grid" @@ -579,8 +625,13 @@ def add_summary_table(doc: Document, title: str, summary: str, recommendation: s def build_docx(source: Path, output: Path) -> Path: meta, raw_body = parse_frontmatter(source.read_text(encoding="utf-8")) body = publish_body(raw_body) + body = replace_obsidian_embeds(body, source) + body = replace_obsidian_links(body) + body = replace_obsidian_block_ids(body) sections = parse_sections(body) + subtitle = "" if sections and sections[0].level == 1: + subtitle = " ".join(sections[0].blocks).strip() sections = sections[1:] doc = Document() @@ -588,31 +639,26 @@ def build_docx(source: Path, output: Path) -> Path: set_page_geometry(doc) title = meta.get("title", source.stem.replace("-", " ").title()) - audience = meta.get("audience", "") - updated = meta.get("updated", "") - add_title_block(doc, title, audience, updated) + add_title_block(doc, title, subtitle) add_footer(doc, title) - summary_section = next((section for section in sections if section.title.lower() == "summary"), None) - recommendation = "" - for section in sections: - if section.title.lower() == "recommendation": - recommendation = " ".join(clean_inline(block) for block in section.blocks[:2]) - break - if summary_section: - summary = " ".join(clean_inline(block) for block in summary_section.blocks[:2]) - add_summary_table(doc, title, summary, recommendation or "Treat the staged model as a pilot and refine it with evidence.") - doc.add_paragraph() - for section in sections: lowered = section.title.lower() - if lowered in {"summary", "notes"}: + if lowered == "notes": continue if lowered == "document" and not section.blocks: continue if section.title and lowered != "document": add_heading(doc, section.title, section.level) for block in section.blocks: + image_target = markdown_image_target(block) + if image_target: + image_path = resolve_markdown_image(image_target, source) + if image_path is not None: + add_markdown_image(doc, docx_image_path(image_path), clean_inline(block)) + else: + add_paragraph(doc, block) + continue if is_markdown_table(block): add_markdown_table(doc, block) continue diff --git a/framework/tools/documents.py b/framework/tools/documents.py index 481bcef..0ece195 100644 --- a/framework/tools/documents.py +++ b/framework/tools/documents.py @@ -17,6 +17,19 @@ "drafts": "draft", "final": "final", } +DECK_REQUIRED = { + "marp", + "type", + "status", + "created", + "updated", + "title", + "author", + "human_author", + "ai_assistance", + "voice_pack", + "tags", +} REQUIRED = { "title", "type", @@ -30,6 +43,7 @@ "tags", } SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*\.md$") +DECK_SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*\.marp\.md$") DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") LINK_RE = re.compile(r"\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]") INDEX_ENTRY_RE = re.compile(r"^\s*-\s+\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]", re.M) @@ -46,6 +60,13 @@ def pieces() -> list[tuple[Path, str]]: return result +def deck_sources() -> list[tuple[Path, str]]: + result: list[tuple[Path, str]] = [] + for folder, status in STAGES.items(): + result.extend((path, status) for path in sorted((DOCUMENTS / folder).glob("*/*.marp.md"))) + return result + + def frontmatter(text: str) -> dict[str, object] | None: if not text.startswith("---\n"): return None @@ -82,7 +103,8 @@ def value(meta: dict[str, object], field: str) -> str: def lint() -> int: errors: list[str] = [] all_pieces = pieces() - names = Counter(path.name for path, _ in all_pieces) + all_decks = deck_sources() + names = Counter(path.name for path, _ in [*all_pieces, *all_decks]) deliverable_files = sorted( path for path in DELIVERABLES.rglob("*") @@ -100,7 +122,7 @@ def lint() -> int: else: index_text = "" index_links = [] - if all_pieces or deliverable_files or reference_files: + if all_pieces or all_decks or deliverable_files or reference_files: errors.append( "documents/index.md: missing curated index; create documents/index.md before managing documents, references, or deliverables" ) @@ -109,6 +131,40 @@ def lint() -> int: if count > 1: errors.append(f"duplicate document filename across stages: {name}") + for path, expected_status in all_decks: + label = relative(path) + text = path.read_text(encoding="utf-8") + meta = frontmatter(text) + if not DECK_SLUG_RE.fullmatch(path.name): + errors.append(f"{label}: deck filename must use lowercase kebab-case ending .marp.md") + if meta is None: + errors.append(f"{label}: missing or malformed frontmatter") + continue + missing = DECK_REQUIRED - meta.keys() + if missing: + errors.append(f"{label}: missing fields {', '.join(sorted(missing))}") + if str(meta.get("marp", "")).lower() != "true": + errors.append(f"{label}: marp must be true") + if meta.get("type") != "deck": + errors.append(f"{label}: type must be 'deck'") + if meta.get("status") != expected_status: + errors.append( + f"{label}: folder requires status {expected_status!r}, found {meta.get('status')!r}" + ) + for field in ("created", "updated"): + if not DATE_RE.fullmatch(value(meta, field)): + errors.append(f"{label}: {field} must be YYYY-MM-DD") + if not value(meta, "title"): + errors.append(f"{label}: title must not be empty") + if not value(meta, "voice_pack"): + errors.append(f"{label}: voice_pack must not be empty") + target = label.removesuffix(".md") + count = index_links.count(target) + if count == 0: + errors.append(f"{label}: missing from documents/index.md") + elif count > 1: + errors.append(f"{label}: listed {count} times in documents/index.md") + for path, expected_status in all_pieces: label = relative(path) text = path.read_text(encoding="utf-8") @@ -163,14 +219,19 @@ def lint() -> int: for item in sorted(set(errors)): print(f"ERROR {item}") - print(f"Checked {len(all_pieces)} document(s): {len(set(errors))} error(s), 0 warning(s).") + print( + f"Checked {len(all_pieces)} document(s) and {len(all_decks)} deck source(s): {len(set(errors))} error(s), 0 warning(s)." + ) return 1 if errors else 0 def status() -> int: counts = Counter(status for _, status in pieces()) + deck_counts = Counter(status for _, status in deck_sources()) print(f"Drafts: {counts['draft']}") print(f"Final: {counts['final']}") + print(f"Draft decks: {deck_counts['draft']}") + print(f"Final decks: {deck_counts['final']}") reference_count = sum( 1 for path in REFERENCE.rglob("*") diff --git a/framework/tools/dtm.py b/framework/tools/dtm.py index dff9091..fdd59ce 100644 --- a/framework/tools/dtm.py +++ b/framework/tools/dtm.py @@ -146,13 +146,11 @@ def set_previous_day_line(text: str, replacement: str) -> str: body = previous_section.group(2) updated_body = re.sub( r"^(Previous note: .+|No Daily Note exists for the previous calendar day\.)$", - replacement, + "", body, - count=1, flags=re.M, - ) - if updated_body == body: - updated_body = f"{replacement}\n\n{body.lstrip()}" if body.strip() else f"{replacement}\n" + ).lstrip("\n") + updated_body = f"{replacement}\n\n{updated_body}" if updated_body.strip() else f"{replacement}\n" return text[: previous_section.start(2)] + updated_body + text[previous_section.end(2) :] @@ -408,7 +406,7 @@ def append_activity(day: date, actor: str, message: str, timestamp: str | None = notes_lines = [line for line in notes_lines if line.strip()] notes_lines.append(entry) - updated_before = f"{notes_prefix}{notes_heading}\n{chr(10).join(notes_lines)}\n" + updated_before = f"{notes_prefix}{notes_heading}\n{chr(10).join(notes_lines)}\n\n" path.write_text(f"{updated_before}## Decisions{after}", encoding="utf-8") print(f"Updated {path.relative_to(vault_root())}") return True diff --git a/framework/tools/marp-chrome b/framework/tools/marp-chrome new file mode 100755 index 0000000..05de279 --- /dev/null +++ b/framework/tools/marp-chrome @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +exec "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + --user-data-dir="/private/tmp/secondbrain-marp-chrome-profile" \ + --no-first-run \ + --no-default-browser-check \ + "$@"