docs/current/05-教学体验/模板与验证解耦设计.md
GitHub ↗
当前有效

算法模板与验证解耦方案

5135 字·约 13 分钟 阅读 2026-10-10 17:07

状态:Phase 1~4 已实施完成,旧 Dart 硬编码模板框架已移除;Phase 4 的前端加载链路(历史资产,已迁出)已随 2026-09-11 前端切割移出仓库;Phase 2 的同步脚本 scripts/sync_templates.py 已由重构批次 R4 G1 于 2026-09-12 恢复为纯后端形态(渲染 + Clang Golden,不再产出 Flutter 资产)(现状见 §1.1、§3、§5.1、§5.4)
核心原则:算法模板即合法 C 代码 + 语法验证双重验证参照 + 自验证降低维护成本(后端原则不因切割改变)

最后核对日期:2026-10-07(删区批现役对照回灌——§2.3 双验证表加现役等价物注记、§三 目录树重写为 corpus/ + clang_direct 现役版〔原 native/tests 与 shadow_verify 双驱动已随删区物理删除〕;设计原则章不变。前一沿革 2026-09-29 全库逐份翻新复核)


一、问题背景

1.1 算法模板与前端耦合

当前 82 个算法模板硬编码在 CideFlutter/lib/models/templates/*.dart 中(已移除,历史资产,已迁出)。

新框架下:

  • 所有模板源码为合法 C/C++ 代码,存放于 templates/<key>/source.c(或 source.cpp)
  • 模板的消费方只剩后端防线:渲染产物 native/tests/cases_template_generated/*.c 被 Shadow Verification(防线 1)与 Rust E2E 直接复用
  • 前端(Flutter/Dart)运行时的 index.json + 模板源码加载链路已随 2026-09-11 前端切割整体移除;社区前端可直接把 templates/<key>/ 当作模板源读取,但本仓库不再提供 index 产物与加载器
  • ✅ 生成闭环已恢复(2026-09-12,R4 G1):同步脚本 scripts/sync_templates.py 已从历史提交恢复为纯后端形态,"渲染 templates/ → cases_template_generated/*.c + Clang Golden"重新可用;静态一致性校验 test_templates.py 仍未恢复(详见 §5.1 与 模板维护指南.md §4.2)

1.2 语法验证重复劳动

同一个 for 循环用例当前维护多份:

位置 形式 问题
Rust 单元测试 C 字符串 Parser/TypeChecker/VM 各测一遍
Python SHADOW_CASES Python 字符串 同一份代码再写一遍,且有 \n 转义陷阱
算法模板 Dart 文件 Dart 字符串 算法代码第三遍

1.3 隐形成本(本次设计重点解决)

隐形成本 根因 后果
模板无法直接编译检查 {{n}} 不是合法 C 作者无法立即用 Clang 验证语法
expected_stdout 静默过期 与源码示例数据分离 改数据后 expected 不匹配
Tutorial 锚点漂移 phase 与源码行号弱关联 重构后高亮错位

二、核心设计

2.1 算法模板:合法 C + 注释参数

占位符改为合法 C 注释标记,模板本身就是可编译的 C 代码。

// templates/bubble_sort/source.c
#include <stdio.h>

void bubbleSort(int arr[], int n) {
    // @tutorial-anchor: outer_loop
    for (int i = 0; i < n - 1; i++) {
        // @tutorial-anchor: inner_loop
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int arr[/*__PARAM_n__*/ 5] = {5, 3, 8, 1, 2};
    int expected[/*__PARAM_n__*/ 5] = {1, 2, 3, 5, 8};
    int n = /*__PARAM_n__*/ 5;

    bubbleSort(arr, n);

    int ok = 1;
    for (int i = 0; i < n; i++) if (arr[i] != expected[i]) ok = 0;
    printf(ok ? "OK\n" : "FAIL\n");
    return 0;
}

关键规则:

元素 语法 说明
参数占位 /*__PARAM_n__*/ 5 合法 C 注释,后接默认值,可被替换
教程锚点 // @tutorial-anchor: outer_loop 源码中的显式标记,重构时跟着代码走
自验证 expected[] + 循环比较 模板自身断言正确性,输出稳定 "OK\n"

替换逻辑:同步脚本扫描 /*__PARAM_{key}__*/\s*(\S+),将匹配到的整段替换为目标值。(该规则由 scripts/sync_templates.py(已退役) 执行——2026-09-12 重构批次 R4 G1 恢复为纯后端形态,见 模板维护指南.md §4)

验证方式:模板作者随时可 clang templates/bubble_sort/source.c 编译运行,确认语法和逻辑正确。

2.2 元数据:templates/<key>/meta.yaml

纯声明式,不重复源码中已存在的信息:

key: bubble
name: 冒泡排序
category: 排序

params:
  n:
    label: 数组长度
    type: int
    default: 5
    mutations:
      - name: small
        value: 2
      - name: large
        value: 10

tutorial:
  steps:
    - title: 外层循环
      description: 控制趟数,每趟将最大元素沉底
      anchor: outer_loop       # 对应 source.c 中的 @tutorial-anchor 标记
    - title: 内层循环
      description: 相邻元素比较
      anchor: inner_loop

knowledge_nodes:
  - Array
  - BubbleSort
  - BoundaryCondition

2.3 双重验证参照

删区后现役对照(2026-10-07 注):下表为历史双验证形态(Rust e2e + 影子验证驱动,均已随 2026-10-05 native/ 删区退役)。现役等价物 = clang golden(Clang 实跑产 .out 锁定,义务链见 Agent Skill vitro-baseline-corpus-workflow)+ clang_direct 直拍门禁(go run ./scripts/clang_direct,corpus 六目录全量逐例对照,known 白名单 digest 锁定)+ e2e(corpus/ 四域,MoonBit cmd 侧驱动)——「双重验证」设计原则不变,载体已换。

维度 Rust e2e 集成测试 Python 影子验证
参照系 Golden .out 文件(Clang 生成并锁定) Clang 实时输出
验证目标 Vitro 行为是否符合锁定预期 Vitro 行为是否与标准 C 一致
发现问题 Vitro 自身 bug / Golden 过期 缺失特性 / 语义偏差 / 标准不符
运行方式 cargo test --test vitro_e2e(已随删区退役) go run ./scripts/shadow_verify(已随删区退役;2026-09-13 D5 起 Go 驱动、Python 版更早退役)
依赖 仅需 Vitro 自身 需 Clang
反馈速度 秒级 分钟级

交叉验证场景:

Rust e2e Python 影子 结论
✅ ✅ 健康
❌ ❌ 预期值/Golden 写错,或两者都对但输出不同
✅ ❌ Vitro 行为自洽但与标准 C 偏差
❌ ✅ Golden 文件过期,需重新生成
- compile_gap 缺失特性或编译器 bug

三、目录结构(2026-10-05 删区后现役)

project_root/
│
├── templates/                          # 算法模板(人类维护)
│   ├── bubble_sort/
│   │   ├── source.c                    # 合法 C,含 /*__PARAM_n__*/ 和 @tutorial-anchor
│   │   └── meta.yaml                   # 元数据 + 教程声明
│   └── ...
│
├── corpus/                             # C 语料(原 native/tests/cases/ 等随删区迁入——单一事实来源)
│   ├── baseline/                       # 已支持特性(385 例)
│   ├── knr/ · leetcode/ · gap/         # 其余三域(81 / 138 / 18 例)
│   ├── codegen_skeleton/ · cpp/        # 骨架域与 C++ 存量语料
│   ├── template_generated/             # 模板生成用例(82 个 .c,可重生成;生成器 sync_templates.py 已随删区退役)
│   └── (golden 义务链:Clang 实跑产 .out 锁定——见 Agent Skill vitro-baseline-corpus-workflow)
│
└── scripts/
    └── clang_direct/                   # Clang 直拍门禁(现役——吸收原影子验证防线,known_direct.json 白名单 digest 锁定)

删区注记(2026-10-05):下述路径已随 native/ 物理删除——native/tests/{cases,cases_golden,cases_template_generated,shadow_verification}/、scripts/shadow_verify{,_cpp}/(Go 驱动)、scripts/sync_templates.py;更早的前端切割期迁出件(CideFlutter/assets/templates/、Python 驱动三件等)见 2026-09-11 注记与 git 历史。历史形态均见 tag rust-oracle-freeze。


四、文件格式规范

4.1 算法模板:templates/<key>/source.c

#include <stdio.h>

void bubbleSort(int arr[], int n) {
    // @tutorial-anchor: outer_loop
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int arr[/*__PARAM_n__*/ 5] = {5, 3, 8, 1, 2};
    int expected[/*__PARAM_n__*/ 5] = {1, 2, 3, 5, 8};
    int n = /*__PARAM_n__*/ 5;

    bubbleSort(arr, n);

    int ok = 1;
    for (int i = 0; i < n; i++) if (arr[i] != expected[i]) ok = 0;
    printf(ok ? "OK\n" : "FAIL\n");
    return 0;
}

4.2 语法验证用例:native/tests/cases/baseline/*.c

// @category: baseline
// @features: for_loop, array_index, printf
// @expected_stdout: 0 1 2
int main() {
    for (int i = 0; i < 3; i++) printf("%d ", i);
    return 0;
}
注释标记 用途 消费者
// @category: baseline / gap / arch_diff_bug shadow(Go 驱动)、Rust e2e
// @features: 特性标签(逗号分隔) ⚠️ 暂无消费者:特性矩阵脚本未实现,现行 shadow 驱动(Go)也不解析该标记
// @expected_stdout: Rust e2e 断言预期 Rust e2e

4.3 Golden 输出文件:cases_golden/<name>.out

纯文本,无注释,就是 stdout 的精确字节:

ative/tests/cases_golden/bubble_sort_default.out
---
OK

生成方式:同步脚本先用 Clang 渲染并运行,stdout 写入 .out,首次需人工 Review 后提交锁定。同步脚本已由 R4 G1 恢复(2026-09-12):改模板后运行 python scripts/sync_templates.py(已退役) 即可重新生成;人工核对流程见 模板维护指南.md §4.3。


五、消费端实现

5.1 同步脚本(✅ 已由 R4 G1 恢复为纯后端形态 · 2026-09-12)

现状(2026-09-13 核对):scripts/sync_templates.py 已从历史提交恢复(重构批次 R4 G1,提交 3c767bb),只保留后端职责:渲染 templates/<key>/source.c → cases_template_generated/*.c、Clang 生成/锁定 Golden、扫描 // @tutorial-anchor: 锚点。原第 4、5 项 Flutter 职责随前端切割永久移除。scripts/test_templates.py(静态一致性校验)仍未恢复,模板目录核对依赖人工(见 模板维护指南.md §5.1)。

历史职责清单(保留供追溯):

  1. 渲染 templates/<key>/source.c → native/tests/cases_template_generated/<key>_<case>.c(替换 /*__PARAM_x__*/,解析逻辑为正则 /\*__PARAM_(\w+)__\*/\s*(\S+),未声明参数时保留源码中的默认值)——✅ 已恢复;
  2. 调 Clang 编译运行渲染结果,生成/锁定 native/tests/cases_golden/<key>_<case>.out(仅当 .out 不存在时生成,避免覆盖人工 Review 过的 Golden)——✅ 已恢复;
  3. 扫描 // @tutorial-anchor: <name> 得到 {name: line_number} 映射——✅ 已恢复;
  4. 汇总元数据 + 锚点生成 CideFlutter/assets/templates/index.json(历史资产,已迁出)——⛔ 永久移除;
  5. 把 source.c / source.cpp 复制到 CideFlutter/assets/templates/<key>.c/.cpp(历史资产,已迁出)——⛔ 永久移除。

剩余缺口:

  • native/tests/cases_template_generated/ 的用例现已可随 python scripts/sync_templates.py 重新生成(改模板后运行即可);Golden 仅在 .out 不存在时生成,人工 Review 纪律保持;
  • 静态一致性校验(模板目录完整性 / params 与占位符对应)仍无自动化,是尚未排期的缺口,如实记录,不粉饰。

历史实现骨架(已删除,仅保留算法要点供恢复参照):

# ⚠️ 历史资产,已迁出:本代码块为已删除脚本的实现骨架,勿直接使用
PARAM_RE = re.compile(r'/\*__PARAM_(\w+)__\*/\s*(\S+)')   # 占位符正则

def render_template(source, args):
    # 用 args 覆盖占位符;args 中缺失的 key 保留源码中的默认值
    ...
def scan_tutorial_anchors(source):
    # 扫描 // @tutorial-anchor: <name>,返回 {name: line_number}
    ...
def sync():
    # 1) 渲染 templates/<key>/source.c → cases_template_generated/<key>_<case>.c(首行补 // @category:)
    # 2) 仅当 cases_golden/<key>_<case>.out 不存在时,用 Clang 运行并写入 Golden
    # 3) 汇总 meta.yaml + 锚点 → CideFlutter/assets/templates/index.json(已随前端迁出)
    # 4) 复制 source.c/source.cpp → CideFlutter/assets/templates/<key>.c/.cpp(已随前端迁出)
    ...

5.2 Rust e2e 集成测试

// native/tests/vitro_e2e.rs

use std::fs;
use std::path::Path;

struct TestCase {
    name: String,
    source: String,
    expected_stdout: String,
    category: String,
}

fn load_cases(dir: &str) -> Vec<TestCase> {
    let mut cases = vec![];
    for entry in fs::read_dir(dir).unwrap() {
        let path = entry.unwrap().path();
        if path.extension().unwrap_or_default() != "c" {
            continue;
        }
        let source = fs::read_to_string(&path).unwrap();
        let expected = extract_comment_tag(&source, "@expected_stdout");
        let category = extract_comment_tag(&source, "@category")
            .unwrap_or_else(|| "baseline".into());
        
        cases.push(TestCase {
            name: path.file_stem().unwrap().to_string_lossy().into(),
            source: source.clone(),
            expected_stdout: expected.unwrap_or_default(),
            category,
        });
    }
    cases
}

fn load_golden_cases(cases_dir: &str, golden_dir: &str) -> Vec<TestCase> {
    let mut cases = vec![];
    for entry in fs::read_dir(cases_dir).unwrap() {
        let path = entry.unwrap().path();
        if path.extension().unwrap_or_default() != "c" {
            continue;
        }
        let name = path.file_stem().unwrap().to_string_lossy().to_string();
        let golden_path = Path::new(golden_dir).join(format!("{}.out", name));
        if !golden_path.exists() {
            continue;  // 无 Golden 则跳过(或报错)
        }
        let source = fs::read_to_string(&path).unwrap();
        let expected = fs::read_to_string(&golden_path).unwrap();
        let category = extract_comment_tag(&source, "@category")
            .unwrap_or_else(|| "baseline".into());
        
        cases.push(TestCase { name, source, expected_stdout: expected, category });
    }
    cases
}

#[test]
fn e2e_baseline_cases() {
    for case in load_cases("tests/cases/baseline") {
        if case.category == "gap" { continue; }
        let output = run_vitro(&case.source);
        assert_eq!(output.stdout.trim(), case.expected_stdout.trim(),
            "[{}] stdout mismatch", case.name);
    }
}

#[test]
fn e2e_template_cases() {
    for case in load_golden_cases(
        "tests/cases_template_generated",
        "tests/cases_golden"
    ) {
        let output = run_vitro(&case.source);
        assert_eq!(output.stdout.trim(), case.expected_stdout.trim(),
            "[{}] golden mismatch", case.name);
    }
}

5.3 Python 影子验证

shadow_verify.py 完整保留现有 analyze_diff 四分类逻辑,仅简化用例加载:

def load_case_files() -> List[ShadowCase]:
    """从 .c 文件加载用例,替代硬编码 SHADOW_CASES 列表"""
    cases = []
    for root in [
        Path("tests/cases/baseline"),
        Path("tests/cases_template_generated")
    ]:
        if not root.exists():
            continue
        for path in root.glob("*.c"):
            source = path.read_text(encoding="utf-8")
            meta = parse_comment_tags(source)
            cases.append(ShadowCase(
                name=path.stem,
                source=source,
                category=meta.get("category", "baseline")
            ))
    return cases

SHADOW_CASES = load_case_files()

# analyze_diff、classify_compile_error、generate_report 完全保留原有逻辑

5.4 前端加载链路(⚠️ 已随 2026-09-11 前端切割迁出 · 现状说明)

现状:CideFlutter/ 整目录、assets/templates/index.json 与 Dart 加载器(TemplateRepository / CodeTemplate / TemplateBar 等,均为历史资产,已迁出)均不在本仓库中。本仓库不再产出 index.json,也不再提供任何运行时模板加载实现。

仍有效的后端契约(社区前端接入时应遵守):

契约 形式
模板源位置 templates/<key>/source.c 或 source.cpp(二选一)+ meta.yaml
参数占位符 /*__PARAM_<key>__*/ <默认值>(合法 C 注释,可被 Clang/Vitro 直接编译)
教程锚点 源码中的 // @tutorial-anchor: <name>,其行号即高亮行号,无需额外维护
元数据 meta.yaml 的 key / name / category / params / tutorial.steps / knowledge_nodes
生成用例(可作对照素材) native/tests/cases_template_generated/<key>_default.c 与 native/tests/cases_golden/<key>_default.out

历史实现(已迁出,供参照):原 Dart 侧从 assets/templates/index.json 读取模板清单,再按需 rootBundle.loadString('assets/templates/<key>.c') 加载源码;ext 字段决定加载 .c 还是 .cpp。该实现见标签 before-frontend-split。


六、特性矩阵生成(可选增值 · 未实现)

现状核实(2026-09-11):scripts/feature_matrix.py 不存在,shadow_verify.py 也不解析 // @features: 注释(ShadowCase 无 features 字段)。本节为规划草案,下例为伪代码。

从 // @features: 注释自动生成特性支持矩阵:

# scripts/feature_matrix.py(规划中,文件尚不存在)
from collections import defaultdict

features = defaultdict(lambda: {"cases": 0, "match": 0})
for case in load_case_files():
    for feat in case.features:
        features[feat]["cases"] += 1
        if case.diff_type == "match":
            features[feat]["match"] += 1

# 输出:for_loop: 15/15 (100%), array_index: 12/12 (100%), ...

教学价值:直接回答"Vitro 支持哪些 C 语法特性"。


七、迁移路径

Phase 1:文件化影子用例 ✅

  1. ✅ 创建 native/tests/cases/baseline/
  2. ✅ 将 shadow_verify.py 中 baseline 用例逐个提取为 .c 文件
  3. ✅ 每个 .c 文件添加 // @category 和 // @expected_stdout 注释
  4. ✅ shadow_verify.py 改为 load_case_files() 扫描目录
  5. ✅ 验证:跑一次 python shadow_verify.py,match 率不下降

Phase 2:模板文件化 + sync 脚本 ✅(脚本已于 2026-09-11 随前端切割删除)

  1. ✅ 创建 templates/<key>/source.c(合法 C + /*__PARAM__*/)+ meta.yaml
  2. ✅ 实现 scripts/sync_templates.py(历史资产,已迁出):
    • 渲染 /*__PARAM__*/ → cases_template_generated/*.c
    • Clang 运行生成 cases_golden/*.out
    • 生成 CideFlutter/assets/templates/index.json(历史资产,已迁出)
  3. ✅ 验证 Rust e2e 和 Python 影子都能加载新生成的模板用例

现状(2026-09-13 更新):该脚本已由 R4 G1 恢复(2026-09-12);cases_template_generated/ 的 82 个 .c 可随 python scripts/sync_templates.py 重新生成,后端自动闭环恢复(静态一致性校验仍缺,见 §5.1)。

Phase 3:Rust e2e 集成测试 ✅

  1. ✅ 新建 native/tests/vitro_e2e.rs
  2. ✅ 实现 load_cases() 扫描 cases/baseline/
  3. ✅ 实现 load_golden_cases() 扫描 cases_template_generated/ + cases_golden/
  4. ✅ cargo test --test vitro_e2e 跑通全部 baseline + 模板用例

Phase 4:Flutter 运行时加载 + 删除旧框架 ✅(前端链路已随 2026-09-11 切割整体迁出)

  1. ✅ pubspec.yaml 注册 assets/templates/
  2. ✅ TemplateBar / TemplateParamDialog / TemplateTutorialPanel 改为消费 JSON Index + .c 文件
  3. ✅ 删除 CideFlutter/lib/models/templates/*.dart 硬编码模板文件(11 个 Dart 文件已移除;历史资产,已迁出)
  4. ✅ template_registry.dart 清理:移除 allTemplates fallback 及全部旧 import(历史资产,已迁出)
  5. ✅ code_template.dart 移除旧语法 {{key:defaultValue}} 支持,仅保留 /*__PARAM__*/(历史资产,已迁出)
  6. ✅ @tutorial-anchor 扫描驱动高亮

现状(2026-09-11 补记,不改动上述历史条目):CideFlutter/ 整目录(含 assets/templates/ 与 index.json)已移出本仓库,上述 1、2、4、5、6 项作为历史实现保留在标签 before-frontend-split。后端仍然有效的只是数据约定:模板源为合法 C/C++、占位符为 /*__PARAM_x__*/、锚点为 // @tutorial-anchor: <name>。运行时加载与渲染属社区前端职责。


八、与现有系统兼容性

现有系统 影响 措施
shadow_verify.py 报告格式 无 analyze_diff / generate_report 逻辑不变
SHADOW_CASES 硬编码列表 删除 改为 load_case_files() 扫描 .c
Rust 单元测试(分层) 无 lexer/tests.rs / parser/tests.rs 等保持原样
CodeTemplate Dart API 已迁出 随 CideFlutter/ 于 2026-09-11 移出仓库(历史资产,已迁出)
template_registry.dart 已迁出 同上(历史资产,已迁出)
教程高亮 契约保留、实现移出 契约从"行号绑定"升级为 @tutorial-anchor 源码标记(后端数据约定仍有效,渲染由社区前端实现)
templates/ 源目录 闭环已恢复 由 scripts/sync_templates.py(R4 G1)消费;静态一致性校验仍缺(见 §5.1)

九、风险与回退

风险 缓解措施
/*__PARAM_n__*/ 默认值与替换值类型不匹配 由恢复后的 scripts/sync_templates.py(已退役) 在渲染后先调 Clang 编译、失败则阻断(R4 G1 恢复版,2026-09-12);人工流程(模板维护指南.md §4)保留为第二道
Golden .out 首次生成错误 必须人工 Review 后提交锁定;CI 对 .out 变更保持敏感
@tutorial-anchor 注释遗漏 恢复版脚本扫描锚点;"meta.yaml 的 anchor 必须存在于 source.c"的静态一致性校验仍缺(缺口保留)
模板源与生成用例漂移 templates/ 已有消费者(sync_templates.py → 生成用例 → shadow/e2e 防线);meta↔source 静态一致性自动校验仍缺(见 §5.1,缺口保留)
Flutter 运行时加载性能 已随前端迁出,属社区前端自评范围(历史资产,已迁出)
Windows 无 Clang shadow 驱动(Go)含并发 Defender 锁重试判据;Rust e2e 为必过项

十、总结

算法模板是合法 C 代码(/*__PARAM__*/ 注释占位),作者可直接用 Clang 验证;模板自身断言正确性(expected[] 自验证),输出稳定 "OK\n",Golden 文件由 Clang 生成并锁定;同一份 .c 文件,Rust e2e 以 Golden 为参照验证 Vitro 自洽性,Python 影子以 Clang 实时输出为参照验证标准一致性——双重独立,交叉验证,Python 不降级。

2026-09-11 补记(切割后现状):上述设计原则全部保留且不因前端切割改变——模板即合法 C、Golden 只能来自 Clang、双重参照交叉验证仍是后端防线的基础。发生变化的只有"谁来消费模板":Dart 前端迁出、模板的运行时展示移交社区前端。templates/ → cases_template_generated/ → Shadow/E2E 的后端生成链路曾一度失去生成器脚本(2026-09-11),已由 R4 G1 于 2026-09-12 恢复(详见 §5.1)。