feat(directory-templates): concept-profile template + {{basename}} token + in-body provenance line

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 <ISO timestamp> via <Provider> <model id>._
   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) <noreply@anthropic.com>
This commit is contained in:
mpstaton 2026-05-09 17:42:24 -05:00
parent e68287ca80
commit 11f0305ca9
2 changed files with 102 additions and 17 deletions

64
src/docs/templates/concept-profile.md vendored Normal file
View file

@ -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 24 sentence paragraph giving more context: what the concept is, when it applies, and why it matters.
# Uses in Context
- 36 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 24 cited bullets, whichever fits.
## Evolution
- 23 inflection points where the concept was adapted, redefined, or expanded.
- Dated bullets, oldest first. Cite each.
# Best Real-World Examples
- 57 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
- 23 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.

View file

@ -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<string, unknown>;
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 <think> 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 <date>?"). 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 <think> 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 <date>?"). Uses fileManager.processFrontMatter so other
// frontmatter keys remain byte-identical apart from these two.
await app.fileManager.processFrontMatter(target, (fm: Record<string, unknown>) => {
fm['cf_last_run'] = runTimestamp;
fm['cf_last_run_model'] = runModelLabel;