working-tree-tapp-cli-npx:把 Tapp 校验器变成可分发的契约工具

Myriad · HEAD 61ea971 · preview 工作树 · 2026-07-20 · 自包含,读完即弃

37 个工作树路径
+279 / −125 个已跟踪差异
4,564 行新增材料
16/16 CLI 测试通过

写法说明:本文按「逐跳走读」展开。代码片段均来自本次工作树中亲自读取的文件;青色斜体注释为解读所加,灰色斜体保留源码注释。每条旅程末尾给出排查路标。

1TL;DR

这组变更把 Tapp 校验规则从后端代码里的分散事实,变成一份可导出的契约:Rust 类型提供结构层,Rust 常量提供语义层,前端权限表提供 action/permission 目录,最后汇合到生成的 JSON。

CLI 通过这份 JSON 做 Manifest、资源、静态代码和权限检查;只有 starter 模板与 ZIP 机制保留手写。随后 CLI 被整理成 npm scoped package,发布前重新同步契约并跑测试,用户可用固定版本的 npx 在本地、Playground 和 CI 中复用检查器。

核心心智模型:这不是孤立的本地脚本,而是由 Myriad 后端契约生成、随 npm 包分发、运行时离线消费的检查器;后端安装器仍是最终权威。

2变更地图(称重)

这是一份混合工作树,而非已提交 PR。已跟踪差异主要是后端规则抽取;未跟踪材料承载 CLI、生成产物、测试、样例工程和 handoff。

tools/tapp-cli
约 2,153 行 · 入口/检查/测试
contract export
约 1,773 行 · schema/rules/json
my-app
约 699 行 · 验收样例
backend/docs
约 600 行 · 接线与文档
设计重心(要细读)可放心略过
contract_rules.rsmanifest.rs、exporter、sync-contract.mjs:定义契约来源与汇合点。Cargo.lock:依赖解析结果;样例 SVG、locale 和模板:用于验证布局。
project.mjs、CLI 入口与测试:决定用户运行时能得到什么。contract.json 大部分行:生成结果,不应逐字段手工维护。

3架构一图流

以前 · 规则只在后端路径

Manifest 类型
serde + 手写校验
安装器
第三方工程
无统一本地检查入口
插件设想

现在 · 源码契约进入多个消费者

Rust schema/rules
cargo exporter
contract.json
contract.json
CLI + npx
本地/Playground/CI

明确的新边界是:生成器只在 Myriad 源码和发布流程中运行;用户侧 CLI 只读取随包分发的契约。

4数据与状态先行

形状来源下游用途
TappManifest JSON Schemamanifest.rsJsonSchema 派生字段、嵌套对象、required、Rust enum
limits/rules/patternscontract_rules.rs 常量条件字段、数量、大小、路径、权限关系
permissions前端 permissionConfig.tsaction → permission、permission → level
inspectProject() reportproject.mjs诊断、缺少权限、调用位置、待打包文件
backend/src/api/tapp_store/manifest.rs结构层入口(节选)
#[cfg_attr(feature = "tapp-contract-schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TappManifest {
    pub id: String,
    pub name: String,
    pub version: String,
    pub main: String,
    #[serde(default)]
    pub permissions: Vec<String>,
    pub apis: Option<HashMap<String, TappApiDef>>,
}

类型负责“有哪些字段、字段长什么样”;像 Widget 权限、事件 topic 前缀、API 互斥字段这样的关系进入语义层。

5旅程 A:一条规则如何变成生成契约

全景 · 涉及 5 个核心文件
类型语义常量Rust exportersync-contractcontract.json

A.1结构和语义被刻意拆开

manifest.rs 的结构与 enum 在 feature 打开时派生 Schema。分类别名不是 canonical enum,因此别名进入语义规则,CLI 再合并两组可接受值。

backend/src/api/tapp_store/contract_rules.rs语义层(节选)
pub const MAX_TAPP_APIS: usize = 64;
pub const MAX_API_INJECT_ALIASES: usize = 32;
pub const API_BUILTINS: &[&str] = &["geo", "ai:chat", "ai:generate"];
pub const EVENT_TOPIC_PREFIXES: &[(&str, &[&str])] = &[
    ("publish", &["tapp.{id}."]),
    ("subscribe", &["tapp.", "system."]),
];

规则有稳定名字后,后端校验与 exporter 都能引用它;修改限制不再需要在 CLI 复制数字。

A.2Exporter 只负责拼出 JSON

exporter 用 schema_for! 获取结构层,再序列化 limits、rules、patterns。它不是第二个校验器,也不包含 npm 逻辑。

tools/tapp-contract-export/src/main.rs生成边界(节选)
fn main() {
    let schema = schemars::schema_for!(manifest::TappManifest);
    let output: Value = json!({
        "schema": schema,
        "limits": { "tappApis": contract_rules::MAX_TAPP_APIS },
        "rules": {
            "apiBuiltins": contract_rules::API_BUILTINS,
            "eventTopicPrefixes": list_map(contract_rules::EVENT_TOPIC_PREFIXES)
        }
    });
    println!("{}", serde_json::to_string_pretty(&output).unwrap());
}

sync-contract.mjs 调 exporter,再从前端权限配置读取 permission levels 与 action map,写入 src/generated/contract.json。运行时 npm 包只带结果。

排查路标 · 旅程 A
症状从哪下手
CLI 仍接受旧限制contract_rules.rssync-contract.mjscontract.json
字段有了但 CLI 不认识manifest.rs 的 Schema 与 schemaFields()
权限数量变化permissionConfig.ts 与 catalog drift test

6旅程 B:一次 CLI check 如何走完工程

CLI 不启动后端,也不执行 Tapp 代码。它读取 Manifest、扫描资源与源码,把静态可证明的事实汇总成 report。

全景 · 从命令到诊断
bin/myriad-tapp.mjsrunCli()inspectProject()diagnostics + permissions + packageFiles

B.1Manifest 与资源先形成边界

project.mjs 从 generated contract 建立字段、枚举、limits 和 regex。未知字段产生诊断;声明资源转成 path/extension/kind 条目,再检查安全路径、扩展名、存在性和大小。

tools/tapp-cli/src/project.mjs读取生成契约(节选)
const contract = JSON.parse(
  await readFile(new URL('./generated/contract.json', import.meta.url), 'utf8'),
)
const TOP_LEVEL_FIELDS = new Set(Object.keys(contract.schema.properties))
const WIDGET_SIZES = new Set(contract.rules.widgetSizes)
const MAX_ASSET_BYTES = contract.limits.assetBytes
const SAFE_COMPONENT = new RegExp(contract.patterns.safeComponent)

my-app 作为真实样例同时覆盖 page、widget、AI、events、Agent、Data Exchange、i18n 与 assets,检查结果为 0 条诊断、13 个 package files。

B.2静态源码把调用翻译成权限事实

扫描器识别 Tapp.storage.set() 这样的静态调用,从 action map 找到 permission,并保留文件与行列位置。动态属性访问无法证明,仍留给 warning 或运行时。

tools/tapp-cli/src/project.mjsaction → permission(节选)
for (const match of source.matchAll(TAPP_ACTION_CALL)) {
  const action = match[1] + '.' + match[2]
  const permission = catalog.actions[action]
  if (!permission) continue
  usedActions.push({ action, permission, file, ...location })
  addRequiredPermission(requiredPermissions, permission, reason, file)
}

report 因而能区分声明权限、实际所需权限和缺失权限;这仍不等于执行时授权,安装器会再次验证。

B.3pack 只消费检查结果

packProject() 先调用 inspectProject(),有 error 就停止;否则只把 report.packageFiles 写入 ZIP,并应用文件数、压缩大小和解压大小限制。

tools/tapp-cli/src/project.mjs校验先于打包(节选)
const report = await inspectProject(projectRoot)
if (report.diagnostics.some(({ severity }) => severity === 'error')) {
  const error = new Error('Project validation failed')
  error.report = report
  throw error
}
for (const path of report.packageFiles) {
  entries.push({ path, data: await readFile(join(report.root, path)) })
}
排查路标 · 旅程 B
症状从哪下手
字段或枚举被拒绝validateManifest()validateFields()
缺少权限inspectCode()contract.permissions.actions
包里没有声明资源resourceDeclarations()validateResources()
包大小超限packProject()zip.mjs

7旅程 C:同一个 CLI 如何变成 npx 包

npm 下载后,Node 只需找到 bin 与包内 generated contract;用户不再需要 Myriad 仓库路径、Rust 或 frontend 源码。

全景 · 发布与执行
package.jsonnpm publishnpx/npm execbingenerated contract

C.1bin 入口把 npm 包接到 CLI

入口文件只做 argv 转发与顶层错误转译;命令解析在 src/cli.mjs。package.json 暴露三个同实现名字,其中 tapp-cli 与 scoped package 短名一致。

tools/tapp-cli/package.json发布入口(节选)
"files": ["bin", "src", "README.md"],
"bin": {
  "myriad-tapp": "bin/myriad-tapp.mjs",
  "tapp": "bin/myriad-tapp.mjs",
  "tapp-cli": "bin/myriad-tapp.mjs"
},
"publishConfig": { "access": "public" }

白名单让同步脚本与测试留在源码仓库;generated contract 进入 tarball,因为它是运行时依赖。

C.2发布前锁住契约与测试

prepublishOnly 在发布前重新生成契约并执行测试;它不会成为用户安装时的依赖。

tools/tapp-cli/package.json发布闸门
"scripts": {
  "sync-contract": "node ./scripts/sync-contract.mjs",
  "test": "node --test",
  "pack:check": "npm pack --dry-run",
  "prepublishOnly": "npm run sync-contract && npm test"
}
🟠 发布状态:只执行了 npm publish --dry-run --access public,真实 npm 上传尚未发生;registry 中是否已有同名包没有在本报告中验证。
排查路标 · 旅程 C
症状从哪下手
npx 找不到命令package.jsonbin;本地等价验证是 npx ./tools/tapp-cli --version
发布包找不到契约tarball 白名单是否包含 src/generated/contract.json
发布前契约过期prepublishOnly、同步脚本与 drift test

8计划与实现

原计划/讨论工作树实际形态
可能先做 VS Code 插件,再考虑 CLI。没有新增 VS Code Extension;先交付可被终端、Playground 和 CI 共用的 CLI。
除 starter 与 ZIP 外,其余字段、枚举、限制、权限和语义规则都生成。实现了 Schema + rules/limits/patterns + permission catalog;starter 与 ZIP 明确保留手写。
CLI 只能从源码仓库路径运行。增加 npm files 白名单、bin aliases、public publish metadata 与发布闸门;真实上传仍未执行。

9心智模型补丁

规则写在后端校验里,CLI 只能复制一份。Rust Manifest + contract_rules 是源;CLI 读取生成 JSON。
标准路径:改类型/规则 → exporter → sync-contract → drift test。
JSON Schema 能表达全部 Tapp 合同。Schema 负责结构,条件关系与权限推导属于语义层。
npm 包要把 Rust exporter 带给用户。发布包只带生成结果;exporter 只在源码仓库发布闸门运行。
check 通过就等于安装一定通过。CLI 是离线预检,backend installer 仍是最终权威。

10新词表

契约生成
structure layer由 Rust 类型派生的字段、required 和 enum 形状。
semantic layer由 Rust 常量导出的限制、条件关系、路径与权限规则。
contract exporter把 Schema 与语义规则序列化成 JSON 的小 Rust 程序。
CLI / npm
generated contract随 CLI 包分发、运行时只读的 contract.json
bin inferencenpm 根据 package 名与 bin 名推断执行命令。
prepublishOnlynpm 发布前闸门;本项目用它同步契约并跑测试。

11测试与风险地图

有兜底的
  • help/version、init、check、permissions、pack 端到端。
  • generated contract 与 Rust exporter 对齐。
  • permission levels/actions 与前端配置对齐。
  • page/widget/both、未知字段、缺权限、Agent schema、i18n、ZIP 上限。
  • npx 包元数据、bin、files 白名单、public publishConfig。
  • 真实 my-app:0 条诊断、13 个 package files。
薄冰(事实)
  • 🟠 npm registry 实际发布未发生,仅 dry-run。
  • 🟡 静态源码分析无法证明动态属性、动态 API 名与运行时行为。
  • 🟡 drift test 覆盖生成产物,但不替代后端安装测试。
  • ⚪ 未新增 VS Code 插件或实时编辑器诊断。
合并前的外部动作:若要让外部开发者真正使用 npx,仍需在拥有 npm scope 权限的环境执行真实 publish;本次工作树没有做这一步。

12验收提示与覆盖声明

覆盖范围包括已跟踪后端差异,以及与 Tapp CLI/npx 相关的未跟踪文件:契约规则、Rust exporter、同步脚本、generated contract、CLI 入口/校验/ZIP、npm metadata、测试、my-app、开发文档和 handoff。生成 JSON、Cargo.lock、静态资源按机械或验收材料读取,没有把每个生成字段伪装成独立设计决策。