feat(checker): compile template as main, augmented manifest closes optional-content (WU-D', ADR-0011)

Replace generated-driver compilation with the template model.

cph-typst:
- driver.rs DELETED (generate_driver, static-include driver, numbering-threading).
- New manifest.rs: build_augmented_manifest(&Lesson) emits a TOML doc with [info]
  + ordered [[parts]] each carrying a `fields` array = the kind's content fields
  whose <root>/<path>/<field>.typ exists on disk. Reuses cph-schema's
  content_field_names (new dep; no cycle). This closes the OPEN point WU-C'
  surfaced: typst has no file-exists primitive, so the engine (which has disk
  access) tells the template which optional content (lemma proof) is present.
- World main = the real engineering-file template (<root>/<template>); the
  augmented manifest is served as an in-memory virtual file at /.cph/manifest.toml
  (never written to the tree), injected via sys.inputs.manifest. render_dir kept
  only for @local/cph-render + vendored @preview/numbly resolution.
- Artifact handling: SingleFile+TypstCompile compiles; FileTree, shell-only, and
  undeclared-target are blocking "deferred/not-declared" diagnostics (ADR-0011).
- Engine public signatures unchanged; LessonWorld::new takes template+manifest_src.

cph-check: no source change needed (only uses lesson.targets/target.name +
unchanged Engine methods); pipeline + Legal alignment intact.

Fixtures: mini gets exports/{student,teacher}.typ (from render/templates/) + v2
manifest + a second proof-less lemma to exercise optional content. render-stub
retired (tests use the real render/). Through-template PDFs offline: student 44KB,
teacher 56KB; proof-less lemma compiles clean (optional-content confirmed).
Workspace: fmt + clippy -D warnings + all tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 10:05:03 +08:00
parent 7e15cf34f6
commit d76f9f9a54
15 changed files with 705 additions and 573 deletions
+18 -8
View File
@@ -12,11 +12,13 @@
//! 3. compute 1-based line/col from the file's [`typst::syntax::Source`] via
//! `lines().byte_to_line_column` (0-based ⇒ `+1`).
//!
//! A span whose `FileId` is the generated **driver** is re-pointed: its `file`
//! is set to the driver vpath and a hint flags it as driver-internal (a
//! driver-gen bug indicator) rather than dropped or mis-attributed to a user
//! file. Spans in the `cph-render` package are reported with an
//! `@local/cph-render:…` pseudo-path.
//! The `main` file is now the real **template** (`exports/<target>.typ`), so a
//! span there resolves naturally to an authored file — no re-pointing needed. A
//! span landing in the **virtual augmented manifest** ([`crate::MANIFEST_VPATH`],
//! served in-memory, not on disk) is reported with its vpath plus a hint marking
//! it as the framework-synthesized manifest (a manifest-generation bug
//! indicator) rather than mis-attributed to a user file. Spans in the
//! `cph-render` package are reported with an `@local/cph-render:…` pseudo-path.
use std::path::PathBuf;
@@ -55,10 +57,11 @@ pub fn map_diagnostic(world: &LessonWorld, d: &SourceDiagnostic) -> Diagnostic {
if let Some(id) = d.span.id() {
if let Some(file) = file_for(world, id) {
if id == world.main() {
if is_virtual_manifest(id) {
hints.push(
"this span is inside the generated driver (.cph/driver-*.typ), \
not a lesson source file — likely a driver-generation issue"
"this span is inside the framework-synthesized manifest \
(/.cph/manifest.toml), not a lesson source file — likely a \
manifest-generation issue"
.to_string(),
);
}
@@ -80,6 +83,13 @@ pub fn map_diagnostic(world: &LessonWorld, d: &SourceDiagnostic) -> Diagnostic {
diag
}
/// Whether `id` is the in-memory augmented-manifest virtual file
/// ([`crate::MANIFEST_VPATH`]) the engine injects (not a real lesson file).
fn is_virtual_manifest(id: FileId) -> bool {
matches!(id.root(), VirtualRoot::Project)
&& id.vpath().get_with_slash() == crate::MANIFEST_VPATH
}
/// 1-based (line, col) for a byte offset in file `id`, defaulting to `(1, 1)`
/// when the source can't be read or the offset is out of range.
fn line_col(world: &LessonWorld, id: FileId, byte: usize) -> (u32, u32) {