Files
curriculum-project-hub/docs/adr/0010-diagnostics-legal-lesson-and-check-pipeline.md
T
sjfhsjfh c684d25d50 chore(spec+checker): cleanup pass — trim docs, reorg Courseware, covers-as-data, fold DanglingReference (ADR-0012)
Spec母本噪音清理 + 一处 spec↔impl 对齐 + 一处契约自洽修正。

Part A — spec doc 瘦身:每条 doc 收到"语义点 + 标签 + ADR ref + 承载性 why",
把跨文件复读的方法论(element-kind 开放性对比、likec4 画不出、分歧点测试)上移到
module header。OPEN 框架保持清晰(RunState/Capability 完整性仍明示须 surface)。

Part B — Courseware/ 由 10 文件平铺重组为 Model/ Export/ Check/ Open/ 四子命名空间
(namespace Spec.Courseware 不变,零引用改动)+ 四个子 aggregator。lake build 绿(24 jobs)。

Part C — 渲染覆盖落为数据(ADR-0011 对齐):去掉 cph-check 里硬编码的
COVERED_TARGETS,改读 TargetConfig.covers。cph-model 新增 covers: Option<Vec<String>>
(None=未声明,由 cph-check 默认为全部 known kinds;显式 [] 表示不覆盖任何 kind)。
新增 3 个覆盖行为测试。

ADR-0012 — DanglingReference 退役(诊断 7→6 类):其两种情形(未解析 @ref、越界/缺失
相对 import)都是 typst 编译期失败,归 typstCompile。同步移除 Oracle.refsResolve
(被 compiles 蕴含),Legal 少一个合取项。impl 删去从未被发射的 DiagCode::DanglingReference,
闭合 named-but-unemitted 的 spec↔impl 缝。ADR-0010 加修订指针。

OPEN 点(RunState/Capability 完整性、QuestionBank、Course)按既定纪律保持 OPEN,本次
只改善其框架措辞,不决策。

验证:spec lake build 绿;cargo test 全绿(含新增覆盖测试);clippy 静默;
TH-141 check 0 errors/0 warnings 无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:59:32 +08:00

8.2 KiB
Raw Blame History

ADR 0010: Checker Diagnostics, Legal Lesson, And The Check Pipeline

Status

Accepted — for the decisions below. Backfills the "full lesson legality" item ADR-0005 explicitly deferred ("the complete definition of a legal lesson; this ADR fixes only the one seed rule"). Builds on ADR-0005's seed (missing render ⇒ warning) and ADR-0009 (target = build). Some items deferred; see Open Questions.

Corrected by ADR-0012. The taxonomy below lists seven classes including DanglingReference. ADR-0012 retires DanglingReference — its cases (unresolved @ref, out-of-boundary import) are all typst compile-time failures, so they fold into TypstCompile — leaving six classes, and removes the Oracle.refsResolve conjunct from Legal. Read the taxonomy and the Legal definition here through that correction.

Context

The checker is the product's moat — the thing that "stands in Lean's position" at runtime, validating an engineering file and emitting helpful fix hints. The MVP implementation grew a diagnostic taxonomy (seven codes), a notion of when a lesson is legal (publishable / buildable), and an ordered check pipeline — and then treated all three as "implementation detail." They are not. What each error class means, whether it blocks, what "legal" is, and in what order checks run with what gating are exactly the non-obvious domain decisions the constitution (rule 5) says must be pinned: the developer and the agent would otherwise each assume a different answer. This ADR lifts them into the contract.

A deliberate boundary runs through the taxonomy. Some checks the model can state structurally (a manifest part points at a folder that exists; a declared kind is known). Others are external facilities the semantic model cannot itself decide — does this typst source compile? do its cross-references resolve? does this data conform to its JSON Schema? Those are real, contractually-named diagnostics, but their verdict comes from an implementation oracle (the typst compiler, the schema validator), not from Lean. The contract names them and fixes their severity; it does not pretend to decide them.

Decision

The diagnostic taxonomy (seven classes)

Each diagnostic carries a code, a severity (error | warning, per ADR-0005 / Spec.Courseware.Severity — two-valued, no info/note), a message, an optional source span, and a fix hint. The MVP classes:

Code Meaning Severity Kind
PartPathMissing A manifest [[parts]] entry points at a folder that does not exist (or escapes the root via ..). error structural
UnknownKind A part declares a kind not in the known kind set (and: a part/element.toml kind mismatch). error structural
MissingContentFile A content field required by the kind's schema has no corresponding <field>.typ on disk. error structural/schema
SchemaViolation Instance data does not conform to its kind's JSON Schema (bad scalar type, missing required, stray key); also the fallback for a malformed manifest.toml/element.toml. error schema / external
DanglingReference A reference does not resolve — a relative import outside the import boundary, or an unresolved cross-reference. error external
TypstCompile The assembled typst source fails to compile (syntax error, unresolved @ref, etc.). error external
RenderIgnored A used kind has no render rule for a declared target; the element is omitted from that export. warning semantic

Six are error (blocking); RenderIgnored alone is a warning — the ADR-0005 seed rule, restated. The split between structural (model decides), schema/external (oracle decides), and semantic (the coverage rule) is the boundary described above.

RenderIgnored's severity is pinned to warning as a named contract fact (Lean: renderIgnoredSeverity), so "it does not block export" is greppable and alignable, not an inline literal.

A lesson is legal (valid, publishable) iff the check pipeline produces no error-severity diagnostic. Warnings do not affect legality — a lesson with RenderIgnored warnings is still legal and still exports (lossily). This is the complete legality predicate ADR-0005 deferred: legality is defined by the diagnostic set, not by a separate hand-written rulebook. New rules enter legality by adding diagnostics, keeping one source of truth.

External-facility diagnostics are oracle verdicts, not model judgments

TypstCompile, DanglingReference, and the schema-conformance face of SchemaViolation assert facts the semantic model cannot decide on its own: they depend on running the typst compiler and the schema validator. The contract therefore models them as abstract predicates (e.g. "this source compiles", "these references resolve", "this data conforms") parameterized over an implementation-provided oracle. The Lean spec states that such a diagnostic exists, what it means, and its severity — and explicitly marks that its truth value is supplied by the implementation, not computed in Lean. This keeps the contract honest (it does not embed a typst compiler) while still naming the diagnostic as part of the legality definition.

The check pipeline: ordered phases with compile gating

check runs five phases in a fixed order, collecting all diagnostics:

  1. load — parse manifest.toml + each element.toml. A hard failure (no parseable lesson) stops the pipeline; nothing downstream can run.
  2. structural — every part path exists, no .., kind is known, part/element kinds agree. Parts already flagged missing here are skipped downstream (no piling diagnostics on one root cause).
  3. schema — each present, known-kind part's data validated against its kind schema (scalars + required content files).
  4. typst compile — the external-facility phase. Gated: it runs only if phases 13 produced zero errors, because compiling known-broken input yields noise on top of the real cause. When it runs, it compiles each declared target (ADR-0009), deduping identical diagnostics across targets.
  5. render-coverage — the semantic warning rule; runs regardless of gating.

build --target T runs phases 13, refuses (no artifact) if any error, else compiles + exports T's artifact (ADR-0009). It does not run coverage — coverage describes export loss, informational for check, not a gate on one target's build.

The order and the gating are contract, because they determine which diagnostics a user sees (a syntax error behind a missing file is hidden until the file exists — deliberate). The per-phase algorithms are not pinned in Lean (depth ceiling, rule 5): the spec fixes the phases, their order, and the gate, not how each check is computed.

Consequences

  • "Is this lesson done / publishable?" has one precise answer: no error-level diagnostic. The CLI's exit code (1 on any error) is this predicate.
  • Adding a checker rule = adding a diagnostic with a severity; legality updates automatically, no separate legality rulebook to keep in sync.
  • The contract can name compile/reference/schema diagnostics as part of legality without embedding the oracles that decide them — the spec stays a semantic master, not a re-implementation.
  • The implementation's DiagCode enum, CheckReport, and phase sequence now have a contract to align to (by review, per the constitution — no CI gate); the alignment is documented at both ends.

Open Questions / Deferred

  • Additional codes. A dedicated ManifestMalformed (today folded into SchemaViolation) and finer schema/reference codes may be added; the taxonomy is open to growth, each addition a deliberate decision.
  • Cross-lesson diagnostics. Anything about a course (arrangement of lessons) or the question bank is out of scope — those models are still OPEN (ADR-0005).
  • Severity of future classes. Each new diagnostic's blocking/non-blocking status is decided when introduced; only the seven above are fixed here.
  • Warning sub-structure. Whether warnings ever need ordering/grouping or a third "info" level remains deferred (ADR-0005 kept Severity two-valued).