xl
把 xl-md 编译器装进 DeepSeek Harness 的插件(bundle)。
它做两件事:
- 提供
xlCLI —— 新增一个名为xl的 dsh profile,dsh xl build/dsh xl check/dsh xl targets与xl-cli.md的接口一致。 - 把非 ts 通道交给 DSH ——
xl_*七个模型工具让会话里的 agent 直接读需求、docs 与*.xl.md,自己生成目标语言文件;xl 只提供标准化零件:产物计划、结构契约、语言上下文、增量 cache、校验、写盘与产物头。
纯 ESM、零依赖、零构建:装进任何 profile 都能直接被 Loader 加载。
docs/ 下是本实现自己的规范文档(语法 · CLI · 诊断码 · ts 产物映射 · 验收基准),加上设计说明。改行为就改文档,见 §7。
1. 两条通道
| 目标 | 通道 | 生成者 | 是否联网 |
|---|---|---|---|
ts |
直出(direct) | xl 内置打印器,离线、确定性、字节稳定 | 否 |
| 其它语言 | 计划(plan) | DSH 会话里的 agent | 由该会话的模型决定 |
与原设计的差别
xl-cli.md §3.4 的原设计是:xl 自己拼一段 prompt,再 spawn dsh --profile headless,从 --json 事件流的 final 里取回代码。
本插件把它换成了「xl 出零件,DSH 主动调用」:
原: *.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 完全一致。
2. 安装
2.1 装进现有 profile(拿到模型工具)
在 Web 会话里用 plugin_manager,或者命令行:
dsh plugin --profile web add C:\Users\Admin\Documents\GitHub\xl
装完刷新页面,xl_plan 等七个工具即可用。这一步只注册 Host 插件与工具,不解析 profile 的命令行。
2.2 新建 xl profile(拿到 CLI)
dsh plugin --profile xl add C:\Users\Admin\Documents\GitHub\xl
dsh plugin 会初始化一个 base-backed profile 并选中本 bundle。之后:
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 卸载
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 一次生成的标准流程
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 §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 各有一份),注释符按该部件的扩展名选:
// @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 命令
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:
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 §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 §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)
- 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 §4 的查找顺序与字段:
{
"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 §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/ 下的四份 xl-*.md 与 design.md 现在描述的就是本实现(见 docs/design.md §0):早先它们是与上游 xlanguage 仓库逐字同步的副本,靠 pnpm docs:sync 比对;该机制已取消——实现行为一变,文档就地更新,因此不再需要「文档 vs 本实现」的差异表。
仍然值得知道的偏差集中在两处:
- docs/design.md §8「已知行为偏差与缺口」:面向维护者(几个选项在
xl build上不生效、build.naming不校验、interface 成员修饰符不报错、E4001故意不产生等)。 - 本文 §9「已知限制」:面向使用者。
docs/xl-base-case.md 是冻结的字节级验收基准:tests/fixtures/* 由它抽取(pnpm fixtures)。改动它必须重抽 fixture 并一起提交,其余四份文档则随手改。
8. 开发
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 抽出来,打印器的输出必须逐字节等于期望产物。基准样例更新后重新抽取:
pnpm fixtures
docs/xl-*.md 描述的是本实现,改行为就改文档(§7);只有 docs/xl-base-case.md 是冻结的验收基准。
目录
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。
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 §16)。
更完整的偏差清单(含位置与修法建议)见 docs/design.md §8。
No comments yet. Be the first to write one.