CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

项目

ACE(Agent for Cangjie to Engine) —— 用仓颉(Cangjie)语言编写的服务端框架。角色对标 Spring/MidwayJS,技术路线对标 Micronaut/Quarkus(编译期宏而非运行时反射)。自底向上分层:Koa 式洋葱中间件内核 → 路由/请求解析中间件 → 声明式宏层(@Controller/@Service/@Inject/@Get)→ 智能体运行时。路线与设计说明见 doc/roadmap-m12-plus.md、doc/blog-ace-technical-deep-dive.md。

环境与命令(关键)

任何 cjpm 命令前必须先 source scripts/env.sh,否则链接失败。本机(macOS 26 / cjc 1.1.0-alpha / arm64)有两个绕过:

  • macOS 26 SDK 与 cjc 自带 LLD 不兼容 → env.sh 把 SDKROOT 指向 MacOSX15.sdk。
  • 运行 stdx 需 DYLD_LIBRARY_PATH 含 openssl@3 → env.sh 已设。
source scripts/env.sh        # 每个 shell 必做
cjpm build                   # 构建整个 workspace
cjpm test                    # 运行全部单测(推荐:见下方 gotcha)
cjpm run examples/hello-api  # 运行示例(或直接 target/release/bin/hello_api)

测试 gotchas:

  • 成员目录名(ace-router)≠ 包名(aceboot::router),所以 cjpm test aceboot::router 会报“路径不存在”、cjpm test ace-router 跑 0 个 → 用整 cjpm test(跑全 workspace),或 cjpm test -m <member>。
  • 单测 println 默认被捕获,调试加 cjpm test --no-capture-output。
  • 单测用 @Test + @Expect(actual, expected)(import std.unittest.* 与 std.unittest.testmacro.*),文件名 *_test.cj 与被测同包。

stdx 接入(预编译,勿误改 cjpm.toml)

cjpm 的 git=/version= 方式拉取 stdx 在本机不可用(TLS 崩溃 / 源码 pre-build 缺工具链)。已采用预编译 bin-dependencies:

  • 各模块 cjpm.toml 的 path-option / link-option 统一经 ${CANGJIE_STDX_PATH} 环境变量引用 stdx(官方约定),并同时配了 aarch64-apple-darwin 与 x86_64-unknown-linux-gnu 两个 target。
  • macOS 由 scripts/env.sh 导出 CANGJIE_STDX_PATH="$STDX_HOME"(STDX_HOME=~/darwin_aarch64_cjnative,预编译 stdx 1.1.3.1);Linux 构建前自行 export 指向对应平台的 stdx 根目录。
  • 可执行模块需 link-option = "-L${CANGJIE_STDX_PATH} -lcangjie-dynamicLoader-opensslFFI"(运行时 dlopen OpenSSL,macOS 的 DYLD 路径由 env.sh 设置)。
  • 新增 stdx 依赖成员时按上述双 target 模式配置,勿硬编码绝对路径。

架构(大图)

请求生命周期:stdx.net.http → ace-http 的 catch-all 分发器 → 构造 ace-web.Context → 跑 App 洋葱 → 写回响应。

  • ace-web(无 stdx,纯仓颉,可独立单测):洋葱内核。compose.cj 是核心——嵌套函数 dispatch(i) + 闭包递归实现洋葱,Array<Bool> 守卫防 next() 重复调用。Context 是框架无关的请求/响应载体(method/path/headers/state/status/body/query/rawBody)。App.handle(ctx) 驱动洋葱并兜底异常。
  • ace-router(依赖 ace-web,无 stdx):路由中间件。vendored 了 path_to_regex 到 aceboot::router.pathregex 子包(已去掉其 stdx.encoding.url 依赖、改纯 std 百分号编解码),Router.routes() 用 matchDetailed 支持 :name/*name/:name?/:name+/:name([0-9]+)/可选分组/转义全规则。Route.mwChains 是“由外到内的中间件层(各级 Router 的 mws 活引用)”,保证根/组中间件正确叠加。
  • ace-bodyparser(依赖 ace-web,无 stdx):请求解析中间件,读 ctx.rawBody(),目前解析 urlencoded 表单(带 URL 解码),结果经 formValue(ctx, name) 读取。
  • ace-http(依赖 ace-web + stdx):唯一接触 stdx 的层。AceDistributor(catch-all 分发器)把所有路径交给同一入口进洋葱;listen(app, addr, port) 起服务。
  • ace-framework(M3,进行中):声明式宏层,规划为三 module(macros / runtime / facade)。核心机制已 spike 验证:@Service/@Controller 宏在类旁生成顶层 let __ace_reg_X = Registry.register(...),其初始化器在程序启动期自动执行完成自注册——无反射、无枚举(这是“编译期宏 + 零运行时反射”路线的关键)。cjpm 自动处理 --compile-macro 宏包先编译。
  • ace-redis(依赖 ace-framework + stdx):零反射 Redis 组件。子包 resp(RESP2 编解码,可离线单测)/ client(连接池、全命令、RedisSubscriber Pub/Sub、RedisLockManager 分布式锁,仅触 std.net)/ integration(@Component RedisComponent,import 即自注册,读 [redis] 装配)。接入点:@Inject RedisClient/RedisLockManager、@Cacheable[ttl, "redis"] 命名缓存后端、Redis 后端限流、[redis].pubsub.bridgeChannels 桥接为 RedisMessage 应用事件(@EventListener 消费)。示例见 examples/redis-demo。
  • ace-template(依赖 ace-web + ace-framework):零反射模板渲染引擎(Mustache 风格逻辑轻量)。子包 engine(词法/语法/渲染,纯 std、零 IO,可离线单测)/ integration(@Component TemplateComponent,import 即自注册,读 [template] 装配 dir/suffix)。数据以显式 TemplateValue(TStr/TInt/TBool/TList/TMap/TNil)建模、TemplateModel 链式构造(无反射);语法含 {{ x }}(默认 HTML 转义)/{{& x }}·{{{ x }}}(原样)/{{# }}{{/ }} section(bool/对象上下文/列表迭代)/{{^ }} 反转/{{! }} 注释/{{> name }} 局部模板/点分路径。功能完整层:过滤器链 {{ x | upper | default:"无" }}(内置 upper/lower/capitalize/default:"arg"/length/json + 终结转义 raw/html/js/url/attr;engine.registerFilter(name, fn) 加自定义助手);循环元数据(section 迭代列表内){{@index}}(0基)/{{@number}}(1基)/{{@first}}/{{@last}}/{{@length}}/{{@odd}}/{{@even}};模板继承(父 {{$ block}}默认{{/ block}}、子首行 {{< parent}} + 同名 {{$ block}} 覆盖,多级就近者胜,block 外内容忽略,防环);自定义分隔符 {{=<% %>=}};standalone 空白(独占一行的块级标签不产生空行,standalone partial 按缩进逐行缩进);上下文转义(html/js/url/attr 各自策略)。JSON 桥接(integration 层,触 stdx):jsonToTemplateValue(jsonText): TemplateValue 把 JSON 文本转模型直接喂 render。接入点:@Inject TemplateEngine 后 engine.render(name, model)(按名走文件加载器)或 renderString(src, model)(内联),配 ctx.html(...)(ace-web 新增便捷方法)或 renderView(ctx, engine, name, model) 回写 text/html。踩坑记:indexOfSeq/标签切分不能用 src[i..i+m] String 切片比较——扫描位落在多字节字符中间会抛 "Invalid utf8",须按 toArray() 字节比较(标签内含中文即触发)。示例见 examples/template-demo。

设计取向(已定,勿擅自反转):内核纯显式、零运行时反射;声明式层用编译期宏生成等价显式代码(可 --debug-macro 审计);纯内核 ace-web/ace-router/ace-bodyparser 不依赖 stdx、可独立单测;声明层 ace-framework-runtime(JSON 工具基于 stdx.encoding.json:parseJson/stringifyJson/field*/mapToJson/jsonToMap 等)、ace-http、ace-security、agent 接触 stdx。JSON DOM/解析统一用 stdx(不再自维护解析器);纯字符串辅助(jsonEscape/jsonStringArray 等)仍在 ace-web。

仓颉语言 gotchas(本仓库踩过的坑)

  • std.regex 用 POSIX 语法 [0-9],不认 \d(路由内联正则写 :id([0-9]+))。
  • 闭包捕获并重新赋值 var 不可靠 → 需在闭包内累积状态时用引用类型(ArrayList/Array)并调用其方法,不重新赋值被捕获变量(见 compose.cj 的守卫、洋葱测试)。
  • 字节比较要带 u8 后缀(b == 37u8);String.toArray() 得 Array<UInt8>,String.fromUtf8(...) 反向。
  • class 引用类型 / struct 值类型;Option 无 null(?T + match/??/getOrThrow());enum + match 穷尽。
  • 验证 .o/.a 符号用系统 /usr/bin/nm(cangjie 自带 llvm-nm 是 LLVM15,读不了 Apple clang21 新目标格式,会显示 0 符号)。

工作流

新增成员后把目录加入根 cjpm.toml 的 [workspace].members。每个能力做成可单独引入的 module(高内聚、可单测)。改代码后跑 cjpm test 保持全绿(当前基线见 git log)。全局编码规范(不可变、小文件、错误处理等)见 ~/.claude/rules/。