mirror of
https://github.com/lossless-group/perplexed-plugin.git
synced 2026-07-22 06:49:50 +00:00
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:
parent
e68287ca80
commit
11f0305ca9
2 changed files with 102 additions and 17 deletions
64
src/docs/templates/concept-profile.md
vendored
Normal file
64
src/docs/templates/concept-profile.md
vendored
Normal 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 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.
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue