diff --git a/docs/AUTHORING-ARTIFACTS.md b/docs/AUTHORING-ARTIFACTS.md index c0914d7..a6324ea 100644 --- a/docs/AUTHORING-ARTIFACTS.md +++ b/docs/AUTHORING-ARTIFACTS.md @@ -28,6 +28,14 @@ The core package exports `EditorialPlan`, `validateEditorialPlan()`, and `formatEditorialPlanMarkdown()` for tools that need structured authoring data. The Markdown artifact remains the normal review surface. +An EditorialPlan may select up to two soft explanation patterns: one primary +and one supporting pattern. Seqvio provides `causal-diagnosis`, +`mechanism-trace`, `system-flow`, `evidence-demonstration`, +`misconception-reframe`, and `progressive-model`. They are optional editorial +recipes, not executable templates. An agent may adapt their suggested stages or +omit pattern selection entirely when a custom structure better fits the source. +Pattern metadata does not change the ExplainerDocument schema or renderer. + ## Visual design brief `VISUAL-DESIGN.md` records concrete visual decisions: canvas, palette, diff --git a/examples/authoring/native-module-ci/EDITORIAL.md b/examples/authoring/native-module-ci/EDITORIAL.md index 51fca88..c699dd1 100644 --- a/examples/authoring/native-module-ci/EDITORIAL.md +++ b/examples/authoring/native-module-ci/EDITORIAL.md @@ -28,6 +28,22 @@ Node.js maintainers who understand npm and CI but do not routinely debug native The rebuild command can succeed while npm blocks the install script that produces pty.node, so verification must test both the artifact and the import. +## Explanation Strategy + +### Causal diagnosis + +- ID: `causal-diagnosis` +- Role: **primary** +- Reason: The source contains an observed failure, an expected install path, a break point, a root cause, and a verifiable repair. +- Adaptations: + - Combine repair and verification into the final section to stay within the duration budget. + +### Evidence demonstration + +- ID: `evidence-demonstration` +- Role: **supporting** +- Reason: The conclusion must be supported by checking the native artifact and importing the module, not by trusting the rebuild message. + ## Content Decisions ### Command success and native artifact existence are different conditions. diff --git a/examples/authoring/native-module-ci/authoring-data.json b/examples/authoring/native-module-ci/authoring-data.json index ad56de8..34770fc 100644 --- a/examples/authoring/native-module-ci/authoring-data.json +++ b/examples/authoring/native-module-ci/authoring-data.json @@ -17,6 +17,23 @@ }, "thesis": "The rebuild command can succeed while npm blocks the install script that produces pty.node, so verification must test both the artifact and the import.", "durationBudgetSec": 75, + "explanationStrategy": { + "patterns": [ + { + "id": "causal-diagnosis", + "role": "primary", + "reason": "The source contains an observed failure, an expected install path, a break point, a root cause, and a verifiable repair.", + "adaptations": [ + "Combine repair and verification into the final section to stay within the duration budget." + ] + }, + { + "id": "evidence-demonstration", + "role": "supporting", + "reason": "The conclusion must be supported by checking the native artifact and importing the module, not by trusting the rebuild message." + } + ] + }, "concepts": [ { "id": "success-is-not-artifact", diff --git a/packages/core/src/authoring/index.ts b/packages/core/src/authoring/index.ts index 8cbcddc..96fc152 100644 --- a/packages/core/src/authoring/index.ts +++ b/packages/core/src/authoring/index.ts @@ -1,3 +1,4 @@ export * from './schema'; +export * from './patterns'; export * from './validate'; export * from './markdown'; diff --git a/packages/core/src/authoring/markdown.ts b/packages/core/src/authoring/markdown.ts index 3a9e3f6..3e5b644 100644 --- a/packages/core/src/authoring/markdown.ts +++ b/packages/core/src/authoring/markdown.ts @@ -1,9 +1,19 @@ import type { EditorialPlan, VisualDesignBrief } from './schema'; +import { getExplanationPattern } from './patterns'; function bullets(values: string[] | undefined, empty = 'None declared.'): string { return values?.length ? values.map((value) => `- ${value}`).join('\n') : empty; } export function formatEditorialPlanMarkdown(plan: EditorialPlan): string { + const strategy = plan.explanationStrategy?.patterns.length + ? plan.explanationStrategy.patterns.map((selection) => { + const pattern = getExplanationPattern(selection.id); + const adaptations = selection.adaptations?.length + ? `\n- Adaptations:\n${selection.adaptations.map((item) => ` - ${item}`).join('\n')}` + : ''; + return `### ${pattern.name}\n\n- ID: \`${selection.id}\`\n- Role: **${selection.role}**\n- Reason: ${selection.reason}${adaptations}`; + }).join('\n\n') + : 'Custom structure; no library pattern selected.'; const concepts = plan.concepts.map((concept) => { const lines = [ `### ${concept.claim}`, @@ -31,7 +41,7 @@ export function formatEditorialPlanMarkdown(plan: EditorialPlan): string { return lines.join('\n'); }).join('\n\n'); - return `---\nformat: ${plan.format}\nid: ${plan.id}\nduration_budget_sec: ${plan.durationBudgetSec}\n---\n\n# Editorial Plan: ${plan.title}\n\n## Objective\n\n${plan.objective}\n\n## Audience\n\n${plan.audience.description}\n\n### Prior Knowledge\n\n${bullets(plan.audience.priorKnowledge)}\n\n### Likely Misconceptions\n\n${bullets(plan.audience.likelyMisconceptions)}\n\n## Thesis\n\n${plan.thesis}\n\n## Content Decisions\n\n${concepts}\n\n## Explanation Structure\n\n${sections}\n`; + return `---\nformat: ${plan.format}\nid: ${plan.id}\nduration_budget_sec: ${plan.durationBudgetSec}\n---\n\n# Editorial Plan: ${plan.title}\n\n## Objective\n\n${plan.objective}\n\n## Audience\n\n${plan.audience.description}\n\n### Prior Knowledge\n\n${bullets(plan.audience.priorKnowledge)}\n\n### Likely Misconceptions\n\n${bullets(plan.audience.likelyMisconceptions)}\n\n## Thesis\n\n${plan.thesis}\n\n## Explanation Strategy\n\n${strategy}\n\n## Content Decisions\n\n${concepts}\n\n## Explanation Structure\n\n${sections}\n`; } export function formatVisualDesignBriefMarkdown(brief: VisualDesignBrief): string { diff --git a/packages/core/src/authoring/patterns.ts b/packages/core/src/authoring/patterns.ts new file mode 100644 index 0000000..0e14a51 --- /dev/null +++ b/packages/core/src/authoring/patterns.ts @@ -0,0 +1,114 @@ +import { + EXPLANATION_PATTERN_IDS, + type EditorialSectionPurpose, + type ExplanationPatternId, +} from './schema'; + +export interface ExplanationPatternStage { + id: string; + title: string; + purpose: EditorialSectionPurpose; + outcome: string; +} + +export interface ExplanationPatternDefinition { + id: ExplanationPatternId; + name: string; + intent: string; + stages: readonly ExplanationPatternStage[]; + advisoryChecks: readonly string[]; +} + +export const EXPLANATION_PATTERNS = { + 'causal-diagnosis': { + id: 'causal-diagnosis', + name: 'Causal diagnosis', + intent: 'Explain a failure from observed symptom through root cause and verified repair.', + stages: [ + { id: 'symptom', title: 'Observed symptom', purpose: 'hook', outcome: 'The audience can state the failure without assuming its cause.' }, + { id: 'expected-path', title: 'Expected mechanism', purpose: 'establish-model', outcome: 'The audience understands the path that should have succeeded.' }, + { id: 'break-point', title: 'Break point', purpose: 'explain-mechanism', outcome: 'The audience can locate where actual behavior diverged.' }, + { id: 'root-cause', title: 'Root cause', purpose: 'explain-mechanism', outcome: 'The audience can connect evidence to the causal explanation.' }, + { id: 'repair', title: 'Repair', purpose: 'demonstrate', outcome: 'The audience understands the change and why it addresses the cause.' }, + { id: 'verification', title: 'Verification', purpose: 'summarize', outcome: 'The audience can distinguish a verified result from a successful command.' }, + ], + advisoryChecks: ['Keep symptoms separate from inferred causes.', 'End with evidence that verifies the repaired behavior.'], + }, + 'mechanism-trace': { + id: 'mechanism-trace', + name: 'Mechanism trace', + intent: 'Explain how inputs are transformed through state changes into outputs.', + stages: [ + { id: 'input', title: 'Input and actors', purpose: 'establish-model', outcome: 'The audience knows the starting state and relevant objects.' }, + { id: 'transformations', title: 'Transformations', purpose: 'explain-mechanism', outcome: 'The audience can follow the ordered changes.' }, + { id: 'state', title: 'State change', purpose: 'explain-mechanism', outcome: 'The audience understands what is different after each operation.' }, + { id: 'output', title: 'Output', purpose: 'demonstrate', outcome: 'The audience connects the mechanism to its observable result.' }, + { id: 'boundary', title: 'Boundary conditions', purpose: 'summarize', outcome: 'The audience knows when the model does and does not apply.' }, + ], + advisoryChecks: ['Name the state changed by each important operation.', 'Do not replace mechanism with a list of component names.'], + }, + 'system-flow': { + id: 'system-flow', + name: 'System flow', + intent: 'Follow a request, event, or data item across actors and system boundaries.', + stages: [ + { id: 'origin', title: 'Origin', purpose: 'hook', outcome: 'The audience knows what starts the flow.' }, + { id: 'actors', title: 'Actors and boundaries', purpose: 'establish-model', outcome: 'The audience can identify ownership and boundaries.' }, + { id: 'forward-path', title: 'Forward path', purpose: 'explain-mechanism', outcome: 'The audience can trace the ordered handoffs.' }, + { id: 'handling', title: 'Critical handling', purpose: 'explain-mechanism', outcome: 'The audience understands the important processing point.' }, + { id: 'result', title: 'Response or terminal state', purpose: 'demonstrate', outcome: 'The audience can connect the path to its result.' }, + ], + advisoryChecks: ['Make every boundary crossing explicit.', 'Keep actor ownership stable throughout the explanation.'], + }, + 'evidence-demonstration': { + id: 'evidence-demonstration', + name: 'Evidence demonstration', + intent: 'Support a claim with an operation, observed state, interpretation, and conclusion.', + stages: [ + { id: 'claim', title: 'Claim', purpose: 'hook', outcome: 'The audience knows what the demonstration will establish.' }, + { id: 'setup', title: 'Setup', purpose: 'establish-model', outcome: 'The audience knows the relevant conditions.' }, + { id: 'operation', title: 'Operation', purpose: 'demonstrate', outcome: 'The audience can reproduce or inspect the action.' }, + { id: 'observation', title: 'Observed state', purpose: 'demonstrate', outcome: 'The audience sees the result independently of the plan.' }, + { id: 'interpretation', title: 'Interpretation', purpose: 'explain-mechanism', outcome: 'The audience understands why the observation supports the claim.' }, + { id: 'conclusion', title: 'Conclusion', purpose: 'summarize', outcome: 'The audience can apply the demonstrated result.' }, + ], + advisoryChecks: ['Do not treat an intended operation as observed evidence.', 'Explain what the observation proves, not only what appeared.'], + }, + 'misconception-reframe': { + id: 'misconception-reframe', + name: 'Misconception reframe', + intent: 'Replace a plausible but incomplete model with one that explains conflicting evidence.', + stages: [ + { id: 'prior-model', title: 'Plausible prior model', purpose: 'hook', outcome: 'The audience recognizes why the misconception is attractive.' }, + { id: 'conflict', title: 'Conflicting evidence', purpose: 'correct-misconception', outcome: 'The audience sees what the prior model cannot explain.' }, + { id: 'replacement', title: 'Replacement model', purpose: 'establish-model', outcome: 'The audience gains a coherent alternative.' }, + { id: 'reapply', title: 'Reapply the model', purpose: 'demonstrate', outcome: 'The audience can explain the original evidence correctly.' }, + { id: 'rule', title: 'Decision rule', purpose: 'summarize', outcome: 'The audience can avoid the misconception in a new case.' }, + ], + advisoryChecks: ['Represent the prior model fairly before correcting it.', 'Provide a usable replacement model, not only a negation.'], + }, + 'progressive-model': { + id: 'progressive-model', + name: 'Progressive model', + intent: 'Introduce a complex system from a small stable overview, then deepen and reintegrate it.', + stages: [ + { id: 'overview', title: 'Small overview', purpose: 'establish-model', outcome: 'The audience has a compact map of the main objects.' }, + { id: 'focus', title: 'Focus one subsystem', purpose: 'explain-mechanism', outcome: 'The audience knows which part is being expanded.' }, + { id: 'deep-dive', title: 'Deep dive', purpose: 'explain-mechanism', outcome: 'The audience understands the focused mechanism.' }, + { id: 'reintegrate', title: 'Reintegrate', purpose: 'summarize', outcome: 'The audience connects local detail back to the whole.' }, + ], + advisoryChecks: ['Keep object identity stable between overview and detail.', 'Do not reveal the complete complex model before its parts are introduced.'], + }, +} as const satisfies Record; + +export function isExplanationPatternId(value: unknown): value is ExplanationPatternId { + return typeof value === 'string' && EXPLANATION_PATTERN_IDS.includes(value as ExplanationPatternId); +} + +export function getExplanationPattern(id: ExplanationPatternId): ExplanationPatternDefinition { + return EXPLANATION_PATTERNS[id]; +} + +export function listExplanationPatterns(): ExplanationPatternDefinition[] { + return EXPLANATION_PATTERN_IDS.map((id) => EXPLANATION_PATTERNS[id]); +} diff --git a/packages/core/src/authoring/schema.ts b/packages/core/src/authoring/schema.ts index 0762f36..c43f75b 100644 --- a/packages/core/src/authoring/schema.ts +++ b/packages/core/src/authoring/schema.ts @@ -1,6 +1,31 @@ export const EDITORIAL_PLAN_FORMAT = 'seqvio-editorial-plan' as const; export const VISUAL_DESIGN_BRIEF_FORMAT = 'seqvio-visual-design' as const; +export const EXPLANATION_PATTERN_IDS = [ + 'causal-diagnosis', + 'mechanism-trace', + 'system-flow', + 'evidence-demonstration', + 'misconception-reframe', + 'progressive-model', +] as const; + +export type ExplanationPatternId = (typeof EXPLANATION_PATTERN_IDS)[number]; +export type ExplanationPatternRole = 'primary' | 'supporting'; + +export interface ExplanationPatternSelection { + id: ExplanationPatternId; + role: ExplanationPatternRole; + reason: string; + /** Explicit departures from the suggested pattern arc. */ + adaptations?: string[]; +} + +export interface EditorialExplanationStrategy { + /** Omit explanationStrategy entirely when a custom structure is better. */ + patterns: ExplanationPatternSelection[]; +} + export type EditorialConceptRole = | 'essential' | 'evidence' @@ -28,6 +53,8 @@ export interface EditorialPlan { }; thesis: string; durationBudgetSec: number; + /** Optional, composable structural guidance. It does not constrain executable IR. */ + explanationStrategy?: EditorialExplanationStrategy; concepts: Array<{ id: string; claim: string; diff --git a/packages/core/src/authoring/validate.ts b/packages/core/src/authoring/validate.ts index 421e886..e2fb215 100644 --- a/packages/core/src/authoring/validate.ts +++ b/packages/core/src/authoring/validate.ts @@ -4,6 +4,7 @@ import { type EditorialPlan, type VisualDesignBrief, } from './schema'; +import { isExplanationPatternId } from './patterns'; import { EXPLAINER_DOCUMENT_DEFAULTS, type ExplainerDocument, @@ -32,6 +33,49 @@ export function validateEditorialPlan(plan: EditorialPlan): AuthoringIssue[] { issues.push({ severity: 'error', path: 'durationBudgetSec', code: 'invalid_duration_budget', message: 'Duration budget must be greater than zero.' }); } + const strategy = plan.explanationStrategy; + if (strategy) { + if (!Array.isArray(strategy.patterns) || strategy.patterns.length === 0) { + issues.push({ severity: 'warning', path: 'explanationStrategy.patterns', code: 'empty_explanation_strategy', message: 'Omit explanationStrategy when no library pattern improves the plan.' }); + } else { + const selections = strategy.patterns as unknown[]; + if (strategy.patterns.length > 2) { + issues.push({ severity: 'warning', path: 'explanationStrategy.patterns', code: 'too_many_explanation_patterns', message: 'More than two patterns usually weakens the editorial focus; keep only patterns that materially shape the explanation.' }); + } + const ids = selections.map((selection) => ( + selection && typeof selection === 'object' && 'id' in selection + ? String((selection as { id?: unknown }).id) + : '' + )); + for (const id of duplicateIds(ids)) { + issues.push({ severity: 'error', path: 'explanationStrategy.patterns', code: 'duplicate_explanation_pattern', message: `Explanation pattern "${id}" is selected more than once.` }); + } + const primaryCount = selections.filter((selection) => ( + selection && typeof selection === 'object' && (selection as { role?: unknown }).role === 'primary' + )).length; + if (primaryCount !== 1) { + issues.push({ severity: 'error', path: 'explanationStrategy.patterns', code: 'invalid_primary_pattern_count', message: 'A selected strategy must contain exactly one primary pattern.' }); + } + selections.forEach((rawSelection, index) => { + const selectionPath = `explanationStrategy.patterns[${index}]`; + if (!rawSelection || typeof rawSelection !== 'object') { + issues.push({ severity: 'error', path: selectionPath, code: 'invalid_explanation_pattern_selection', message: 'Pattern selection must be an object.' }); + return; + } + const selection = rawSelection as { id?: unknown; role?: unknown; reason?: unknown }; + if (!isExplanationPatternId(selection.id)) { + issues.push({ severity: 'error', path: `${selectionPath}.id`, code: 'unknown_explanation_pattern', message: `Unknown explanation pattern "${String(selection.id)}".` }); + } + if (selection.role !== 'primary' && selection.role !== 'supporting') { + issues.push({ severity: 'error', path: `${selectionPath}.role`, code: 'invalid_explanation_pattern_role', message: 'Pattern role must be primary or supporting.' }); + } + if (typeof selection.reason !== 'string' || !selection.reason.trim()) { + issues.push({ severity: 'warning', path: `${selectionPath}.reason`, code: 'missing_explanation_pattern_reason', message: 'Explain why this pattern fits the source, or remove the selection.' }); + } + }); + } + } + for (const id of duplicateIds(plan.concepts.map((concept) => concept.id))) { issues.push({ severity: 'error', path: 'concepts', code: 'duplicate_concept_id', message: `Concept id "${id}" is duplicated.` }); } diff --git a/packages/core/tests/authoring.test.mjs b/packages/core/tests/authoring.test.mjs index 1cf110c..63cf8e8 100644 --- a/packages/core/tests/authoring.test.mjs +++ b/packages/core/tests/authoring.test.mjs @@ -3,6 +3,7 @@ import assert from 'node:assert'; import { formatEditorialPlanMarkdown, formatVisualDesignBriefMarkdown, + listExplanationPatterns, validateEditorialPlan, validateAuthoringTrace, validateVisualDesignBrief, @@ -20,6 +21,21 @@ const plan = { }, thesis: 'Blocked install scripts can leave node-pty without a loadable native binary.', durationBudgetSec: 60, + explanationStrategy: { + patterns: [ + { + id: 'causal-diagnosis', + role: 'primary', + reason: 'The source contains an observed failure, a break point, and a verifiable repair.', + adaptations: ['Combine the repair and verification into one concise section.'], + }, + { + id: 'evidence-demonstration', + role: 'supporting', + reason: 'The conclusion depends on distinguishing command output from a loadable artifact.', + }, + ], + }, concepts: [ { id: 'blocked-script', @@ -77,9 +93,58 @@ describe('human-readable authoring artifacts', () => { const markdown = formatEditorialPlanMarkdown(plan); assert.match(markdown, /# Editorial Plan:/); assert.match(markdown, /Decision: \*\*omit\*\*/); + assert.match(markdown, /## Explanation Strategy/); + assert.match(markdown, /Causal diagnosis/); + assert.match(markdown, /Combine the repair and verification/); assert.match(markdown, /## Explanation Structure/); }); + it('exports six optional explanation patterns without requiring one', () => { + assert.deepEqual( + listExplanationPatterns().map((pattern) => pattern.id), + [ + 'causal-diagnosis', + 'mechanism-trace', + 'system-flow', + 'evidence-demonstration', + 'misconception-reframe', + 'progressive-model', + ], + ); + const customPlan = { ...plan, explanationStrategy: undefined }; + assert.deepEqual(validateEditorialPlan(customPlan), []); + assert.match(formatEditorialPlanMarkdown(customPlan), /Custom structure/); + }); + + it('validates strategy references but keeps pattern fit advisory', () => { + const unknown = { + ...plan, + explanationStrategy: { + patterns: [{ id: 'generic-template', role: 'primary', reason: 'Forced template.' }], + }, + }; + assert.ok(validateEditorialPlan(unknown).some((issue) => issue.code === 'unknown_explanation_pattern')); + + const broad = { + ...plan, + explanationStrategy: { + patterns: [ + ...plan.explanationStrategy.patterns, + { id: 'system-flow', role: 'supporting', reason: 'A third optional lens.' }, + ], + }, + }; + const broadIssues = validateEditorialPlan(broad); + assert.ok(broadIssues.some((issue) => issue.code === 'too_many_explanation_patterns' && issue.severity === 'warning')); + + const malformed = { + ...plan, + explanationStrategy: { patterns: [null, { id: 'causal-diagnosis', role: 'primary' }] }, + }; + assert.doesNotThrow(() => validateEditorialPlan(malformed)); + assert.ok(validateEditorialPlan(malformed).some((issue) => issue.code === 'invalid_explanation_pattern_selection')); + }); + it('rejects an included concept that is missing from the structure', () => { const issues = validateEditorialPlan({ ...plan, sections: [] }); assert.ok(issues.some((issue) => issue.code === 'unscheduled_concept')); diff --git a/packages/renderer/src/agent-contract.ts b/packages/renderer/src/agent-contract.ts index 571429c..20559ae 100644 --- a/packages/renderer/src/agent-contract.ts +++ b/packages/renderer/src/agent-contract.ts @@ -5,7 +5,10 @@ * returned JSON deterministically. This file does not call AI or the network. */ -import { listAgentAuthorableSceneCapabilities } from '@seqvio/core'; +import { + listAgentAuthorableSceneCapabilities, + listExplanationPatterns, +} from '@seqvio/core'; export type AgentLanguage = 'zh' | 'en' | 'auto'; export type AgentDomain = @@ -30,6 +33,15 @@ export interface AgentAuthoringContext { visualDesignBrief?: string; } +export function formatExplanationPatternCatalog(): string { + return listExplanationPatterns() + .map((pattern) => { + const arc = pattern.stages.map((stage) => stage.title).join(' -> '); + return `- ${pattern.id}: ${pattern.intent}\n Suggested arc: ${arc}`; + }) + .join('\n'); +} + export function formatEditorialPlanningPrompt( content: string, options: AgentPlanningOptions = {} @@ -48,6 +60,7 @@ The plan must contain these headings: - Objective - Audience (including prior knowledge and likely misconceptions) - Thesis +- Explanation Strategy - Content Decisions (each item: stable id, include/omit, role, reason, prerequisites, time estimate) - Explanation Structure (each section: stable id, purpose, concept ids, audience outcome, target seconds) @@ -56,8 +69,16 @@ Rules: - Every included essential concept must appear in the explanation structure. - Keep the section budget within the intended video length. - One section should perform one cognitive job. +- Select zero to two explanation patterns only when they improve the content. +- If selected, use exactly one primary pattern, optionally one supporting pattern, + and state the reason and any adaptations. Patterns are guidance, not templates: + reorder, merge, or omit suggested stages when the source requires it. +- If none fits, write "Custom structure; no library pattern selected." - Return Markdown only, beginning with "# Editorial Plan:". +Available optional explanation patterns: +${formatExplanationPatternCatalog()} + ${language} ${describeDomain(options.domain)} Target ${options.maxScenes ?? 5} explanation sections. diff --git a/packages/renderer/tests/agent-contract.test.mjs b/packages/renderer/tests/agent-contract.test.mjs index 7ee944a..4f3277c 100644 --- a/packages/renderer/tests/agent-contract.test.mjs +++ b/packages/renderer/tests/agent-contract.test.mjs @@ -5,6 +5,7 @@ import { formatEditorialPlanningPrompt, formatVisualDesignPrompt, formatAgentSceneCapabilities, + formatExplanationPatternCatalog, resolveAgentIrFormat, } from '../dist/agent-contract.js'; @@ -45,11 +46,22 @@ describe('agent-contract', () => { const editorial = formatEditorialPlanningPrompt('Explain HTTP caching', { language: 'en' }); assert.match(editorial, /Content Decisions/); assert.match(editorial, /Make omissions explicit/); + assert.match(editorial, /zero to two explanation patterns/); + assert.match(editorial, /Patterns are guidance, not templates/); + assert.match(editorial, /causal-diagnosis/); + assert.match(editorial, /progressive-model/); const visual = formatVisualDesignPrompt('Explain HTTP caching', '# Editorial Plan: Cache', { language: 'en' }); assert.match(visual, /Section Treatments/); assert.match(visual, /real\s+capture material/); }); + it('describes all optional editorial patterns without adding scene capabilities', () => { + const catalog = formatExplanationPatternCatalog(); + assert.equal(catalog.split('Suggested arc:').length - 1, 6); + assert.match(catalog, /evidence-demonstration/); + assert.doesNotMatch(catalog, /terminal:/); + }); + it('refuses final IR planning without both approved authoring artifacts', () => { assert.throws( () => formatAgentPlanningPrompt('Explain HTTP caching', { domain: 'programming' }),