Bundle
xl
xl-md (*.xl.md) compiler for DeepSeek Harness: the xl command line, an xl Host service, and model-facing xl_* tools.
- Source
- Cangjier
- License
- MIT
- Updated
- Updated 9 days ago
Readme
# xl
把 **xl-md 编译器**装进 DeepSeek Harness 的插件(bundle)。
它做两件事:
1. **提供 `xl` CLI** —— 新增一个名为 `xl` 的 dsh profile,`dsh xl build` / `dsh xl check` / `dsh xl targets` 与 [`xl-cli.md`](docs/xl-cli.md) 的接口一致。
2. **把非 ts 通道交给 DSH** —— `xl_*` 七个模型工具让会话里的 agent 直接读需求、docs 与 `*.xl.md`,自己生成目标语言文件;xl 只提供标准化零件:产物计划、结构契约、语言上下文、增量 cache、校验、写盘与产物头。
纯 ESM、零依赖、零构建:装进任何 profile 都能直接被 Loader 加载。
`docs/` 下是本实现自己的规范文档([语法](docs/xl-syntax.md) · [CLI](docs/xl-cli.md) · [诊断码](docs/xl-check.md) · [ts 产物映射](docs/xl-emit-ts.md) · [验收基准](docs/xl-base-case.md)),加上[设计说明](docs/design.md)。改行为就改文档,见 §7。
---
## 1. 两条通道
| 目标 | 通道 | 生成者 | 是否联网 |
| --- | --- | --- | --- |
| `ts` | 直出(direct) | xl 内置打印器,离线、确定性、字节稳定 | 否 |
| 其它语言 | 计划(plan) | DSH 会话里的 agent | 由该会话的模型决定 |
### 与原设计的差别
`xl-cli.md` §3.4 的原设计是:`xl` 自己拼一段 prompt,再 `spawn dsh --profile headless`,从 `--json` 事件流的 `final` 里取回代码。
本插件把它换成了「**xl 出零件,DSH 主动调用**」:
```text
原: *.xl.md ──► xl 拼 prompt ──► dsh 子进程(一次性) ──► 代码
本: *.xl.md ──► DSH agent(自己读文件) ──► xl_plan / xl_context / xl_cache
└─► xl_verify / xl_emit ──► 代码
```
因此:
- `xl build -t csharp` **只输出计划**,不调用模型、不起子进程、不写文件;
- prompt 组装、重试、超时这些原来属于 xl 的职责不再存在——agent 自己决定读什么、怎么改;
- 产物头、指纹、历史版本 cache、结构回读校验仍然由 xl 统一负责,agent 调 `xl_emit` 一次就全部拿到。
`-t ts` 不受影响:仍然是内置打印器直出,字节与 [`xl-base-case.md`](docs/xl-base-case.md) 完全一致。
---
## 2. 安装
### 2.1 装进现有 profile(拿到模型工具)
在 Web 会话里用 `plugin_manager`,或者命令行:
```sh
dsh plugin --profile web add C:\Users\Admin\Documents\GitHub\xl
```
装完刷新页面,`xl_plan` 等七个工具即可用。这一步只注册 Host 插件与工具,不解析 profile 的命令行。
### 2.2 新建 `xl` profile(拿到 CLI)
```sh
dsh plugin --profile xl add C:\Users\Admin\Documents\GitHub\xl
```
`dsh plugin` 会初始化一个 base-backed profile 并选中本 bundle。之后:
```sh
dsh xl build . -t ts
dsh xl check .
dsh xl targets
```
`xl` 是一个 profile 名,所以 `dsh xl …` 就是 `dsh --profile xl …`。这是 dsh 唯一支持的应用入口写法:launcher 级子命令表是硬编码的(只有 `plugin`),profile 名是插件唯一能占用的「命令」。
> 应用行 `xl/cli` 只在 profile 名恰为 `xl` 时启用(bundle patch 里的 `disabled: !!js "ctx.get('profileContext')?.name !== 'xl'"`)。原因是一个 profile 的命令行只能由一个应用插件解析:Web profile 已经用它解析 `dsh web --port`,headless profile 用它解析任务文本。
### 2.3 卸载
```sh
dsh plugin --profile web remove xl
```
---
## 3. 模型工具
七个工具,一个操作一个工具,不按选项拆分。
| 工具 | 作用 | 关键参数 |
| --- | --- | --- |
| `xl_plan` | 列出每个源 × 目标会产出哪些文件、走哪条通道、cache 是否已满足 | `paths?` `targets?` `out?` `naming?` `flat?` `cwd?` |
| `xl_context` | 一个源 × 目标的标准化生成上下文:要写的确切路径(每个部件一条)、结构契约、语言覆盖段、每个部件的上一版指针,以及该目标语言的**生成指南路径**(存在时) | `file` `target` `out?` `naming?` `flat?` `cwd?` |
| `xl_cache` | 缓存状态:产物是否已匹配源指纹,以及每个部件最近归档的旧版本全文 | `file` `target` `out?` `cwd?` |
| `xl_verify` | 只校验不写盘:类型集合、成员名、参数个数 | `file` `target` `files` `cwd?` |
| `xl_emit` | 校验并写盘:产物头、指纹、prompt hash、归档旧版、更新 cache | `file` `target` `files` `model?` `force?` `verify?` `cwd?` |
| `xl_check` | 静态检查,返回规则码 | `paths?` `targets?` `ignore?` `strict?` `maxWarnings?` `cwd?` |
| `xl_build` | 跑一次构建:ts 直出写盘,其它目标只给计划 | `paths?` `targets?` `out?` `naming?` `flat?` `force?` `dryRun?` `cwd?` |
布局不是参数,而是目标的属性:`ts` 恒为 `file`(一个源一个产物),其余目标(含自定义目标)恒为 `type`(每个类型各一文件 + 一个模块文件);一个单元产出几个文件同样由目标的 `parts` 决定(`cpp` 是 `.h` + `.cpp`)。没有任何参数能改变它们,见 §4.4。
### 3.1 一次生成的标准流程
```text
xl_plan → 有哪些源、每个源要产出哪些文件、cache 是否 reusable
xl_context → 这一份源 × 目标的契约:输出路径(每个部件一条)+ 结构摘要 + ## <lang> 段 + 每个部件的上一版指针 + 生成指南路径
(agent 自己读 *.xl.md、需求文档、依赖文件,以及 xl_context 报出的生成指南)
xl_cache → 有上一版就取全文,在它之上改而不是重写
xl_emit → 一次提交这一份源 × 目标的全部文件;xl 负责校验、头、指纹、归档、cache
```
**生成指南**:`xl_context` 会在 `## Next` 里给出该目标语言的指南路径(本仓库 `docs/xl-emit-<lang>.md`),存在才给——插件里没有对应指南的目标(今天的非 cpp 目标)会明确回一句「没有指南」,agent 就只依据契约与目标工程惯例。指南是**建议**,不是契约:它不进 `promptHash`,改指南不会让任何产物失效([docs/design.md](docs/design.md) §4)。
`xl_emit` 的 `files` 必须逐一对应 `xl_plan`/`xl_context` 报出的路径:给出计划外的路径会被拒(`E2001`),漏掉计划内的路径会被拒(`E4002`),产物结构与 IR 不一致会被拒(`E4002`)且**不写盘**,agent 改完再调一次即可。
路径由 `out` / `naming` / `flat` 参与计算:`xl_plan`、`xl_context`、`xl_verify`、`xl_emit` 都接受这三个参数,调用时必须与规划时给的完全一致,否则算出来的路径与计划对不上。
### 3.2 产物头
非 ts 目标的**每个部件**各写一份产物头(C++ 的 `.h` 与 `.cpp` 各有一份),注释符按该部件的扩展名选:
```csharp
// @generated by xl from pkg/demo.xl.md
// xl:sha256:<源指纹> xl:target:csharp xl:ver:0.1.0 xl:model:<模型> xl:prompt:<上下文哈希>
// DO NOT EDIT — 修改请改 xl.md 并重新生成
```
`xl:prompt` 是 `xl_context` 报出的上下文哈希(结构摘要 + 语言覆盖段 + 依赖摘要 + layout + **parts** + naming)。它变了就说明生成依据变了,cache 因此失效。
---
## 4. `xl` CLI
### 4.1 命令
```text
xl build [paths...] [options] 编译(ts 写盘;其它目标只给计划)
xl check [paths...] [options] 只做静态检查
xl targets [options] 列出已知目标
xl --help | -h 帮助
xl --version | -v 版本
```
### 4.2 选项
| 选项 | 作用 |
| --- | --- |
| `-t, --target <lang>` | 目标语言,可重复;缺省取 `xl.json` 的 `build.target`,再取行配置的 `defaultTargets`(bundle patch 是 `['ts']`),再缺省 `ts` |
| `-o, --out <dir>` | 输出根目录;缺省取 `xl.json` 的 `build.out`,再缺省 `dist`;可给绝对路径 |
| `--flat` | 丢弃源文件相对目录层级 |
| `--stdout` | 产物正文写标准输出、不落盘 |
| `--naming <mode>` | `idiomatic`(缺省)\| `preserve` |
| `--force` | 忽略指纹与缓存,强制重新生成 |
| `--no-cache` | 本次不读写增量缓存,也不写历史版本归档 |
| `--clean` | 归档并删除本次计划的所有产物,然后强制重新生成 |
| `--dry-run` | 只打印计划,不写盘 |
| `--ignore <codes>` | 忽略指定诊断码,逗号分隔(产物层的 `E2001` / `E2002` / `E2003` 不可忽略) |
| `--strict` | warning 视为 error(只对 `xl check` 生效) |
| `--max-warnings <n>` | warning 数量上限(只对 `xl check` 生效) |
| `--format <fmt>` | `pretty`(缺省)\| `compact` \| `json` |
| `-q, --quiet` / `--verbose` / `--json` / `--color <when>` | 输出控制;前两者也接受 `XL_LOG`,颜色受 `NO_COLOR` / `FORCE_COLOR` 影响 |
| `--cwd <dir>` | 以指定目录为基准解析路径与配置 |
### 4.3 被接受但无效的选项
原设计里这些选项服务于「xl 自己 spawn harness」,本插件不再调用模型,因此它们只是为了让既有命令行不报错而被接受;`--verbose` 下会提示一次:
`--harness` `--harness-profile` `--timeout` `--retries` `--keep-going` `--concurrency`
另有几个选项在 `xl build` 上**被接受但读取后不使用**:`--verify` / `--no-verify`(只决定 `xl_emit` 的 `verify` 缺省);`--keep-going` 是冗余的(单个文件失败本来就不中断整轮构建),`--concurrency` 对 ts 直出也没有意义。`XL_TIMEOUT` / `XL_CONCURRENCY` / `XL_HARNESS` 不被读取。
### 4.4 输出布局
产物一律落在 `<out>/<目标语言>/…`,两种 layout 都保留源文件相对目录。源文件 `pkg/demo.xl.md`,`xl build . -t ts -t csharp -t cpp -t python`:
```text
dist/
ts/pkg/demo.ts # ts 恒为 file:一个源文件一个产物
csharp/pkg/DemoModule.cs # 其余目标恒为 type:模块级 # method / # const 合并到这里
csharp/pkg/Point.cs # 每个类型各成一文件
cpp/pkg/demo_module.h # cpp 一个单元两个部件:header …
cpp/pkg/demo_module.cpp # … 与 source(该单元有函数体,所以被计划)
cpp/pkg/point.h
cpp/pkg/point.cpp
python/pkg/demo_module.py # type 布局的其它语言同样带语言目录
python/pkg/point.py # 文件名按 --naming 变换(idiomatic 下 Python / cpp 用 snake_case)
```
- 布局是目标的属性,不是调用的属性:`ts` 恒为 `file`,csharp / java / python / go / rust / cpp 与自定义目标恒为 `type`。命令行与工具都没有 layout 参数([docs/xl-cli.md](docs/xl-cli.md) §3.5),`xl.json` 的 `targets.<lang>.layout` 根本不会被读取(§9)。
- **`parts` 同样是目标的属性**:一个单元产出几个文件由目标的部件表决定,几乎所有语言只有一项,`cpp` 有两项 —— `header`(`.h`)与 `source`(`.cpp`)。带 `requires: "bodies"` 的部件只在单元确实有可执行内容(有函数体 / 代码块初始值 / `# statement` / 对应语言覆盖段的代码)时才计划:只有 inline 字段的类是 header-only,不会被要求交一个空的 `.cpp`([docs/xl-cli.md](docs/xl-cli.md) §3.5)。这个判定只依赖 IR 与目标名,所以同一份源的计划是确定的。
- 语言目录是计划的产物路径的一部分:`xl_plan` / `xl_context` 报出的路径就是要写的确切路径(每个部件一条,带 `part` 标记),`xl_emit` 只接受这些路径、且一个都不能少。
- `--flat` 丢弃源文件相对目录,但保留语言目录(`dist/ts/demo.ts`)。
- 每个语言目录下另有自己的增量 cache:`dist/ts/.xl/`、`dist/csharp/.xl/`(§6)。
- 未给 `-o` / `build.out` 时 `<out>` 是 `dist`;`-o` 可以是相对工作目录的路径,也可以是绝对路径(绝对路径会原样出现在计划里)。
### 4.5 退出码
| 码 | 含义 |
| --- | --- |
| 0 | 成功(允许 warning;`--strict` 下不允许) |
| 1 | 源文件有 error、输出冲突、写盘失败 |
| 2 | 用法错误(未知选项 / 未知目标 / 无输入 / 路径不存在) |
| 3 | 计划通道失败(本插件不返回;由 agent 侧的 `xl_emit` 以 `E4002` 表达) |
`--ignore` 只对解析 / 检查层的码生效:`E2001` / `E2002` / `E2003` 写进 `--ignore` 也照常计数,写盘失败或输出冲突一律退出 1。
---
## 5. 配置
### 5.1 profile 行配置(`cordis.patch.yml`)
```yaml
- id: xl
name: 'xl'
config:
workspaceRoot: null # 工具缺省的工作目录;null 用进程 cwd
cacheDir: '.xl' # 每个语言目录下的 cache 目录名(缺省即 dist/<lang>/.xl);xl.json 与 XL_CACHE_DIR 优先级更高
defaultTargets: ['ts'] # 请求未给 targets 且 xl.json 也没有时的缺省
verifyOnEmit: true # xl_emit 未显式给 verify 时是否先校验
```
字段类型错误会在激活时抛错(Loader 无法替本包校验配置,因为校验需要 harness 依赖)。
### 5.2 `xl.json`
沿用 [`xl-cli.md`](docs/xl-cli.md) §4 的查找顺序与字段:
```jsonc
{
"build": { "target": ["ts"], "out": "dist", "naming": "idiomatic", "header": true, "cacheVersions": 5 },
"check": { "ignore": ["W3102"], "strict": false, "maxWarnings": null },
"targets": {
"kotlin": { "ext": ".kt" },
"objcpp": {
"parts": [
{ "role": "header", "ext": ".h" },
{ "role": "source", "ext": ".mm", "requires": "bodies", "scope": "definition" }
]
}
}
}
```
- 自定义目标给 `ext`(单部件,等价于 `parts: [{ "role": "file", "ext": … }]`)或直接给 `parts`。`parts` 里 `role` 是部件名(缺省 `part1`、`part2`…,同一目标内唯一),`ext` 必填,`requires: "bodies"` 表示"该单元有可执行内容时才计划",`scope: "definition"` 表示"这个部件只定义部分成员"(见 [docs/xl-cli.md](docs/xl-cli.md) §3.5)。声明不成部件表是用法错误(`E0004`,退出码 2)。
- `build.target`(字符串或数组)是缺省目标,优先于行配置的 `defaultTargets`;`build.out`、`build.naming`、`build.header`、`build.cacheVersions`、`build.cacheDir` 都生效。
- `harness` 段整体不再被读取(没有子进程通道);`targets.<lang>.model` 虽然被读进目标描述符,但无人消费——模型 id 由 agent 在 `xl_emit` 的 `model` 参数里给出;`targets.<lang>.layout` 则根本不读:自定义目标走计划通道,而计划通道恒为 `type`(§4.4)。
优先级一律是 **CLI 参数 > 环境变量(`XL_TARGET` / `XL_OUT` / `XL_CONFIG` / `XL_CACHE_DIR`)> `xl.json` > 内置缺省**,所有入口(CLI 与七个工具)适用同一套规则。
---
## 6. 增量 cache
两份存储,职责不同:
| 路径 | 内容 |
| --- | --- |
| `<out>/<目标语言>/.xl/cache.json` | 每个 源 × 目标 的源指纹、产物路径、产物内容哈希、model、prompt hash |
| `<out>/<目标语言>/.xl/cache/<镜像源路径>.<扩展名>.<N>` | 最近 N 版产物(缺省 5,`build.cacheVersions` 为 `false`/`0` 时关闭) |
归档家族以**源 × 扩展名**为单位:`demo.h.1` 与 `demo.cpp.1` 是两份互不干扰的历史,单部件语言仍然只有一个家族。所以"上一版"按部件给出:`xl_context` / `xl_cache` 的 `previous` 是数组(`{ part, ext, version, path, text }`),C++ 给两条(header 一条、source 一条),csharp 这类给一条(`part: "file"`)。
缺省 `<out>` 是 `dist`,所以 ts 的存储是 `dist/ts/.xl/`、csharp 的是 `dist/csharp/.xl/`:每个语言一份独立存储,互不读取。`build.cacheDir` / `XL_CACHE_DIR` 给的是该目录名(缺省 `.xl`)——相对值放在语言目录之下,绝对值原样使用(此时所有语言共享一份存储,仍然正确:`cache.json` 按 源 × 目标 分键,归档文件名带目标扩展名)。仅仅**计划**某个语言(`xl_plan` / `xl_context`,或 `xl build -t <其它语言>`)不会建出该目录:没有任何 源 × 目标 被记录时 cache 不落盘。
- **复用判定**:计划中的每个产物都存在、带 xl 头、`xl:target` 是本目标、`xl:sha256` 等于当前源指纹 → 跳过。`--clean` 直接判定为不可复用,于是整轮重新生成。
- **归档**:覆盖前把被覆盖的那一份存为下一序号;最新一版就是 `xl_cache` / `xl_context` 给出的「上一版」,用来在它之上改而不是重写。`--no-cache` 同时关闭 `cache.json` 与归档。
- **手改检测**(`E2003`):产物内容哈希与 cache 记录不一致,说明它在 xl 之外被改过,覆盖前给一次 warning。
---
## 7. 规范文档
[`docs/`](docs/) 下的四份 `xl-*.md` 与 `design.md` 现在**描述的就是本实现**(见 [docs/design.md](docs/design.md) §0):早先它们是与上游 `xlanguage` 仓库逐字同步的副本,靠 `pnpm docs:sync` 比对;该机制已取消——实现行为一变,文档就地更新,因此不再需要「文档 vs 本实现」的差异表。
仍然值得知道的偏差集中在两处:
- [docs/design.md](docs/design.md) §8「已知行为偏差与缺口」:面向维护者(几个选项在 `xl build` 上不生效、`build.naming` 不校验、interface 成员修饰符不报错、`E4001` 故意不产生等)。
- 本文 §9「已知限制」:面向使用者。
`docs/xl-base-case.md` 是**冻结的字节级验收基准**:`tests/fixtures/*` 由它抽取(`pnpm fixtures`)。改动它必须重抽 fixture 并一起提交,其余四份文档则随手改。
---
## 8. 开发
```sh
pnpm test # 等价于 node --test "tests/*.test.js"
pnpm check # 入口文件语法检查
pnpm codes # 诊断码 → 产生它的模块;有码没人产生就失败
pnpm fixtures # 从 docs/xl-base-case.md 重抽验收 fixture
```
测试里最重要的一条是**字节级一致性**:`tests/fixtures/demo.xl.md` 与 `demo.expected.ts` 直接从 `docs/xl-base-case.md` 抽出来,打印器的输出必须逐字节等于期望产物。基准样例更新后重新抽取:
```sh
pnpm fixtures
```
`docs/xl-*.md` 描述的是本实现,改行为就改文档(§7);只有 `docs/xl-base-case.md` 是冻结的验收基准。
### 目录
```text
index.js bundle 入口:xl 服务 + 模型工具
cli.js xl profile 的应用行入口
cordis.patch.yml bundle patch:插入这两行
docs/ 规范文档(xl-*.md)+ design.md(本实现的事实来源)
src/core/ 纯核心(解析 / 检查 / ts 打印 / 计划 / cache / 校验)
parse.js *.xl.md → IR
check.js 跨文件与跨目标规则、生成质量提示
emit-ts.js ts 直出打印器(确定性)
plan.js 输出路径与冲突
cache.js 每个目标语言的 cache.json 与历史版本归档
artifact.js 计划通道的三个操作:context / verify / emit
build.js 扫描 → 解析 → 检查 → 计划 → 产出
src/plugin/ Host 插件与 CLI
service.js xl 服务(能力缝)
tools.js 七个模型工具
cli.js / args.js / console.js
tests/ node:test
scripts/ extract-fixtures.mjs / code-map.mjs
```
### 分层
`src/core` 里的 `parse` `check` `emit-ts` `plan` `verify` `types` `header` `text` `diagnostics` 全是纯函数,不碰文件系统,因此可以脱离进程与 CLI 单测;`build` `artifact` `scan` `config` `cache` `source` 是 I/O 层。`src/plugin` 只做三件事:把请求翻译成 core 调用、把结果渲染给模型或终端、把副作用留在 core 的 I/O 层。详见 [docs/design.md](docs/design.md)。
---
## 9. 已知限制
- **写盘绕过 `ctx.fs` 沙箱。** `xl_emit` 与 ts 直出直接用 `node:fs` 写文件,因此不受 profile 的文件沙箱与审批策略约束。这是为了让插件零依赖、可在任意 profile 装载;如果部署需要沙箱,应把 `src/core/artifact.js` 与 `src/core/build.js` 的写盘改成走 `ctx.fs`。
- **没有子进程通道。** 这是设计目标,不是缺口:非 ts 目标的生成者是会话里的 agent。
- **`xl build` 上的几个选项不产生作用。** `--verify` / `--no-verify` 只决定 `xl_emit` 的 `verify` 缺省;`--keep-going` 是冗余的(单文件失败本来就不中断整轮);`--concurrency` 对 ts 直出没有意义;`--harness` / `--harness-profile` / `--timeout` / `--retries` 属于旧设计,只为兼容而被接受。
- **未实现的文档能力。** `xl.json` 的 `build.layout` / `build.verify` / `build.specHint` / `build.source`、`targets.<lang>.namespace`(只被读入描述符,无人消费)、`targets.<lang>.layout`(布局恒为目标的属性)、`targets.<lang>.model`(模型 id 由 `xl_emit` 的 `model` 参数给出)在本实现中不生效。`XL_TIMEOUT` / `XL_CONCURRENCY` / `XL_HARNESS` 不被读取。
- **`E2003` 依赖 cache。** `--no-cache` 时无法判断产物是否被手改,因此不会报告;`xl_emit` 也不做这项检查。
- **归档家族是 源 × 扩展名,不是 源 × 产物路径。** `xl_cache` 给的"上一版"是该扩展名最新归档的那一份;`layout=type` 下一个源有多个同扩展名产物时(`Point.cs`、`Box.cs`…),它只保留其中一个的历史。`cpp` 因此每份 `previous` 对应一个部件扩展名,而不是每个类型各一份。
- **类成员只支持 `### <lang>`。** 类级的 `## <lang>` 会被当成成员标题而报 `E1201`([docs/xl-syntax.md](docs/xl-syntax.md) §16)。
更完整的偏差清单(含位置与修法建议)见 [docs/design.md](docs/design.md) §8。
Install
dsh plugin --profile web add github:Cangjier/xl
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install xl from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.