Commit Graph
14 Commits
Author SHA1 Message Date
wyuc 3b4567076f refactor(generation): consume the package everywhere and delete lib/generation (#1090)
Part D of #1057 — the switch. All consumers import @openmaic/generation; lib/generation deleted (net -11,900 lines). PBL wiring preserved (single-call -> classified fallback -> injected loop), streaming outline route deduplicated onto buildOutlinePrompt, serverExternalPackages wired, and the change verified by a real-model four-kind end-to-end run against the production build with a rendered-classroom screenshot check. Closes #1057.
2026-08-10 03:58:33 -04:00
wyuc 0ae21aa559 refactor(pbl): retire v1 write path and dead UI, converge on v2 (#1060)
* refactor(pbl): retire v1 write path and dead UI (#1058)

Remove the legacy generator, renderer branches, chat API, and compatibility mirror writes.

Keep stored v1 scenes readable through documented legacy adapters, and surface ordinary PBL planner double failures instead of degrading.

* fix(pbl): contain planner failure to the scene, support legacy scenes in action regeneration, tighten legacy guards

- Contain typed planner failures per scene while preserving retry and skip behavior.
- Upgrade legacy PBL content before regenerating scene actions.
- Apply video-export import boundaries to legacy readers.
- Harden legacy rendering and empty-state handling.
- Restore legacy upgrade compatibility coverage.
- Remove retired translations and refresh fallback narration and legacy cover documentation.

* fix(pbl): keep tolerant legacy configs renderable, guard invalid chat timestamps

* chore(dsl): bump to 0.6.3 for README accuracy fix

The README no longer references the deleted PBLProjectConfig symbol; the version gate requires a bump for any publishable package input change.

* fix(pbl): harden legacy upgrade and persistence contract

* fix(pbl): align action prompt with v2 workflow and validate project containers

* fix(i18n): remove retired PBL v1 keys from the new fr-FR locale

The fr-FR locale landed on main after this branch removed the v1 UI key families; prune the same 38 keys so all 10 locales align with en-US.

* fix(generation): sync packaged pbl-actions template with the v2 wording

The generation package that landed on main carries a byte-identical copy of the app's pbl-actions template; sync the v2 rewording and bump the package version.

* fix(pbl): normalize legacy content and preserve planner status

* fix(pbl): propagate provider status and ground action prompts

* fix(pbl): classify planner failures for retry, default the project summary prompt input

* fix(pbl): fall back to legacy data when v2 payloads are damaged, harden summary building

* refactor(pbl): resolve hybrid content through a validity-aware resolver

* fix(pbl): keep one v2 acceptance criterion across validator, resolver and persistence

Persistence gates on the same container predicate as the validator and the renderer's resolver: payloads the renderer will not treat as v2 are inert bytes everywhere, avoiding runtime side effects for documents the write barrier would reject. An explicit null proficiencyAssessment stays processable.

* fix(pbl): require runnable projects and harden legacy fallback

* test(pbl): exercise the real save reload and hydration chain

* fix(pbl): require an instructor and usable microtasks before v2 takes precedence

* fix(pbl): fall back to the loop planner unless the provider failed or the user cancelled

* fix(pbl): require role and task identities before v2 takes precedence

Runtime normalization can repair status and create the instructor thread, but it cannot synthesize the role id that binds the thread, the microtask ids used by lookup and update paths, or the labels the workspace renders. The runnable predicate and its video-export mirror now require them, so identity-less payloads fall back to usable legacy data.
2026-08-08 23:26:17 -04:00
LING DUAN 61a7154f07 feat(pi): add evidence-aware Director context runtime (#971)
* feat(pi): add evidence-aware Director context runtime

* fix(web-search): require completed native search

* fix(orchestration): preserve whiteboard across agent handoffs

* fix(pi): remove wrap-up turn bypass
2026-07-27 11:38:41 -04:00
Yanpeng Wang 4b821272ff feat(interactive-actions): feed real HTML element inventory into the prompt (#829)
* feat(interactive-actions): feed real HTML element inventory into the prompt

Teacher actions (highlight/setState/annotation/reveal) for interactive scenes
frequently miss because the model has to guess selectors from the prompt's
naming conventions (e.g. `#{var}-slider`, `#start-btn`) that don't match the
ids the content model actually emits (`#score-val`, `#launch-btn`, …). The
game widget category is the worst hit because its conventions cover almost
nothing.

Extract an inventory of interactable elements (ids + notable classes +
aria/role/data-step-id) from the generated widget HTML and thread it into
`INTERACTIVE_ACTIONS` under an `{{elementInventory}}` variable. The system
prompt now instructs the model to select `target` from the inventory first
and only fall back to conventions when nothing matches.

The inventory is:
- Persisted on the runtime `InteractiveContent` so pro-mode
  `regenerate_scene_actions` gets the same real selectors, not a stale guess.
- Refreshed by `edit_interactive_html` — the tool re-runs the extractor on
  the post-edit HTML and the client writes it back into `scene.content`, so
  a subsequent action regen sees the new dom.
- Computed on the fly in `generateSceneActions` when a legacy scene lacks it,
  so pre-existing courses benefit without a data migration.

Tailwind / utility class tokens are filtered so the 30-class inventory cap
keeps semantic hooks like `.pairing-rules`, `.dna-card`, `.btn-launch` from
being crowded out by layout tokens.

* style: prettier format

* fix(interactive-actions): compute element inventory at prompt-build time

Address PR #829 review feedback:

1. Drop the persisted `elementInventory` field on
   `InteractiveContent`/`GeneratedInteractiveContent`. `generateSceneActions`
   already computes the inventory on the fly from `content.html`; make that
   the only path. Removes born-stale mismatches (post-processing rewrite vs
   raw-html extraction), same-turn staleness of `sceneContextMap`, and the
   silent divergence between `createSceneWithActions` (drops the field) and
   `scene-builder.ts` (keeps it) — server-generated classrooms already ran
   in recompute-only mode today.

2. Extractor hardening:
   - Strip HTML comments alongside `<script>` / `<style>` so commented-out
     markup can't forge phantom inventory entries the prompt would prefer.
   - Drop content after an unmatched `<script` open so a truncated
     generation doesn't inventory ids inside `innerHTML` template strings.
   - Parse attributes into a name→value map by walking the tag grammar
     instead of per-attribute regex over the flat string. Prevents
     `aria-label="try name=alpha or id=fake"` from lifting phantom
     `name=alpha` / `id=fake` attributes.
   - Collapse whitespace and truncate each attribute value (~120 chars)
     when building inventory lines so a multi-line or hostile aria-label
     can't fake prompt sections.
   - Keep semantic hooks whose names collide with Tailwind category
     prefixes (`.grid-cell`, `.fill-blank`, `.text-input`, `.select-btn`,
     `.ring-carbon`) when they are declared in the page's own `<style>`
     block.

* style: prettier format

* fix(interactive-actions): surface id-less data-attribute targets in inventory

The interactive-actions system prompt tells the model to prefer
`[data-step-id="step-1"]` selectors for procedural-skill widgets whose
step rows typically carry only `data-step-id` (no id, and often no
surviving semantic class). Under the previous extractor those rows were
invisible: `data-step-id` and `data-action` only surfaced as decoration
on id rows, so a widget with all-data-only steps could hit the
"(no interactive elements detected)" sentinel while the system prompt
kept telling the model to prefer the inventory — degrading exactly the
one widget family the conventions already served well.

Add a third `Stable data attributes:` section that emits an
`[data-step-id="step-1"] <li>` line for each id-less element carrying
data-step-id or data-action, deduped on attribute value and capped at 30
entries. Elements that already have an id keep those attributes as row
decoration only (no duplicate row).
2026-07-03 02:09:03 -04:00
cec3c6aaea Refactor widget actions into the scene action pipeline (#796)
* refactor: align widget actions with scene pipeline

* feat(edit): migrate legacy teacherActions off interactive content on load

The widget-actions pipeline no longer persists a teacherActions authoring
layer; playback reads only scene.actions. Add migrateInteractiveContent to
strip the dead field from existing documents as they load through migrateScene
(wired into all store/classroom load paths). Satisfies the #794 forward-migration
acceptance item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(generation): default widget_setState.state to {} when the LLM omits it

The removed TeacherAction->Action converter always defaulted state to {}.
Restore that guard in the structured-output parser so an action missing
state can't forward state: undefined into the widget iframe, where
SET_WIDGET_STATE handlers dereference it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(edit): also strip nested widgetConfig.teacherActions in migration

Legacy WidgetConfig variants each declared their own teacherActions, so existing
documents can carry the dead field nested inside content.widgetConfig as well as
at the top level. Extend migrateInteractiveContent to drop both, preserving all
other widgetConfig fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(prompts): document stable widget-action selectors for diagram/game/viz3d

The interactive-actions prompt documented stable target selectors only for
procedural-skill and simulation widgets. Add the canonical selectors the
diagram / game / visualization3d content prompts actually emit (node IDs via
revealOrder; #zoom-in-btn / #canvas-container / #speed-slider / ...; #start-btn),
plus a rule to use widget_setState or a speech-only beat instead of guessing a
target when no stable selector is known (e.g. free-form code widgets). Hardens
against widget actions silently no-oping on non-existent selectors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-06-28 21:13:21 +08:00
杨慎 09863af421 Add PBL v2 core schema and generation path (#795)
* feat: add PBL v2 core planner

* chore: restore unrelated lint comments

* fix: document PBL v2 scenario outline flags

* fix: expose PBL scenario subtype in outlines

* chore: clear PBL v2 completion stats lint warning

* chore: address PBL instructor code quality comments

* fix: address PBL v2 review findings

* chore: restore unrelated lint comments
2026-06-25 18:44:46 +08:00
RongHuiQiang e6debb093b fix(vocational): address review feedback 2026-06-09 11:30:38 +08:00
jackefn c58893e76f feat(vocational): add task engine and procedural skill widgets 2026-06-08 11:13:09 +08:00
wyucandClaude Opus 4.8 ea049417cd fix(prompts): gate media snippets in interactive-outlines template (#626) (#628)
The interactive-outlines template (Ultra Mode) never included the
media-generation instructions, so the `mediaGenerations` field was only
ever produced by strong cloud models inferring it from context. Weak
local models followed the prompt literally and omitted it, so images
silently never generated.

Mirror requirements-to-outlines by including the same conditional
snippet blocks (image / video / media-safety), gated on the
imageEnabled / videoEnabled / mediaEnabled variables that both outline
paths already pass to buildPrompt. Reuses the existing snippets as the
single source of truth rather than duplicating the instructions.

Extends media-conditional.test.ts to hold INTERACTIVE_OUTLINES to the
same media-gating contract as the standard template.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:41:24 +08:00
wyuc 2dff6d10be feat: add generated video manifest refs (#540)
* feat: add generated video manifest refs

* style: format video manifest changes

* fix: address video manifest review feedback
2026-05-09 20:56:31 +08:00
Yizuki_Ame 3fafca5f99 feat(prompts): conditional media snippets in generation prompts (#490)
* feat(prompts): conditional media snippets in generation prompts (#483)

Replace always-included media instructions + negative-override pattern with conditional snippet includes gated on imageEnabled/videoEnabled flags.

- Extend prompt loader with {{snippet:name?if=condition}} syntax

- Extract 15 media instruction snippets from outline and slide templates

- Wire imageEnabled/videoEnabled through outline-generator, scene-generator, and route handler

- Remove mediaGenerationPolicy negative-override from all call sites

- Close gap: slide content generation now receives media policy

- Unify pre-output checklist to labeled bullet format

* refactor(prompts): replace ?if= snippet syntax with {{#if}} inline blocks

Address review feedback on PR #490 — the file-per-fragment pattern was
too fine-grained (6 of 15 new snippets were ≤2 lines).

Changes:
- Add {{#if name}}...{{/if}} block syntax to the loader (~10 lines)
- Inline 9 micro-snippets (≤4 lines each) back into host templates
- Keep 3 large instruction snippets as {{snippet:name}} wrapped in
  {{#if}} at the call site
- Drop ?if= syntax and PromptLoadOptions.conditions — conditional
  blocks read directly from buildPrompt's variables
- Update tests and README

Processing order: snippets → conditional blocks → variable interpolation

* style: format media-conditional test with prettier
2026-04-26 14:52:09 +08:00
fc6b186929 refactor(whiteboard): file-based prompts + geometry conflict detection (#485)
* feat(prompts): declare whiteboard role prompt IDs + whiteboard-reference snippet

* feat(prompts): add whiteboard role template skeletons + reference snippet stub

* refactor(orchestration): move buildWhiteboardGuidelines to role-specific markdown templates

* docs(prompts): whiteboard guidelines now in markdown templates

* feat(prompts): whiteboard-reference canvas + JSON output sections

* feat(prompts): whiteboard-reference action schemas for text/shape/line

* feat(prompts): whiteboard-reference action schemas for latex/chart/table

* feat(prompts): whiteboard-reference action schemas for code/delete/clear/close

* feat(prompts): whiteboard-reference LaTeX JSON escape section

* feat(prompts): whiteboard-reference bounds + overlap section

* feat(prompts): whiteboard-reference font + latex height tables

* feat(prompts): whiteboard-reference pre-output checklist

* feat(prompts): expand teacher whiteboard template

* feat(prompts): expand assistant whiteboard template

* feat(prompts): expand student whiteboard template

* test(prompts): assert whiteboard-reference sections reach every role prompt

* tune(prompts): cap teacher element budget at 4/turn, encourage wb_clear on crowded board

* tune(prompts): simplify teacher template — single conservative rule + state-awareness emphasis

* tune(prompts): add LaTeX-height × text-fontSize pairing table for visual weight consistency

* feat(orchestration): preserve image parts from UIMessage to AI SDK ModelMessage

* feat(eval): attach prior-turn whiteboard screenshot as user message image part

Gated behind EVAL_ATTACH_PRIOR_SCREENSHOT=1 env. When on, captures a
screenshot after every turn (not just checkpoints) and attaches it as a
file part with mediaType:'image/png' on the next user message. Teacher
prompt gets a new 'Prior-state image' section telling the agent to
treat it as direct visual feedback.

* fix(orchestration): strip data: prefix before passing image to AI SDK

Vercel AI SDK's streamText/generateText treats ImagePart.image as a URL
to fetch when it's a string. data: URLs fail the http/https scheme check
and throw AI_DownloadError. Strip the data URL prefix and pass raw base64
with mediaType separately — AI SDK treats base64 strings as data content.

* refactor(whiteboard): add conflict summarizer, drop prior-screenshot feature

Eval across flash (image-on vs off × repeat-3) and pro (same) showed the
prior-screenshot image-input feature gives ~0 net benefit: +0.4 overall on
flash and −0.6 on pro. The image feedback loop's theoretical upside
(agent self-correcting from visual) didn't materialize — weak models
couldn't act on the image and strong models "found things to fix" and
over-corrected. Meanwhile programmatic geometry detection on the raw JSON
cleanly lifted overlap from 6.3 → 8.1 on flash.

So: remove the image-input plumbing, keep a programmatic equivalent.

## Removed (reverts 0e07e9e, b5675bc, image half of 097ec2f)

- lib/orchestration/ai-sdk-adapter.ts: revert multimodal content passthrough
- lib/orchestration/summarizers/message-converter.ts: revert image part
  extraction; user content is back to string-only
- lib/orchestration/summarizers/conversation-summary.ts: revert multimodal
  content flattening
- lib/orchestration/director-graph.ts: revert HumanMessage multimodal cast
- tests/orchestration/message-converter.test.ts: deleted (was purely
  image-conversion tests)
- eval/whiteboard-layout/runner.ts: drop image attach, vision gate,
  per-turn screenshot capture; revert to checkpoint-only capture
- Role templates (wb-teacher / wb-assistant / wb-student): strip
  "Prior-state image" sections

## Added

- lib/orchestration/summarizers/whiteboard-conflicts.ts: pure geometry
  detector. Computes bbox IoU, line segment vs bbox intersection, and
  canvas edge clipping on the current whiteboard state. Renders a text
  block for inclusion in the system prompt.
- lib/orchestration/summarizers/state-context.ts: wires the conflict
  block in after the whiteboard element listing.
- Role templates: replace image-reading guidance with concise
  "Layout conflicts" sections pointing at the computed list, with a
  strong anti-action anchor so agents don't self-trigger wb_clear when
  the list is empty.

## Eval instrumentation (orthogonal)

- EVAL_ENABLE_THINKING=1 opt-in to enable model thinking per-request.
  app/api/chat/route.ts reads body.thinking; runner.ts forwards it.
  Default chat path stays on enabled:false for latency.
- Per-turn wall-clock timing: eval/whiteboard-layout/{runner,reporter,
  types}.ts capture and report mean/p50/p95/total turn latency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(whiteboard): address code-review feedback

W1: drop stale "prior-state image" reference from conflict-block prose
    (lib/orchestration/summarizers/whiteboard-conflicts.ts) — the image
    feature was removed; the text now says "real visible problem on the
    current board".

W3: type `thinking` on StatelessChatRequest instead of reading it through
    a cast. `app/api/chat/route.ts` now accesses `body.thinking` directly;
    frontend callers can discover the field via TS completion.

W4: unify canvas height constant to 563 (matching the actual rendered
    pixel count from capture.ts). The geometry detector's
    CANVAS_HEIGHT, the snippet's Dimensions / coordinate system /
    layout-guide text, and examples all now agree.

N6: add tests/orchestration/whiteboard-conflicts.test.ts — 17 cases
    covering empty input, bbox overlap threshold (30%, 50%, 100%, plus
    10% sub-threshold), line-crosses-bbox (through, endpoint-inside,
    path-above), canvas clipping on all 4 edges, exact-edge placement
    (not reported), malformed elements (skipped not crashed), and
    output format.

All 43 tests pass; tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(eval): finish canvas-height 563 unification

Second-pass code review caught one residual 562.5 in app/eval/whiteboard/page.tsx
that the first cleanup (commit 7a006bb) missed. The 0.5px disagreement was
harmless in practice (Playwright captures rounded to 563), but inconsistent
with the geometry detector and the prompt snippet now agreeing on 563.

All 43 tests pass; tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: prettier format whiteboard-conflicts.test + eval/runner

CI's `pnpm check` (prettier --check) caught two unformatted files:
- eval/whiteboard-layout/runner.ts
- tests/orchestration/whiteboard-conflicts.test.ts

Auto-fixed via `pnpm prettier --write`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-04-25 15:18:41 +08:00
wyucandClaude Opus 4.7 550c43a3f3 fix(generation): unify languageDirective across outline + scene pipeline (#472) (#474)
* fix(generation): unify languageDirective across outline + scene pipeline (#472)

Root issue: languageDirective was dropped, renamed, or hardcoded at several
points in the scene-generation pipeline, silently degrading prompt-level
language control. Slide paths survived because their fallback happened to
hint requirement-language; widget paths degraded to zh-CN default.

Fix plan (all code-only threading + prompt hygiene, no schema changes):

1. Thread languageDirective through every buildPrompt in scene-generator.ts:
   - Add param to generateSlideContent / generateQuizContent / generatePBLSceneContent
   - Forward from generateSceneContent's slide / quiz / pbl branches
   - Include key in the four generateSceneActions buildPrompt calls
   - Demote generatePBLSceneContent's hardcoded English to a fallback
   - Drop the dishonest `as 'zh-CN' | 'en-US'` cast on the widget path

2. Unify variable naming across widget templates + callers:
   - Rename {{language}} → {{languageDirective}} in 6 widget templates
     (simulation / diagram / code / game / visualization3d / widget-teacher-actions)
   - generateWidgetContent and generateWidgetTeacherActions now pass
     `languageDirective: languageDirective || ''` instead of a locale-code shim

3. Make the outline LLM reliably emit the wrapper object:
   - requirements-to-outlines/system.md + user.md restructured so the
     "top-level JSON object with languageDirective + outlines" contract is
     surfaced up-front, short, and unambiguous — drowning-by-scene-examples
     removed. Important Reminders reordered: top-level shape first.
   - interactive-outlines/system.md + user.md aligned with main outliner:
     adds full Language Inference section, requires wrapper output, drops
     the orphan {{language}} placeholder that never had a value source.

4. Remove lib/prompts/loader.ts prompt/snippet caches:
   - Disk reads for prompt templates are not on any hot path (LLM call
     dominates by 5 orders of magnitude). The cache's only observable
     effect was blocking prompt edits from taking effect without restart
     — which masked the wrapper bug during this investigation.

Verification:
- 7 new regression tests in tests/generation cover directive threading at
  each buildPrompt site plus the PBL caller-forwarding guarantee.
- outline-language eval (50 scenarios, gemini-3-flash-preview inference,
  qwen-3.5-flash judge): 48/50 (96%). The 2 persistent failures are in
  the proxy-request category (teacher / tutor preparing lessons for
  students) where the system prompt's rule 5 has genuine ambiguity —
  orthogonal to this fix.
- Curl burst tests: 8/8 runs each for regular-mode and interactive-mode
  outline endpoints emit the wrapper object and extract a detailed
  inferred directive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: apply prettier formatting to loader.ts and new test file

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: thread languageDirective in scene-builder and classroom-generation

Code review caught two additional sites missing the directive:
- lib/server/classroom-generation.ts:371 — generateSceneActions call
  was missing languageDirective, reproducing the issue #472 symptom
  in the shareable-classroom server generation path.
- lib/generation/scene-builder.ts:114 — buildSceneFromOutline's
  generateSceneActions call had the same miss; latent (no current
  live caller) but wrong for consistency.

Also renumber duplicate `8.` in requirements-to-outlines/system.md
scene-level rules.

Extend regression tests with a buildSceneFromOutline end-to-end case
that captures both the content and actions prompts via a stub aiCall
and asserts the directive reaches both phases — this would have
caught the scene-builder miss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: widget-path test + docs + fallback constant (PR #474)

Address @cosarah's review feedback:

- Add widget-path regression test (tests/generation/...): interactive
  scene with widgetType=simulation captures both widget content and
  widget-teacher-actions prompts, asserts the directive reaches each
  and no `{{language}}` / `{{languageDirective}}` placeholder leaks.
  This is exactly the path issue #472 was filed for.

- Rewrite lib/prompts/README.md "Caching" section: the cache was
  removed in this PR; README still instructed callers to use a
  clearPromptCache() function that no longer exists.

- Sync interactive-outlines/user.md with the main outliner's
  "## Language Context" reminder block (line 13-19) so both outline
  paths give the LLM the same language-inference reminders.

- Hoist 'Teach in the language that matches the user requirement.'
  into DEFAULT_LANGUAGE_DIRECTIVE, exported from outline-generator.
  Replaces 4 copies across outline-generator, scene-generator, and
  the scene-outlines-stream route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:00:06 +08:00
wyucandClaude Opus 4.7 f40c92f1aa refactor(orchestration): migrate prompt builders to file-based templates (#459)
* refactor(prompts): move loader to lib/prompts/

Move file-based prompt loader from lib/generation/prompts/ to
project-level lib/prompts/ so it can be shared by orchestration
and PBL prompts in subsequent commits.

- Templates and snippets moved alongside the loader
- getPromptsDir() now points at lib/prompts/
- Generation pipeline imports updated to @/lib/prompts
  (outline-generator, scene-generator, search-query-builder,
   scene-outlines-stream route)
- Add tests/prompts/loader.test.ts as sanity gate

Pure file move — no behavior change.

* test(orchestration): add baseline snapshots for prompt builders

Snapshot tests cover variant matrix:
- buildStructuredPrompt: role × scene-type × peer/ledger/discussion/profile
- convertMessagesToOpenAI: mixed message kinds
- summarizeConversation: truncation
- buildDirectorPrompt: Q&A vs discussion, ledger, profile
- buildPBLSystemPrompt: default config

These lock current output so the body refactor in subsequent
commits can be verified byte-equal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(orchestration): plug snapshot coverage gaps from review

- Split convertMessagesToOpenAI test: same-agent (existing) +
  cross-agent (new). The original test passed currentAgentId
  matching the message's agentId, so the cross-agent role-conversion
  branch was never exercised.

- Add 'teacher / quiz scene' snapshot to lock the spotlight/laser
  strip path in getEffectiveActions. The previous variant-7 test
  (whiteboard-open) only triggered the mutual-exclusion warning,
  not the strip — renamed it to reflect what it actually tests.

- Add assistant-role variant (was uncovered: ROLE_GUIDELINES,
  buildLengthGuidelines, buildWhiteboardGuidelines all branch on
  role).

* refactor(orchestration): extract summarizers from prompt-builder.ts

Move state-context, virtual whiteboard ledger replay, peer context,
message converter, and conversation summary out of the 890-line
prompt-builder.ts into focused modules under lib/orchestration/summarizers/.

prompt-builder.ts now only owns prompt assembly. director-graph
imports updated to the new locations.

Snapshot tests pass byte-equal — no behavior change.

* refactor(orchestration): move agent-system prompt to template file

The buildStructuredPrompt template literal is now
lib/prompts/templates/agent-system/system.md, assembled by a
thin variable-pass through the shared prompt loader.

Per-variant ternaries (format example, ordering, spotlight
examples, mutual-exclusion note) are kept as module-level
constants in prompt-builder.ts. ROLE_GUIDELINES,
buildLengthGuidelines, and buildWhiteboardGuidelines stay too
(they may further migrate to template partials in Phase 2).

Also: PROMPT_IDS gains a 'satisfies Record<string, PromptId>'
clause to prevent constant/union drift as more IDs are added.

Snapshot tests (12 prompt-builder + 4 director + 1 pbl) pass
byte-equal.

* refactor(prompts): align agent-system template with project conventions

- Rename agent-system placeholders snake_case → camelCase to match
  the convention already used by slide-actions/user.md and other
  generation templates. Settles convention before Task 5 lands two
  more templates that would otherwise inherit drift.

- Extract '## Speech Guidelines (CRITICAL)' into
  lib/prompts/snippets/speech-guidelines.md for reuse by director
  and PBL prompts in Task 5.

Snapshot tests pass byte-equal — placeholder name changes are
invisible in rendered output, and snippet inclusion happens at
load time.

* refactor(orchestration,pbl): move director + PBL prompts to templates

director-prompt.ts and pbl-system-prompt.ts now use the shared
template loader. Bodies collapse to thin variable assembly.

Three orchestration-tier prompts now share one infrastructure:
agent-system, director, pbl-design.

Snapshot tests pass byte-equal.

* refactor(orchestration): extract WhiteboardActionRecord + AgentTurnSummary to types module

These two interfaces are imported by 6+ modules including
summarizers/ — having them live in director-prompt.ts created
an awkward upward import direction (summarizers reaching back
to a sibling prompt builder for types).

Move them to lib/orchestration/types.ts and update all callers.
director-prompt.ts now imports them too.

Pure type-location refactor — snapshots pass byte-equal.

* test: remove snapshot tests in favor of eval-as-integration-test

Per review feedback: byte-equal snapshots were the wrong invariant
for this refactor. The goal was a stable substrate for editing
prompts, not a frozen contract. Snapshots would have generated
maintenance churn (~100-200 line diffs per intentional Phase 2
prompt tweak) without providing value the eval doesn't already.

Test posture going forward:
- tests/prompts/loader.test.ts (3 tests) — bounds the loader
  infrastructure (template load, snippet inclusion, variable
  interpolation, missing-id behavior)
- pnpm eval:whiteboard — bounds end-to-end agent loop, template
  composition, and integration with chat/director/state-manager

Removed: 1879 lines of .snap + 320 lines of test scaffolding +
fixtures. Net PR diff drops by ~2200 lines.

* docs(prompts): add README + structural assertion tests (review feedback)

Addresses two items flagged by PR #459 final review:

1. lib/prompts/README.md — conventions, syntax, gotchas, and local-testing
   recipe for template authors. Previously there was no doc for the stated
   goal of "non-engineers can edit prompts."

2. tests/prompts/templates.test.ts — 8 structural assertions covering:
   - no surviving {{...}} placeholders in any rendered template
   - role dispatch (teacher → LEAD TEACHER, student → not)
   - scene-type action stripping (quiz scene has no spotlight/laser)
   - director prompt output spec mentions next_agent

   These replace the removed byte-equal snapshot suite at much lower
   maintenance cost — they assert behaviors the refactor must preserve,
   not exact bytes.

* chore(prompts): relocate templates added on main during rebase

PR #461 (interactive mode clean) added 7 new templates under
lib/generation/prompts/templates/ (the pre-refactor location).
Rebase onto origin/main landed them at the old path; git's rename
detection didn't forward them through the directory move in db56c40.

Manually move them to lib/prompts/templates/ to match the new
convention, and fix scene-generator.ts's relative import
('./prompts/types' → '@/lib/prompts/types') since the dir no
longer exists as a sibling.

No behavior change — templates read from PROMPT_IDS values via
getPromptsDir() which already points at lib/prompts/.

* refactor(prompts): address review feedback from @cosarah

- loadSnippet now throws on missing file instead of silently returning
  a literal {{snippet:id}} string. A typo like {{snippet:speach-guidelines}}
  now fails at load time instead of reaching the LLM.

- README gains a "Still in TypeScript" section listing the role-conditional
  content that still lives in prompt-builder.ts (ROLE_GUIDELINES,
  buildLengthGuidelines, buildWhiteboardGuidelines) so contributors
  expecting a pure-markdown workflow know where to look.

- Expand tests/prompts/templates.test.ts to cover the conditional branches
  Phase 2 is most likely to touch (9 new assertions):
  - assistant role dispatch
  - peer-context section toggles on agentResponses presence
  - language constraint toggles on stage.languageDirective presence
  - director Q&A vs discussion mode branching
  - pbl-design {{issueCount}} substituted at all 3 occurrences
  - placeholder-naming-convention lint scans every template for
    non-camelCase placeholders (slide-content grandfathered)

- Comment above interpolateVariables regex documents why kebab-case
  placeholders pass through silently (the lint test now catches them).

- New test in loader.test.ts locks the throw-on-missing-snippet behavior.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:41:17 +08:00