← Corpus / perplexed / issue
Partials and preambles for perplexed templates
- Path
- issues/Partials-And-Preambles-For-Perplexed-Templates.md
Partials and preambles for perplexed templates
Symptom
Shared guidance — mermaid syntax discipline, citation enforcement, image-placement directives, the editorial “anti-incumbent” stance — is duplicated across the four profile templates (concept-profile, vocabulary-profile, source-profile, toolkit-profile) AND/OR hardcoded in TypeScript as inlined constants. When the rule needs to change, every copy drifts.
Concrete recent example: a mermaid diagram emitted by a concept-profile run for Residual Learning in AI used unquoted parens inside node labels (F1[Layer(s) compute F(x)], H[Output H(x) = F(x) + x]), which broke mermaid’s parser in Obsidian. The fix — a “mermaid syntax discipline” checklist (quote labels with special chars, quote subgraph titles with ids, no bare LaTeX, allowed shapes, etc.) — got pasted into all four templates as a duplicated block. This is the exact failure mode that motivates extracting the rule into a single artifact.
The same shape applies to the three directives currently hardcoded in src/services/directoryTemplateService.ts:49-51:
INLINE_CITATION_DIRECTIVE— prepended to every system promptIMAGE_PLACEMENT_DIRECTIVE— appended to user prompt whenreturn-images: truebuildResearchFraming()— wraps the user skeleton with metadata + skeleton-follows-this framing
These are already preambles in spirit; they just live in code instead of vault-visible markdown.
Architecture review (so the proposal makes sense)
Source of truth at build time: src/docs/templates/*.md are bundled into main.js via esbuild’s text loader and imported statically by templateSeederService.ts.
Seeding: seedTemplatesIfMissing() writes the bundled defaults to settings.templatesRoot (this vault uses zz-cf-lib/templates/) on first run / when the folder is empty. README always seeded; the four templates only seeded into an empty folder so user edits are never clobbered.
Runtime: listTemplates() reads live from the vault, not from the bundle. Once seeded, zz-cf-lib/templates/*.md is authoritative. loadTemplate() parses each into a ```cft YAML config + system prompt and a user skeleton (up to the *** scratch terminator). applyTemplate() then:
interpolate(text, ctx)— mustache-style token replacement only:{{basename}},{{title}},{{frontmatter}},{{today}},{{frontmatter.X}}. No includes, no recursion, no async file reads (directoryTemplateService.ts:278-290).- Prepends the hardcoded
INLINE_CITATION_DIRECTIVEto the template system prompt. - Wraps the user skeleton with
buildResearchFraming()and optionallyIMAGE_PLACEMENT_DIRECTIVE. buildPayload→ POST to Perplexity. (Perplexity’s sonar endpoints are OpenAI-compatible text-only/chat/completions— no file-upload, no attachment API. Anything shared must be inlined as text.)
Why this isn’t just DRY hygiene
The user-facing intent matters: partials and preambles must be vault-visible files in zz-cf-lib/ so users can read, edit, and wikilink to them from notes, the same way they already do with templates. Hiding shared rules in plugin code makes them invisible to the human writing or debugging a generation. The “templates are vault files” decision is the load-bearing design choice this proposal extends, not subverts.
Proposed mechanism
Two complementary primitives — same underlying read-a-vault-file-and-splice-text-into-the-prompt — applied at different scopes.
Partial — {{include: name}}
Per-template, opt-in. A template references a partial by name; expansion happens before token interpolation so partials can themselves contain {{basename}} etc.
- New folder:
zz-cf-lib/partials/ - Syntax:
{{include: mermaid-discipline}}resolves tozz-cf-lib/partials/mermaid-discipline.md. Strip frontmatter from the included file, splice the body at the directive’s line. - Recursive expansion with depth limit (5) and cycle detection (
Set<string>of names being expanded). - Missing-file behavior: emit a visible inline marker
[[include: <name> — file not found]]instead of failing the run.
Preamble — auto-attached to system or user message
Plugin-wide, opt-in via settings. Replaces the three hardcoded directives.
- New folder:
zz-cf-lib/preambles/ - New settings:
preamblesRoot— defaultzz-cf-lib/preamblessystemPreambles: string[]— default["inline-citation"]userPreambles: { name: string; when: "always" | "return-images" }[]— default[{research-framing, always}, {image-placement, return-images}]
- Per-template override via
cftconfig keys:preambles: system: ["inline-citation", "house-rules"] # override defaults for this template skip-user: ["research-framing"] # opt out of one
Proposed vault layout
zz-cf-lib/
├── templates/ (existing — unchanged)
│ ├── concept-profile.md
│ ├── source-profile.md
│ ├── toolkit-profile.md
│ ├── vocabulary-profile.md
│ └── README.md
├── partials/ (new)
│ ├── mermaid-discipline.md
│ ├── editorial-stance-anti-incumbent.md
│ └── README.md
└── preambles/ (new)
├── inline-citation.md (replaces INLINE_CITATION_DIRECTIVE)
├── image-placement.md (replaces IMAGE_PLACEMENT_DIRECTIVE)
├── research-framing.md (replaces buildResearchFraming)
└── README.md
Three peer folders. Partials and preambles separate because they play different roles: partials are referenced explicitly by templates; preambles are wired in by settings and apply to every request.
Implementation plan (incremental, load-bearing first)
- Move the three hardcoded directives out of
directoryTemplateService.ts:49-51into bundled markdown files atsrc/docs/preambles/{inline-citation,image-placement,research-framing}.md. Keep them as fallback content when the vault copy is missing. - Create bundled partials at
src/docs/partials/mermaid-discipline.md(and any other rules currently duplicated across templates). - Extend
seedTemplatesIfMissingto also seedpartialsRootandpreamblesRootwith the bundled defaults — same idempotent rule (only seed when target folder is missing or empty). - Add
expandIncludes(app, text, partialsRoot, seen, depth)todirectoryTemplateService.ts— async, recursive, depth + cycle guarded. - Refactor
applyTemplateto:- read system + user preamble files (vault first, fall back to bundled defaults)
- run
expandIncludesoncftSystemanduserSkeleton - then
interpolateas today - assemble:
[system preambles joined] + templateSystemand[user preambles up to research-framing] + skeleton + [user preambles after, e.g. image-placement]
- Settings tab additions: path inputs for the two new roots; comma-separated lists for system / user preamble names with per-template-override docs.
- Update the four bundled templates to use
{{include: mermaid-discipline}}instead of the duplicated discipline block currently pasted into all four. Delete the duplicates.
Tradeoffs / decisions baked in
- Folder layout: three peers under
zz-cf-lib/instead of nestedzz-cf-lib/templates/_partials/. The peer layout is more discoverable in the Obsidian file tree and matches the conceptual separation. Easy to flip if it turns out to be the wrong call. - Async expansion:
interpolate()stays sync (pure string-replace). NewexpandIncludes()is async because it reads vault files.applyTemplateis already async — invisible to callers. - Bundle defaults, read from vault at runtime. Same pattern as templates today. Bundled copy = first-run safety net; vault copy = source of truth once seeded. User edits are sticky.
- Missing-file behavior asymmetry:
- Partial not found → visible inline marker
[[include: <name> — file not found]]. The user wrote the include explicitly; surface their typo. - Preamble not found → fall back to bundled default silently (with
console.warn). Preambles are infrastructure the user didn’t explicitly invoke from this template.
- Partial not found → visible inline marker
- No Perplexity-side attachment. The sonar endpoints don’t accept files or system-prompt attachments. Inlining text into
messagesis the only path; this proposal makes that inlining file-driven instead of code-driven.
Vault-seeding caveat that recurs
Until step 7 ships, existing vault templates still contain the duplicated mermaid-discipline block. The seeder’s “only seed empty folders” rule means an updated src/docs/templates/*.md won’t auto-replace what’s already in zz-cf-lib/templates/. To roll out template changes mid-design, the user must either copy bundle → vault manually or delete the vault copy and reload the plugin. Worth a short section in the README of zz-cf-lib/templates/ (and the future partials/ + preambles/ READMEs) describing this so the asymmetry isn’t surprising.
Open questions for follow-up
- Should partials support their own frontmatter (e.g.,
applies-to: [system, user]so an include is rejected when used in the wrong slot)? Probably no for v1 — partials are pure snippets — but worth revisiting if misuse appears. - Should the
cftconfig supportpreambles: skip-all: truefor a fully bespoke template that wants no global directives? Useful escape hatch; trivial to add. - Naming:
partialvsincludevssnippet. Current proposal usespartialfor the folder and{{include:}}for the directive (Liquid/Jekyll convention). Acceptable but worth flagging if a different vocabulary fits the rest of the plugin family.