From 11f0305ca937f32c3e040c240c1272e22de5c7f2 Mon Sep 17 00:00:00 2001 From: mpstaton Date: Sat, 9 May 2026 17:42:24 -0500 Subject: [PATCH] feat(directory-templates): concept-profile template + {{basename}} token + in-body provenance line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three landings: 1. New {{basename}} interpolation token. Resolves strictly to the target file's basename without .md, never falling through to a frontmatter `title` field. Concept-profile uses this so the H1 always matches the filename in the vault, regardless of what the frontmatter says. 2. New seed template src/docs/templates/concept-profile.md (matched in the user's vault at zz-cf-lib/templates/concept-profile.md). applies-to-paths is "concepts/**". Heading skeleton: - Defining and Describing {{basename}} (image placeholder bullet, optional mermaid diagram, italic lede, 2–4 sentence paragraph) - Uses in Context - History of Use (Origins, Evolution) - Best Real-World Examples - Case Studies search-recency is "year" rather than "month" since concepts evolve more slowly than products. 3. In-body provenance line inside the # Sources footer: _Generated via ._ Always renders, even when the run returned zero sources (in which case the body becomes "_No sources returned._"). Mirrors and complements the frontmatter stamp; gives readers an at-a-glance record of which model produced a given file and when, without needing to inspect frontmatter. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/docs/templates/concept-profile.md | 64 ++++++++++++++++++++++++ src/services/directoryTemplateService.ts | 55 +++++++++++++------- 2 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 src/docs/templates/concept-profile.md diff --git a/src/docs/templates/concept-profile.md b/src/docs/templates/concept-profile.md new file mode 100644 index 0000000..53d68a7 --- /dev/null +++ b/src/docs/templates/concept-profile.md @@ -0,0 +1,64 @@ +--- +title: Concept Profile (Idea / Pattern / Mental Model) +applies-to-paths: + - "concepts/**" +description: Generates an encyclopedia-style entry for a concept — definition, usage, history, examples, case studies. +--- + +# About this template + +Use this for files under `concepts/` whose body is empty or near-empty. The bullets under each heading are *instructions to the model*, not literal output. The model fills each section based on those instructions. + +The H1 heading uses `{{basename}}` (strict filename, never the frontmatter `title` field) so the rendered heading always matches the file name in the vault. + +```cft +provider: perplexity +model: sonar-deep-research +search-recency: year +return-citations: true +return-images: false +system: | + You are a research analyst writing an encyclopedia-style entry for the + concept named "{{basename}}". Use Perplexity's web search aggressively for + every section. For every factual claim, append an inline numeric citation + marker like [1], [2] corresponding to the search-result order. Quote + phrasing from primary sources where useful. Prefer first-party sources, + academic literature, books, and reputable industry analysis. Use the + entity's existing metadata as starting context. +``` + +# Defining and Describing {{basename}} +- Insert this exact placeholder line on its own bullet so the user can replace it after generation: `[Image embed placeholder — run "Find images for selection" on this section to populate.]` +- If this concept involves a process, hierarchy, taxonomy, or part-relationship that a diagram clarifies, render a `mermaid` codefence here. If a diagram does not add insight, omit it entirely — do not force one. +- Write a one-sentence italicized lede (a zinger or kicker) that captures the core insight in plain language. Use markdown italics: `_..._`. +- Then write a 2–4 sentence paragraph giving more context: what the concept is, when it applies, and why it matters. + +# Uses in Context +- 3–6 bullets describing how the concept is invoked — typically in business, technology, design, or popular culture — to describe something or convey meaning. +- Quote phrasing from sources where they invoke the term inline. Cite each. + +# History of Use + +## Origins +- Where the term first appeared (academic paper, book, blog post, industry report). +- Who introduced it and in what context. +- One concise paragraph or 2–4 cited bullets, whichever fits. + +## Evolution +- 2–3 inflection points where the concept was adapted, redefined, or expanded. +- Dated bullets, oldest first. Cite each. + +# Best Real-World Examples +- 5–7 one-line bullets, each naming a service, product, organization, or research finding that exemplifies the concept. +- Use `[Name](url)` form for the entity name in each bullet. + +# Case Studies +- 2–3 paragraph-length narratives going deeper than the Examples bullets above. +- Each case study explains: who, when, what they did, what changed, what it shows about the concept. +- Cite each factual claim. + +*** + +# User Notes + +Anything below the `***` line is excluded from the request. Use this zone for tuning notes, prior outputs, or examples while iterating on the template. diff --git a/src/services/directoryTemplateService.ts b/src/services/directoryTemplateService.ts index 60a94d7..ce73512 100644 --- a/src/services/directoryTemplateService.ts +++ b/src/services/directoryTemplateService.ts @@ -61,8 +61,11 @@ function wrapThinkBlocks(text: string): string { }); } -function buildSourcesFooter(sources: PerplexitySource[]): string { - if (!sources.length) return ''; +function buildSourcesFooter( + sources: PerplexitySource[], + runTimestamp: string, + runModelLabel: string, +): string { // Canonical Lossless reference-section format per // cite-wide/context-v/reminders/Lossless-Citation-Spec.md: // "always use a `: ` after the citation identifier". @@ -71,14 +74,21 @@ function buildSourcesFooter(sources: PerplexitySource[]): string { // form matches the spec, so emit it. // // Footer shape: blank line before the `***` separator, blank line between - // the separator and the `# Sources` h1, blank line before the references. - const lines = sources.map((s, i) => { + // the separator and the `# Sources` h1, blank line before the provenance + // line, blank line before the references. Provenance always renders, even + // when no sources came back, so the file always carries an in-body record + // of which model produced it and when. + const provenanceLine = `_Generated ${runTimestamp} via ${runModelLabel}._`; + const sourceLines = sources.map((s, i) => { const n = i + 1; const title = (typeof s.title === 'string' && s.title) ? s.title : (s.url ?? 'Source'); const url = s.url ?? ''; return url ? `[${n.toString()}]: [${title}](${url})` : `[${n.toString()}]: ${title}`; }); - return '\n\n***\n\n# Sources\n\n' + lines.join('\n') + '\n'; + const body = sourceLines.length > 0 + ? `${provenanceLine}\n\n${sourceLines.join('\n')}` + : `${provenanceLine}\n\n_No sources returned._`; + return '\n\n***\n\n# Sources\n\n' + body + '\n'; } const FRONTMATTER_FENCE = '---'; @@ -229,6 +239,7 @@ export interface InterpolationContext { title: string; frontmatter: string; frontmatterObj: Record; + basename: string; } export function interpolate(text: string, ctx: InterpolationContext): string { @@ -236,6 +247,7 @@ export function interpolate(text: string, ctx: InterpolationContext): string { if (key === 'title') return ctx.title; if (key === 'frontmatter') return ctx.frontmatter; if (key === 'today') return new Date().toISOString().slice(0, 10); + if (key === 'basename') return ctx.basename; const fmKey = key.startsWith('frontmatter.') ? key.slice('frontmatter.'.length) : key; if (fmKey in ctx.frontmatterObj) { return frontmatterValueToString(ctx.frontmatterObj[fmKey]); @@ -422,7 +434,12 @@ export async function applyTemplate( const title = typeof fm.title === 'string' ? fm.title : target.basename; const fmYaml = buildFrontmatterPayload(fm, settings.frontmatterWhitelist); - const ctx: InterpolationContext = { title, frontmatter: fmYaml, frontmatterObj: fm }; + const ctx: InterpolationContext = { + title, + frontmatter: fmYaml, + frontmatterObj: fm, + basename: target.basename, + }; const templateSystem = interpolate(template.cftSystem, ctx); const interpolatedSkeleton = interpolate(template.userSkeleton, ctx); @@ -462,17 +479,8 @@ export async function applyTemplate( isCancelled, ); - // Post-write cleanup: wrap blocks, append sources footer. - const trimmedStreamed = streamed.replace(/^\s+/, '').replace(/\s+$/, ''); - const cleanedStreamed = wrapThinkBlocks(trimmedStreamed); - const sourcesFooter = buildSourcesFooter(sources); - const finalContent = `${initialContent}${cleanedStreamed}\n${sourcesFooter}`; - await app.vault.modify(target, finalContent); - - // Stamp run metadata in the target's frontmatter so files can be - // queried for staleness ("which Tooling/ entries were last refreshed - // before ?"). Uses fileManager.processFrontMatter so other - // frontmatter keys remain byte-identical apart from these two. + // Compute run metadata once: used both in the in-body provenance line + // (inside the # Sources footer) and the frontmatter stamp. const provider = typeof template.cftConfig['provider'] === 'string' ? template.cftConfig['provider'] : 'unknown'; @@ -484,6 +492,19 @@ export async function applyTemplate( : provider; const runTimestamp = new Date().toISOString(); const runModelLabel = `${providerLabel} ${modelName}`.trim(); + + // Post-write cleanup: wrap blocks, append sources footer with + // provenance line. + const trimmedStreamed = streamed.replace(/^\s+/, '').replace(/\s+$/, ''); + const cleanedStreamed = wrapThinkBlocks(trimmedStreamed); + const sourcesFooter = buildSourcesFooter(sources, runTimestamp, runModelLabel); + const finalContent = `${initialContent}${cleanedStreamed}\n${sourcesFooter}`; + await app.vault.modify(target, finalContent); + + // Stamp run metadata in the target's frontmatter so files can be + // queried for staleness ("which Tooling/ entries were last refreshed + // before ?"). Uses fileManager.processFrontMatter so other + // frontmatter keys remain byte-identical apart from these two. await app.fileManager.processFrontMatter(target, (fm: Record) => { fm['cf_last_run'] = runTimestamp; fm['cf_last_run_model'] = runModelLabel;