feat: release Dever 0.1.4 with enforceable project guidance
This commit is contained in:
1 parent
201bdee372
commit
89629c2197
86 files changed
+2798
-338
No files matched your search
@@ -2,57 +2,76 @@
|
||||
|
||||
## API 和 CMD
|
||||
|
||||
`module/news/article/api/admin/manage.dever`:
|
||||
`module/system/health/api/admin/status.dever` 是一个可匿名访问的健康检查:
|
||||
|
||||
源码:[健康检查 API](../assets/examples/http-rest/module/system/health/api/admin/status.dever)
|
||||
|
||||
```dever
|
||||
get list = app.list
|
||||
get detail = app.detail
|
||||
post create = app.create
|
||||
put replace = app.replace
|
||||
delete remove = app.remove
|
||||
post publish = app.publish
|
||||
public get ready = app.status
|
||||
```
|
||||
|
||||
API 无函数体,继承 App 输入/输出签名。支持 get/post/put/delete,当前没有 patch、旧 get_ 前缀或源码内 site 声明。`cmd greet = app.greet` 生成 `component.domain.greet` 命令,参数来自单个 JSON 对象;main.dever 不存在,也不读取命令行参数。
|
||||
API 只把请求交给 App,不写函数体。参数和返回类型直接使用 App 的声明。HTTP 支持 get/post/put/delete,不支持 patch、旧 get_ 前缀或文件内的 site 声明。
|
||||
|
||||
GET/DELETE 从 query 读标量参数,POST/PUT 从 JSON body 读参数;额外、重复或缺少必填字段均拒绝。无需自己读 body 或构造 dever.http.Response。成功统一为 `{"code":0,"message":"ok","data":...}`;错误 data 为 null,code 对应状态码。接口必须绑定一个可序列化输出,输出名可自定。
|
||||
`cmd greet = app.greet` 生成 `component.domain.greet` 命令,参数是单个 JSON 对象。无需 main.dever,也无需手写命令行解析。
|
||||
|
||||
GET/DELETE 从 URL query 读标量参数,POST/PUT 从 JSON body 读参数。多传、重复或缺少必填字段都会报错。无需自己读 body 或构造 `dever.http.Response`。
|
||||
|
||||
成功返回 `{"code":0,"message":"ok","data":...}`。失败时 data 为 null,code 对应 HTTP 状态码。每个接口绑定一个能序列化的输出,输出名可自定。
|
||||
|
||||
`dever.api.Error` 中 Invalid/Unauthorized/Forbidden/NotFound/Conflict/TooManyRequests 对应 400/401/403/404/409/429;未知内部失败记录脱敏日志并返回安全的 500。
|
||||
|
||||
写 HTTP/CMD/Job 调用链由入口建立事务;GET 调用链不能写库或入队。密码 hash/verify 是例外:不能让昂贵密码运算占用入口长事务,实际写入必须放显式短 transaction,并在其中重新检查依赖状态。不能为了“默认有事务”而删除这种短事务。
|
||||
写 HTTP、CMD 和 Job 默认由入口建立事务;GET 不能写库或入队。
|
||||
|
||||
密码 hash/verify 除外:计算期间不能长期占用数据库事务。实际写入要放在显式的短 `transaction` 内,并重新检查相关状态。不要因为“默认有事务”删掉这里的短事务。
|
||||
|
||||
## 站点、身份与权限
|
||||
|
||||
`sites.admin.path = "admin"` 匹配领域内 `api/admin/**`,front 同理。文件名 topic 也进入 URL;上述 publish 是 `/news/article/admin/manage/publish`。每条 HTTP 声明只能匹配一个站点,重叠或遗漏配置会失败;path 不改写 URL,也不匹配 `api/admin.dever`。
|
||||
`sites.admin.path = "admin"` 匹配领域内 `api/admin/**`,front 同理。文件名 topic 也进入 URL;上述 ready 是 `/system/health/admin/status/ready`。每条 HTTP 声明只能匹配一个站点,重叠或遗漏配置会失败;path 不改写 URL,也不匹配 `api/admin.dever`。
|
||||
|
||||
所有 HTTP 默认鉴权并校验精确权限。只有明确匿名操作写 `public post login = app.login` 等单条 public 声明;没有 public rest、public cmd、auth.require 或每个业务文件的权限注册代码。
|
||||
|
||||
配置的 auth provider 引用一个项目自己的 `verify` App 方法,验证账号、会话、成员和租户关系。输入是 `dever.auth.Claims`,输出 record 必须且只能包含 `id: Text`、`user_id: <UserModelId>?`、`tenant_id: <TenantModelId>?`;只能读 global Model,不能写库、拿原始 Header 或返回自报权限。
|
||||
配置中的 auth provider 指向项目自己的 `verify` App 方法,由它核实账号、会话、成员和租户关系。输入是 `dever.auth.Claims`,输出必须且只能有三个字段:
|
||||
|
||||
认证成功的受保护 App 可读取 `dever.auth.id()`、`session()`、`user_id()`、`tenant_id()`、`dever.site.key()`;来源是隐藏可信上下文,不能让客户端提交 user_id/tenant_id 当作身份。名义 ID 必须与站点 verify 的类型对应,可空 ID 经分句处理后才能使用。public/CMD/Job/普通测试不会自动获得身份读取能力。
|
||||
- `id: Text`:核验后的主体标识。
|
||||
- `user_id: <UserModelId>?`:用户的 Model ID。
|
||||
- `tenant_id: <TenantModelId>?`:租户的 Model ID。
|
||||
|
||||
核心生成 `component.domain.site.action` 权限目录,method 是元数据,不额外拼进权限键。REST 的 action 固定为 read/create/replace/delete。核心维护角色、用户角色、角色权限;项目无需重建这些表。App 用 `dever.auth.permissions/save_role/grant_role/revoke_role/disable_role` 实现后台授权入口,具体参数查当前库签名。它们只能从受保护写 API 到达。资源所有权可用 `dever.auth.owns_user(id)`,不等同于接口角色权限。
|
||||
verify 只能读 global Model,不能写库、拿原始 Header 或返回客户端自报的权限。
|
||||
|
||||
受保护接口认证成功后,App 可用 `dever.auth.id()`、`session()`、`user_id()`、`tenant_id()` 和 `dever.site.key()` 读取身份,不需要传 context。
|
||||
|
||||
这些值来自已核验的上下文,不能用客户端提交的 user_id/tenant_id 替代。ID 类型必须匹配 verify 声明,可空 ID 先用分句处理。public、CMD、Job 和普通测试不会自动获得身份读取能力。
|
||||
|
||||
权限键由核心生成,格式为 `component.domain.site.action`,例如 `news.article.admin.publish`。请求方法记录为元数据,不拼进权限键。REST 的 action 固定为 read/create/replace/delete。
|
||||
|
||||
核心已经维护角色、用户角色和角色权限表。后台 App 使用 `dever.auth.permissions/save_role/grant_role/revoke_role/disable_role` 管理授权,参数见 [签名索引](signatures.md);这些方法只能由受保护的写 API 调用。`dever.auth.owns_user(id)` 检查资源归属,不能替代接口权限。
|
||||
|
||||
Cookie 登录/退出用 `dever.auth.issue_cookie(subject, session, tenant)` 与 `clear_cookie()`。Header/Cookie 使用 `dever.api` 读取/设置,无需显式 context 参数。密钥来自部署配置;Cookie 写入和 Cookie 认证的写请求需要精确同源校验,站点配置 origin。敏感头不能当普通 Text 读取。
|
||||
|
||||
## Model、CRUD 与业务编排
|
||||
|
||||
`module/news/article/model.dever` 的主记录应为 Article:
|
||||
`module/content/article/model.dever` 的主记录是 Article,标题在 REST 写入时使用同领域纯计算规则:
|
||||
|
||||
源码:[文章 Model](../assets/examples/http-rest/module/content/article/model.dever)
|
||||
|
||||
```dever
|
||||
type Article {
|
||||
title: Text(1, 200)
|
||||
title: Text(1, 200) create = domain.normalize(input) replace = domain.normalize(input)
|
||||
body: Text
|
||||
}
|
||||
```
|
||||
|
||||
自动有只读 `id` 与 `created_at`。字段支持 default、private、unique、index;范围和长度在存储边界验证。关系使用目标 Model 专有 id 字段,不用 Int 代替外键。
|
||||
|
||||
同领域 App 可用 `model.create({...})`、`model.get(id)`、`model.update(id,{...})`、`model.delete(id)`、`model.list({where=...,order=[created_at.desc,id.desc],page=1,size=20})`。这是编译器静态 DSL,不是动态字典 SQL;不用手写 repository。聚合、关联和插入流程仍由 App 明确编排,不能自动推测关联图级联写入。
|
||||
同领域 App 直接使用 `model.create({...})`、`model.get(id)`、`model.update(id,{...})`、`model.delete(id)` 和 `model.list({where=...,order=[created_at.desc,id.desc],page=1,size=20})`。
|
||||
|
||||
这些表达式由编译器检查,不是任意字典拼 SQL;无需再写 repository。关联查询和跨表插入由 App 明确编排,不会根据关系声明自动猜测级联写入顺序。
|
||||
|
||||
纯粹 CRUD 可在 API 写 `rest model`,多 Model 时指定 `rest model.<topic>`。Model 中 `create = expr`、`replace = expr`、`search = expr` 控制 REST 字段来源/查询;引用 input 表示客户端同名输入转换,不引用 input 表示服务端提供且禁止客户端提交。不要改成 post/put/get 字段关键字。
|
||||
|
||||
`private owner user_id: ... = dever.auth.user_id()` 在创建时赋值,并将所有权条件加入全部读写 SQL;不是只在输出隐藏 user_id。这种静态 owner 隔离可由 Model 直接声明,无需仅为它增加 App。关联操作、超出字段合同的业务校验、复杂输入输出或发布状态机由 App 编排,不能藏在 API 里。App 对外用明确 View,别泄漏 Model 私有字段。
|
||||
`private owner user_id: ... = dever.auth.user_id()` 创建时写入当前用户,并让全部读写 SQL 都带上所有权条件;它不只是隐藏输出字段。仅需这种隔离时,不用再写 App。
|
||||
|
||||
有关联操作、复杂输入输出、发布状态流转或超出字段约束的业务校验时,再写 App。API 中不放这些逻辑。App 用明确的 View 返回数据,避免泄漏 Model 私有字段。
|
||||
|
||||
不为每个 CRUD 机械建立 App、Domain、Port 和 Adapter;Model 的数据库操作无需再套一层 Port/repository。实际新增业务合同或编排时才增加对应实现。完整反重复、反硬编码及检查边界见 [项目开发](development.md)。
|
||||
|
||||
@@ -62,6 +81,6 @@ type Article {
|
||||
|
||||
运维命令:`dever tenant migrate <root> <id>`;`dever tenant owner <root> <id> <site> <user-id>` 建立核心 Owner;`dever tenant component enable|disable <root> <id> <component>` 控制租户功能。可用租户组件从 Model/Job 推导,不手写 Profile 分组。
|
||||
|
||||
job.dever 声明持久任务并绑定 App;enqueue 与事务、重试、调度、权限重验使用核心机制,不另写后台死循环。实际 Job 声明从对应版本指南和 CMS publishing/schedule 示例复用,不能把第三方 Worker 当持久 Job。
|
||||
job.dever 声明持久任务并绑定 App;enqueue 与事务、重试、调度、权限重验使用核心机制,不另写后台死循环。直接从随 skill 提供的 [Job 示例](examples.md#持久-job) 复用声明、配置和隔离测试,不能把第三方 Worker 当持久 Job。
|
||||
|
||||
部署配置的真实结构可查官方 CMS `config/setting.json`,只添加项目用到的 database、tenant、auth、sites、http、job、runtime、log。最小 CMD 项目只需模板配置。密码、token 与真实数据库连接不提交 Git。
|
||||
部署配置先参考随 skill 提供的 [完整示例](examples.md),只添加项目用到的 database、tenant、auth、sites、http、job、runtime、log。最小 CMD 项目只需模板配置。密码、token 与真实数据库连接不提交 Git。
|
||||
@@ -14,7 +14,9 @@ dever build my_app --output my_app/program
|
||||
|
||||
创建 Markdown 项目用 `dever new my_app --markdown`。两种模板使用相同代码和行为,Markdown 另加编译器校验的说明。目标目录必须不存在,父目录必须已存在;不接受向现有项目合并或覆盖。build 输出也必须不存在。
|
||||
|
||||
模板只包含 CMD、App、配置、一个测试和开发指引,无需数据库或 HTTP 服务。实际项目在它的目录合同上扩展,替换演示领域,删除不再使用的示例与测试。添加 HTTP 时须完成 [站点与身份配置](application.md),不能只把 cmd 换成 get。
|
||||
模板包含 CMD、App、配置、一个测试和开发指引,无需数据库或 HTTP 服务。`dever new` 已生成 `AGENTS.md`;已有项目在根目录运行 `dever skill install` 即可写入同一份规则,保留文件中其他内容。
|
||||
|
||||
按需要替换模板的演示领域,删除不再使用的示例与测试。添加 HTTP 时须完成 [站点与身份配置](application.md),不能只把 cmd 换成 get。
|
||||
|
||||
```text
|
||||
my_app/
|
||||
@@ -48,6 +50,14 @@ my_app/
|
||||
| Job | 持久任务声明,绑定同领域 App |
|
||||
| Test | 文件同名、零输入零输出的唯一测试入口 |
|
||||
|
||||
## 不擅自保留旧实现
|
||||
|
||||
默认只实现当前接口和语法。用户没有明确要求兼容时,不保留旧参数、旧配置读取、旧接口、别名或备用实现,不用“以后可能用到”作为保留理由。
|
||||
|
||||
改接口时,查清本项目调用方,一起修改实现和必要测试,删除本次废弃路径。外部调用方无法一起修改时,说明影响,由用户决定迁移安排;不能擅自加一套兼容流程。
|
||||
|
||||
项目通过 `dever.version` 使用某个编译器版本,是正常的版本选择,不要求业务代码同时支持新旧两套行为。业务明确要求的失败恢复也不是旧版本兼容,但必须有真实依据、明确结果和相应验证。
|
||||
|
||||
## 文件与函数组织
|
||||
|
||||
简单领域保留角色单文件;复杂角色可以拆为 `app/publish.dever`、`app/search.dever` 等内聚主题,调用名不带文件名。非 API topic 目录不递归。App/Domain/Port/Adapter/Job 单文件与同名目录二选一,选择目录时至少有两个内聚主题文件;Model/API 主文件与 topic 目录可按编译器合同共存,不受这个最少文件数限制。只有 `api/` 可按站点和资源继续嵌套,目录会影响 URL。
|
||||
@@ -93,31 +103,57 @@ my_app/
|
||||
|
||||
`test/hello/greeting/greet.dever`:
|
||||
|
||||
源码:[模板测试](../assets/template/test/hello/greeting/greet.dever)
|
||||
|
||||
```dever
|
||||
greet() () {
|
||||
assert_eq(hello.greeting.greet("Dever"), "Hello, Dever")
|
||||
message = app.greet("Dever")
|
||||
assert_eq(message, "Hello, Dever")
|
||||
}
|
||||
```
|
||||
|
||||
模板中的实际字符串以生成文件为准。测试只执行文件同名函数,不需要 main 或注解。`assert` 接受 Bool,`assert_eq` 要求同类型。测试可访问同领域 App/Domain,跨领域仅 App,不能直接写 Model CRUD。使用 Model 时运行器使用临时 SQLite;不使用 Model 时不需要数据库。它不会读取生产 setting.json 或生产 data。对业务失败使用 `result(call)` 配合 choice 分句验证。
|
||||
测试只执行文件同名函数,不需要 main 或注解。`assert` 接受 Bool,`assert_eq` 要求同类型。业务失败用 `result(call)` 和 choice 分句断言。模板中的字符串以生成文件为准。
|
||||
|
||||
常用命令:`fmt --check` 查格式,`check` 查语法/类型/目录/分句/配置,`api` 输出 API 合同,`test` 运行应用套件,`run` 开发执行,`build` 生成独立程序。不要发明 `test --filter`、`run api` 或 `dever init`。
|
||||
测试可调用同领域 App/Domain,跨领域只调用 App,不能直接写 Model CRUD。涉及 Model 时使用临时 SQLite,否则不需要数据库。命令会读取 setting.json 和锁中的依赖信息,但测试不连接部署数据库,也不使用项目 data。
|
||||
|
||||
常用命令:`fmt --check` 查格式,`check` 查语法/类型/目录/分句/配置,`api` 输出 API 合同,`test` 运行应用套件,`run` 开发执行,`build` 生成独立程序。不要发明 `run api` 或 `dever init`。
|
||||
|
||||
选一个测试运行:
|
||||
|
||||
```sh
|
||||
dever test my_app --filter hello.greeting.greet --timeout-ms 5000
|
||||
```
|
||||
|
||||
`--filter` 可以写组件、组件.领域或组件.领域.用例。`news.article` 不会选中 `news.article_extra`,不支持子串或通配符。筛选后仍检查全部源码;没有匹配项就报错,未执行用例显示 `not run`。
|
||||
|
||||
每例默认限时 60 秒,不含编译时间。`--timeout-ms` 必须是正整数毫秒;选项不能重复。超时后回收本例进程及后代,记录失败并继续下一例,最终命令返回失败。
|
||||
|
||||
### 验证范围
|
||||
|
||||
| 检查 | 能证明的范围 |
|
||||
| --- | --- |
|
||||
| `dever fmt <root> --check` | 源码格式;不证明类型或业务正确 |
|
||||
| `dever check <root>` | 静态语法、类型、目录、命名、分句、相关配置、已有 API 基线,以及应用入口可达性和冗余硬检查;不执行应用 |
|
||||
| `dever test <root>` | 当前项目全部应用测试的实际断言;没有筛选参数,不证明生产入口完整 |
|
||||
| `dever check <root>` | 检查语法、类型、目录、配置和已有 API 基线;也检查业务入口无法调用到的代码及确定的冗余。不运行应用 |
|
||||
| `dever test <root> [--filter ...] [--timeout-ms ...]` | 检查全部源码,实际执行所选测试断言(默认全部);不证明生产入口完整 |
|
||||
| `dever run/build` | 通过与 check 相同的应用硬检查,再准备本地编译资源并执行或编译;成功不等于部署或性能验收 |
|
||||
| AI 审查 | 业务含义、职责拆分、复用、硬编码与验证范围,不能替代程序检查 |
|
||||
|
||||
`check/run/build` 共用 C016,拒绝生产入口不可达的 App/Domain,以及符合现有判定条件的私有空转发和 Model CRUD 包装;并非任意角色、所有函数或全部 CRUD 包装都能检出。`check` 不准备 Worker 或 runtime pack,不进行原生编译,不启动应用、服务或数据库。`test` 保持独立的测试入口与 Port fake 检查,不执行生产入口可达性检查;不能用测试调用掩盖生产死代码,也不能把静态检查当作运行验收。
|
||||
`check/run/build` 会拒绝这些 App/Domain 问题(C016):
|
||||
|
||||
W001–W005 等诊断包含未使用赋值、部分冗余条件、相同私有实现、重复纯调用和空转发建议,警告本身不会使命令失败。审查本次引入的诊断,修正有证据的问题;不能声称编译器能判定全部硬编码或业务价值。skill 中的开发要求不自动变成编译器硬错误。
|
||||
- 接口、命令和后台任务等生产入口都调用不到的函数。
|
||||
- 符合现有判定条件的私有空转发和 Model CRUD 包装。
|
||||
- 生产入口可达、同一源码文件内契约和实现完全相同、没有副作用的私有函数。
|
||||
- 生产入口可达的函数内,参数未变且前一次结果仍可复用的重复纯调用。直接复用已有结果;参数或结果被修改时不按这条拒绝。
|
||||
|
||||
选择与改动相符且已获授权的检查。新模板的小套件可以完整运行;已有大项目不默认运行全量 `dever test`,也不默认启动服务、连接真实数据库或压测。需要但尚未运行的检查要说明原因和剩余证据,不能伪报通过。已有 `dever.api` 时保留基线约束;`dever api <root> --output <新文件>` 可以导出待比较合同,不覆盖原文件。
|
||||
这些规则不强迫合并有副作用、契约不同或跨文件的私有实现,也不能识别所有无用代码。
|
||||
|
||||
`check` 不准备 Worker 或运行资源,不生成原生程序,也不启动服务或数据库。`test` 只检查自己的入口和 Port fake,不套用生产应用硬检查;测试调用不能证明函数被实际业务使用,因此还要运行 `check`。
|
||||
|
||||
未使用赋值、部分多余条件、重复私有实现、重复纯调用和空转发还可能产生警告(W001–W005)。普通库和测试保留这些警告;应用满足上述条件时报 C016,同一源码证据的对应警告不重复显示。其他警告仍会显示,警告本身不使命令失败,但必须审查并处理本次引入的问题。编译器不能判断所有业务硬编码;skill 和 AGENTS 的要求也不自动变成编译错误。
|
||||
|
||||
按改动范围和用户授权选择检查。新模板可运行全部测试;已有大项目优先定向测试,不默认启动服务、接真实数据库或压测。未运行的必要检查应说明原因,不能报成通过。
|
||||
|
||||
已有 `dever.api` 时保留接口基线。需要比较新接口时,用 `dever api <root> --output <新文件>` 导出,不直接覆盖原文件。
|
||||
|
||||
源码放 module/,测试放 test/,部署配置放 config/,运行数据放 data/。生成程序从二进制同级 config/setting.json 读取配置。应用无需打包编译器;使用外部 Lib 时相关 runtime/依赖由 build 嵌入。只读 check 不证明真实数据库或部署验收。
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 按需复用的完整项目
|
||||
|
||||
这些项目随 skill 提供,都包含 `module/`、`test/` 和 `config/setting.json`。复制需要的项目到新目录,再替换业务内容,不要把整个 examples 放进 module。最小项目直接用 `dever new` 创建。
|
||||
|
||||
| 场景 | 项目资产 | 首次阅读 |
|
||||
| --- | --- | --- |
|
||||
| HTTP、受保护 REST、真实身份核验 | [http-rest](../assets/examples/http-rest/) | `config/setting.json`、`module/access/session/app.dever`、`module/content/article/` |
|
||||
| 入队、持久 Job、隔离数据库与虚拟时钟 | [jobs](../assets/examples/jobs/) | `module/report/archive/`、`test/report/archive/queued.dever` |
|
||||
| 外部能力、声明失败、每例独立 fake | [external-lib](../assets/examples/external-lib/) | [三语言接入说明](external-lib.md) |
|
||||
| 筛选、映射、累计与逐项断言 | [collections](../assets/examples/collections/) | [集合与处理函数](language.md#集合与处理函数) |
|
||||
| 普通二进制 Port、CMD 与 fake | [command](../assets/examples/command/) | [二进制调用说明](libraries.md#外部-lib-与普通二进制) |
|
||||
|
||||
复制后先运行 `dever fmt <root> --check` 和 `dever check <root>`,再按下面的命令运行相关测试。这些检查不准备 Worker、不连接部署数据库、不启动 HTTP/Job 服务。
|
||||
|
||||
文件要放在示例对应的角色中。遇到无法从业务入口调用到的代码报错(C016),接通真实调用或删除废弃代码,不添加假调用。
|
||||
|
||||
## HTTP 与 REST
|
||||
|
||||
`http-rest` 展示的是受保护内容资源和已有登录系统的 verify 接口,不包含账号注册、密码登录或签发凭据。匿名健康检查 `GET /system/health/admin/status/ready` 返回 `{"code":0,"message":"ok","data":true}`;它没有持久数据或租户依赖。
|
||||
|
||||
`module/content/article/api/admin/entries.dever` 的 `rest model` 生成 `/content/article/admin/entries` 下的资源接口;用 `dever api <root> --output <new-file>` 查看精确方法、路径和输入合同。REST 的权限键为 `content.article.admin.read/create/replace/delete`。例如创建正文包含 `title`、`body`,title 经同领域 pure Domain trim 后再应用 Model 长度约束。Model 自动提供 id/created_at,不手写 CRUD App。
|
||||
|
||||
配置中的 `admin.path = "admin"` 对应两个领域的 `api/admin/**`。
|
||||
|
||||
身份核验先读取 global Session,检查 UUID、subject、site、tenant key、过期与吊销状态,再检查用户、租户和成员关系。成功返回只有 id/user_id/tenant_id 的 Identity;失败返回 Unauthorized。不能原样回显 claims 冒充核验,也不从 verify 返回权限列表。
|
||||
|
||||
接入已有登录系统时,由它的认证流程创建 User、Tenant、Membership、Session,再按 provider 配置签发 subject/session/tenant。示例不会自动建默认用户、发演示 token 或绕过权限。
|
||||
|
||||
部署前替换 JWT 密钥,迁移租户数据库,再用 `dever tenant owner <root> <positive-tenant-id> admin <positive-user-id>` 建立初始 Owner。参数是数据库 ID,不是 token 的 tenant key。后续角色权限用核心 `dever.auth` 管理,不复制一套 RBAC 表。建 Owner 和迁移属于运维操作,不是普通测试。
|
||||
|
||||
这里开启 database 租户隔离:global 身份表在控制库,Article 在租户库,匿名 health 不触达 Article。不要添加 `tenant.mode/field/table` 或在缺少租户时回退控制库。站点没有使用 Cookie;如果改为 Cookie 登录,补充精确 `sites.admin.origin` 和 issue_cookie/clear_cookie 的同源合同。
|
||||
|
||||
定向测试:`dever test <root> --filter content.article.title --timeout-ms 5000`。该例验证标题规则,不证明 REST HTTP、JWT、权限或实际租户迁移已验收。
|
||||
|
||||
## 持久 Job
|
||||
|
||||
`jobs` 的 `report.archive.submit` CMD 接受一个 `input` record:
|
||||
|
||||
```sh
|
||||
dever run <root> -- report.archive.submit '{"input":{"key":"report-2026-10","title":"Monthly report"}}'
|
||||
```
|
||||
|
||||
这是会写示例数据库的显式运行命令,不是普通验证。CMD 只入队并退出;没有命令参数的服务启动才按配置 `runtime.mode = "worker"` 执行持久队列,不能为了跑测试启动它。
|
||||
|
||||
Job 声明 `database default`、同领域 App 参数类型、`retry(3)` 和 `timeout(30000)`,执行时只调用 App。
|
||||
|
||||
入队和入口写入在同一事务中;key 用来识别同一请求,重复提交不会创建另一个任务。Job 执行有自己的事务,Model 的 request_key 唯一约束防止重复档案。失败由核心队列重试,不另写循环、sleep 或第三方 Worker。
|
||||
|
||||
`dever test <root> --filter report.archive.queued --timeout-ms 5000` 使用运行器拥有的临时 SQLite。测试重复入队验证同一任务 ID,通过 `dever.test.drain_jobs(5)` 执行已到期任务,使用 `advance_clock(1000)` 推进虚拟时钟。既不使用配置里的 `data/jobs.db`,也不启动真实后台服务。
|
||||
|
||||
CMD/Test 的可信执行身份由核心建立为 System。若改为 HTTP 入队,只能从受保护写 API 到达;public 不得入队。核心在每次 User Job 尝试时重新验证会话、租户、组件与入队接口的精确权限,不保存角色快照。
|
||||
|
||||
## 来源与验证边界
|
||||
|
||||
集合项目可直接运行 `dever run <root> -- content.article.summarize '{"articles":[{"title":" First ","published":true},{"title":"Hidden","published":false}]}'`,结果包含 `titles: ["First"]` 和 `title_length: 5`。`dever test <root> --filter content.article --timeout-ms 5000` 同时运行集合业务和语法分句断言,无需数据库或服务。
|
||||
|
||||
这些项目保留了当前语言的角色边界、身份核验、Job 和 Port fake 用法,关键源码随 skill 提供。教程中的 dever 代码块必须有“源码”链接,内容与链接文件的连续片段逐字匹配,所属完整项目参加源码、业务入口、格式和测试检查。真实 Lib、数据库部署和 HTTP 仍需各自验收。
|
||||
@@ -0,0 +1,65 @@
|
||||
# 用 Python、JavaScript 或 Go 转换 Markdown
|
||||
|
||||
复制 [external-lib 项目](../assets/examples/external-lib/) 后,就能检查源码并运行 fake 测试。无需安装 Python/Node/Go 或下载依赖。
|
||||
|
||||
项目用现成 Markdown 库转换内容,App 只调用 Port。生成的 HTML 没有做安全清洗;展示不可信输入前,需要按产品要求清洗。外部语言只承接需要复用的能力,不转移普通业务逻辑。
|
||||
|
||||
先读 `module/document/markup/app.dever`、`port.dever`、`adapter.dever` 和两个测试。Port 的成功输出必须带名字 html,失败合同来自公开 App 的 RenderResult;同一份 Dever 声明供三种实现复用。
|
||||
|
||||
默认选择 pip。切换生态时只替换 `adapter.dever` 中的 external 声明、同一文件的 lib 值及 `config/setting.json` 的根 lib 数组;不要同时复制三个 Adapter 到同一角色,也不要改 Port/App/Test:
|
||||
|
||||
| 生态 | external 声明 | lib 声明 | setting.json 根 lib 中的值 | 已附实现 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Python | `external pip "python/render.py"` | `lib "markdown@3.8.2"` | `pip:markdown@3.8.2` | `module/document/markup/python/render.py` |
|
||||
| JavaScript | `external npm "javascript/render.mjs"` | `lib "marked@15.0.12"` | `npm:marked@15.0.12` | `module/document/markup/javascript/render.mjs` |
|
||||
| Go | `external go "go/render.go"` | `lib "github.com/yuin/goldmark@v1.7.8"` | `go:github.com/yuin/goldmark@v1.7.8` | `module/document/markup/go/render.go` |
|
||||
|
||||
项目默认 Python 的完整声明如下,JS/Go 按表替换:
|
||||
|
||||
源码:[外部 Adapter](../assets/examples/external-lib/module/document/markup/adapter.dever)
|
||||
|
||||
```dever
|
||||
external pip "python/render.py" {
|
||||
lib "markdown@3.8.2"
|
||||
}
|
||||
```
|
||||
|
||||
版本是示例固定输入,不表示“最新版本”;显式依赖准备仍须验证来源、许可证和产品需求。库的 Markdown 方言与尾部换行不同,切换生态不保证输出逐字节等价,真实输出验收需针对所选库进行。普通 fake 用例只验证 App/Port 合同。
|
||||
|
||||
## 外部函数怎么写
|
||||
|
||||
Python 导出 `async def render(payload, setting)`;JS 导出 `async function render(payload, setting, signal)`;Go 导出 `func Render(context.Context, any, any) (any, error)`,使用 `package main`,但不手写 main。操作名 `render` 在 Go 映射为 `Render`,蛇形名按段转 PascalCase。
|
||||
|
||||
编译器从 Port 生成 `contract.json`,写入参数、返回值、业务错误和接口标识。运行器自动找到同名处理函数,不需要项目复制 SDK、维护注册表、计算 hash 或编写 stdin 协议。
|
||||
|
||||
Python 使用 `Worker.from_manifest`,JS 使用 `Worker.fromManifest`,Go 由生成的入口调用 `ServeManifest`。SDK 来自匹配版本的运行资源。
|
||||
|
||||
三种实现共用的是 Port 的输入、输出、错误和操作合同。schema 身份还包含所选生态与 Lib 声明,因此切换生态或依赖后会变化,不能复用旧 manifest 或要求三种 hash 相等。
|
||||
|
||||
本例输入是 `{"source":"# Report"}`,成功返回 `{"html":"..."}`。即使只有一个输出,也不能返回裸字符串。
|
||||
|
||||
没有 setting 声明时,setting 为 null/nil。需要配置再增加 Adapter setting 类型和对应配置绑定,不能读取环境变量。
|
||||
|
||||
业务拒绝使用 SDK 的 `BusinessError`:本例的 identity 是 `document.markup.RenderResult.Rejected`,payload 是 `{"message":"..."}`。未知异常属于运行失败,不自动变成业务拒绝,也不返回假成功。
|
||||
|
||||
stdout 专用于协议,日志写 stderr。Python 处理函数要响应取消,JS 使用 signal,Go 使用 context。需要网络、文件、子进程或 GPU 时,通过 `allow` 声明并在配置中授权;本例纯文档转换不需要这些能力。缺少权限时不能关闭沙箱。
|
||||
|
||||
## 准备与测试
|
||||
|
||||
```sh
|
||||
dever check <root>
|
||||
dever test <root> --filter document.markup --timeout-ms 5000
|
||||
```
|
||||
|
||||
`rendered` 与 `rejected` 各自替换 `port.render`,分别检查成功和业务拒绝。fake 只对当前用例有效,不会影响其他测试。编译器检查参数、返回值、允许的错误和调用能力。普通测试不加载真实库、不启动 Worker,也不要求依赖锁或生产配置。
|
||||
|
||||
需要真实运行时,按用户授权准备依赖:
|
||||
|
||||
```sh
|
||||
dever lib add <root> pip:markdown@3.8.2
|
||||
dever run <root> -- document.markup.render '{"source":"# Report"}'
|
||||
```
|
||||
|
||||
JS/Go 改用表中的依赖声明。已有锁迁移机器时使用 `dever lib install <root>`。run/build 离线;缺资源就报错,不借用宿主解释器。切换依赖使用 Lib 命令并核对锁,不手工伪造 dever.lock。
|
||||
|
||||
静态示例与 fake 检查不证明第三方依赖已下载、真实库输出已验收或跨架构 Worker 已可运行。
|
||||
@@ -20,17 +20,21 @@ curl -fsSL https://git.dever.cc/shemic/dever/raw/branch/main/install | bash
|
||||
|
||||
基础安装包含编译器、应用编译资源、数据库运行库和配套 skill。Python、Node、Go 及第三方构建工具按实际依赖准备,同一版本由多个用户和项目共享。生成的应用不依赖系统 Python、Node、Go、Rust 或 Dever 编译器。
|
||||
|
||||
## 将 skill 接入 AI
|
||||
## 让 AI 遵守项目规则
|
||||
|
||||
安装核心后,将稳定入口装到用户选择的 AI 技能目录:
|
||||
在已有项目根目录执行:
|
||||
|
||||
```sh
|
||||
dever skill install <新的AI技能目录>/dever-language
|
||||
dever skill install
|
||||
```
|
||||
|
||||
目标目录必须尚不存在,不覆盖已有 skill,也不把入口嵌套进现有完整 skill 目录。重载 AI 工具的技能列表后使用;入口解析与核心版本配套的完整 skill。它名为 `dever-language`,不是 Go 框架的 `shemic-dever`,不会自动修改其他用户的 AI 目录。
|
||||
命令自动读取项目的版本选择,从配套 skill 获取规则,写入当前项目的 `AGENTS.md`。文件不存在就创建;已有文件只更新 `dever-language` 管理块,保留其他内容。重复执行不会重复追加。不需要指定 AI 技能目录。
|
||||
|
||||
新项目用 `dever skill path`,已有项目用 `dever skill path <project-root>` 查看对应版本的指引。
|
||||
`dever new` 已自动生成同一份规则,无需再执行一次。机器安装和 `dever update` 不改业务项目文件;需要刷新项目规则块时重新执行 `dever skill install`。
|
||||
|
||||
AGENTS 要求 AI 开始任务前执行 `dever skill path .`,读取项目版本的完整 skill 与相关参考。skill 随 Dever 提供,无需复制到项目。AI 指引不能代替编译器检查或业务验证。
|
||||
|
||||
如果旧安装仍提示 `skill install` 需要目录参数,重新运行上面的官方安装命令升级命令入口,再执行本命令;不要改回旧用法。
|
||||
|
||||
## 按需准备依赖和 ARM64 应用目标
|
||||
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
# 核心语法
|
||||
|
||||
以下片段说明语法。将它们放进正确角色文件并补完整调用链后检查,不能当作免配置 HTTP 项目。
|
||||
每个片段都链接到随 skill 提供、经过检查的完整源码。先看它所属的角色和调用链,再复用到项目;不要把片段单独当作完整 HTTP 项目。
|
||||
|
||||
## 类型和函数
|
||||
|
||||
```dever
|
||||
type Greeting {
|
||||
message: Text
|
||||
}
|
||||
源码:[模板 App](../assets/template/module/hello/greeting/app.dever)
|
||||
|
||||
greet(name: Text) (response: Greeting) {
|
||||
response = Greeting { message = "Hello, " + text.trim(name) }
|
||||
```dever
|
||||
greet(name: Text) (message: Text) {
|
||||
message = "Hello, " + name
|
||||
}
|
||||
```
|
||||
|
||||
函数是 `name(输入) (命名输出) { 线性语句 }`,没有 function/return。调用按位置传参。零输入或零输出也写 `()`。单输出直接得到值;多输出调用返回只能按输出名访问的结果。App 的输入、输出和向外传播错误均为公开合同,不暴露 Domain/Adapter 私有类型。
|
||||
函数写成 `name(输入) (命名输出) { 语句 }`,不写 function/return,调用按位置传参。没有输入或输出时也要保留 `()`。一个输出直接得到值;多个输出按名字访问。
|
||||
|
||||
App 的参数、返回值和向外传播的错误都属于公开接口,不能暴露 Domain/Adapter 私有类型。
|
||||
|
||||
函数命名表达业务行为,调用路径不附加主题文件名。相关函数、类型和同名分句可以同处一个内聚文件;拆分与复用的判断见 [文件与函数组织](development.md#文件与函数组织),不按函数长度自动增加 helper。
|
||||
|
||||
常用类型:`Bool`、`Int`(检查溢出)、`Float`、精确 `Decimal`、`Text`、`Bytes`、`Uuid`、`Id`、`DateTime`、`Date`、`Time`、`Duration`、`Secret`、`Json`。可空用 `T?`,集合用 `List<T>`、`Map<K,V>`。Model ID 是每个 Model 独有的名义类型,不等于任意 Int。不得把不同表的 ID 混用。
|
||||
|
||||
record 用字段初始化;choice 用分支构造:
|
||||
record 用字段初始化;choice 用分支构造。以下 State、label、display 和 grade 位于同一份语法测试,测试入口覆盖每个分句;实际业务无需创建这些演示函数。
|
||||
|
||||
源码:[分句测试](../assets/examples/collections/test/content/article/clauses.dever)
|
||||
|
||||
```dever
|
||||
type State {
|
||||
@@ -28,7 +30,11 @@ type State {
|
||||
Published
|
||||
Rejected(reason: Text)
|
||||
}
|
||||
```
|
||||
|
||||
源码:[分句测试](../assets/examples/collections/test/content/article/clauses.dever)
|
||||
|
||||
```dever
|
||||
label(state: State.Published) (text: Text) {
|
||||
text = "published"
|
||||
}
|
||||
@@ -44,7 +50,11 @@ label(state: other) (text: Text) {
|
||||
|
||||
## 分支与边界
|
||||
|
||||
没有 if、else、switch、match、条件表达式或递归。同名、同参数数量的分句必须连续,相同输出合同,互斥且穷尽;没有先匹配谁的顺序优先级。`other` 是同一参数位置所有明确模式的补集。
|
||||
用同名函数的不同分句表达条件,不写 if/else/switch/match、条件表达式或递归。这些分句要连续,参数数量和输出声明一致。
|
||||
|
||||
每种输入必须恰好匹配一个分句:不能漏掉,也不能重叠;写在前面不会优先匹配。`other` 表示该参数位置没有被其他明确模式覆盖的值。
|
||||
|
||||
源码:[分句测试](../assets/examples/collections/test/content/article/clauses.dever)
|
||||
|
||||
```dever
|
||||
display(value: Text) (text: Text) {
|
||||
@@ -68,16 +78,67 @@ grade(score: Int >= 60) (text: Text) {
|
||||
|
||||
字面量分句适合明确的状态和规则,不意味着可以按某个用户 ID、租户名字或样例数据写业务特例。运行时配置与可信身份的归属见 [配置与硬编码](development.md#配置与硬编码)。
|
||||
|
||||
集合遍历使用语言的 `map/filter/reduce/each` 等核心操作及静态 handler,不能加入 for/while。handler 的签名和可达调用链由编译器检查,不是运行期闭包或动态插件。具体签名查当前版本 `library/` 与核心语言指南,不杜撰 lambda 语法。
|
||||
## 集合与处理函数
|
||||
|
||||
Dever 用 `each` 同时表达映射和遍历,没有独立的 `map`:处理函数有一个输出时生成 List,没有输出时逐项执行动作。`filter` 按 Bool 输出筛选,`reduce` 按初始值逐项累积。它们是语言核心操作,不需要导入集合库。
|
||||
|
||||
下面的 App 选出已发布且标题非空的文章,整理标题并统计总字数。输入、结果类型与完整调用链都在 [集合项目](../assets/examples/collections/) 内。
|
||||
|
||||
源码:[文章 App](../assets/examples/collections/module/content/article/app.dever)
|
||||
|
||||
```dever
|
||||
type Article {
|
||||
title: Text
|
||||
published: Bool
|
||||
}
|
||||
|
||||
type Summary {
|
||||
titles: List<Text>
|
||||
title_length: Int
|
||||
}
|
||||
|
||||
summarize(articles: List<Article>) (summary: Summary) {
|
||||
published = filter(domain.is_published, articles)
|
||||
titles = each(domain.published_title, published)
|
||||
summary = Summary {
|
||||
titles = titles
|
||||
title_length = reduce(domain.add_title_length, titles, 0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
处理函数直接使用有业务含义的命名函数。参数顺序、类型和输出由编译器检查,不使用 lambda 或运行期闭包:
|
||||
|
||||
源码:[文章 Domain](../assets/examples/collections/module/content/article/domain.dever)
|
||||
|
||||
```dever
|
||||
is_published(article: app.Article) (selected: Bool) pure {
|
||||
selected = article.published and text.trim(article.title) != ""
|
||||
}
|
||||
|
||||
published_title(article: app.Article) (title: Text) pure {
|
||||
title = text.trim(article.title)
|
||||
}
|
||||
|
||||
add_title_length(title: Text, total: Int) (next: Int) pure {
|
||||
next = total + length(title)
|
||||
}
|
||||
```
|
||||
|
||||
`filter` 的处理函数接收一个元素,输出 Bool;`each` 的单输出成为新 List 的元素;`reduce` 按“元素、当前累计值”传参,输出下一个累计值。顺序保持不变,空集合的 reduce 返回初始值。本例初始值为 0。
|
||||
|
||||
集合项目的 [summary 测试](../assets/examples/collections/test/content/article/summary.dever) 用 `each(assert_clean_title, actual.titles)` 逐条断言。这个处理函数没有输出,所以该调用没有返回 List。测试还验证筛选顺序、清理结果、总字数和空集合。
|
||||
|
||||
## 失败和能力
|
||||
|
||||
业务错误通过 choice 的 `error` 分支声明,`fail` 传播,`result(call)` 显式捕获;普通调用直接得到成功值。不能用字符串/任意 JSON 代替类型化失败,不能捕获后悄悄吞掉错误。错误映射到 HTTP 的方式见 application.md。
|
||||
|
||||
纯 Domain 函数可声明 `pure`,编译器检查其调用链;要从 Model REST 字段来源调用的业务转换必须是合规的同领域 pure Domain。外部文件/网络 I/O 经 Port/Adapter,不把系统调用塞入普通 App 或 Domain。
|
||||
Domain 的纯计算函数可以声明 `pure`,它调用的方法也要满足纯计算要求。Model 的 REST 字段转换只能调用合规的同领域 pure Domain。文件、网络等外部操作经 Port/Adapter,不直接放进普通 App 或 Domain。
|
||||
|
||||
Dever 推断挂起,无需 async/await。直接调用按顺序完成;`run` 显式启动结构化子任务,`wait/stop` 消费 Task,任务不能逃逸所属作用域。Group/Channel/Stream 具有静态资源约束;不能跨挂起、线程或请求随意保存句柄。`result` 不改变调度,也不绕开 effect 检查。
|
||||
不需要写 async/await,普通调用依次完成。要并发执行时用 `run` 启动子任务,再用 `wait/stop` 收尾;不能把任务留到当前作用域之外。
|
||||
|
||||
`Secret` 不可打印、比较或转为普通 Text。密码使用现有 crypto 能力;不要自制哈希。声明类型、分句和 effect 报错时修正真实边界,不添加无意义中转函数、泛用返回包装或吞错分句来绕过检查。
|
||||
Group/Channel/Stream 的使用范围由编译器检查,不能跨等待、线程或请求随意保存句柄。`result` 只负责捕获结果,不改变调度,也不让原本禁止的操作变成合法。
|
||||
|
||||
`Secret` 不可打印、比较或转为普通 Text。密码使用现有 crypto 能力,不自制哈希。类型、分句或调用能力报错时修正真实问题,不增加中转函数、通用包装或吞错分句来绕过检查。
|
||||
|
||||
更多完整语法以官方核心仓库同版本 `LANGUAGE.md` 为准;新建项目先使用本 skill 的已验证模板。
|
||||
@@ -9,11 +9,11 @@
|
||||
| 字节 | `dever.bytes.from_text/to_text/from_ints/length/at/slice/concat` |
|
||||
| 时间 | `dever.time.now/parse_datetime/format_datetime/parse_date/format_date/duration/add/subtract/difference` |
|
||||
| 密码与摘要 | `dever.crypto.password_hash/password_verify/token/sha256/hmac_sha256/constant_time_eq` |
|
||||
| 类型化 JSON | `dever.json`,API/CMD 通常直接复用自动 wire codec |
|
||||
| 类型化 JSON | `dever.json`,API/CMD 自动按声明类型解析和输出 JSON |
|
||||
| HTTP/网络/文件 | `dever.http`、`dever.net`、`dever.io`,业务外部 I/O 按 Port 边界使用 |
|
||||
| 请求与身份 | `dever.api`、`dever.auth`、`dever.site`,受入口可达性限制 |
|
||||
| 请求与身份 | `dever.api`、`dever.auth`、`dever.site`;只有允许的入口调用链能使用 |
|
||||
|
||||
这是查找索引,不保证省略的参数/返回值。精确函数签名在官方核心仓库同版本 `library/`,复杂标准能力先读取对应文件和 LANGUAGE.md。`parse` 常返回可空值,不能假定失败得到 0。新版本发布前 skill 索引与库签名须一起验证。
|
||||
参数、返回值和错误见 [签名索引](signatures.md),无需下载源码。索引未覆盖的能力再查官方同版本 `library/`。解析失败可能返回 null,也可能产生声明的错误,不能一律当成 0。发布新版本时要核对索引与实际库声明。
|
||||
|
||||
## Dever Package
|
||||
|
||||
@@ -21,22 +21,30 @@ Package 是可复用 Dever 组件。通过项目 setting.json 中的 `package.re
|
||||
|
||||
## 外部 Lib 与普通二进制
|
||||
|
||||
第三方生态叫 Lib。项目 `config/setting.json` 的根 lib 数组声明 `pip:name@version`、`npm:name@version` 或 Go 依赖,使用 `dever lib add/list/install/update/remove/doctor` 管理。add/update 自动准备实际需要的运行环境与构建工具;install 按已有锁精确恢复,不改变配置或锁。run/build 只消费锁,不联网,也不读取系统 Python、Node、Go 或 PATH。
|
||||
第三方生态叫 Lib,在 `config/setting.json` 的根 lib 数组声明。Python 用 `pip:name@version`,JS 用 `npm:name@version`,Go 用对应模块路径和版本。
|
||||
|
||||
锁记录原始来源、摘要、固定构建输入和产物。新机器缺少已构建产物时,install 使用固定输入重建并核对输出摘要;第三方构建不能重现相同字节时会明确失败,不能用更新锁或偷偷重新解析版本冒充恢复。检查详细错误后,由用户决定是否执行 update 重新生成锁。
|
||||
用 `dever lib add/list/install/update/remove/doctor` 管理依赖。add/update 准备所需运行环境与构建工具;install 按已有锁恢复,不改配置或锁。run/build 使用锁定结果,不联网,不借用系统 Python、Node、Go 或 PATH。
|
||||
|
||||
Lib 必须通过领域 Port/Adapter 使用。Port 声明类型与失败合同,Adapter 实现它;App 只调用 Port。声明的类型、返回值、业务失败、超时和 capability 使用统一 Worker 协议,各语言 SDK 不替代编译器类型检查。Python/JS/Go SDK 与可用 runtime 必须匹配 Dever 发行版本。
|
||||
锁记录包的来源、摘要、构建输入与产物。新机器缺少构建结果时,install 按记录重建并核对摘要。重建结果不一致就失败,不能偷偷换版本或改锁;需要重新选版本时,由用户决定执行 update。
|
||||
|
||||
Lib 必须通过领域 Port/Adapter 使用:Port 声明参数、结果和业务错误,Adapter 提供实现,App 只调用 Port。各种语言共用 Worker 协议,统一检查类型、错误、超时和授权能力。SDK 和运行环境必须匹配 Dever 版本,SDK 不能代替编译器类型检查。
|
||||
|
||||
完整 Port、每例独立的 fake、三种语言实现和配置见 [外部 Lib 示例](external-lib.md)。按示例导出同名处理函数,不手写 schema hash、协议握手或操作注册表。
|
||||
|
||||
Dever 不检查第三方语言源码内部的函数拆分、重复实现和业务硬编码;Port 类型与 Worker 协议验证不能代替这部分审查。外部生态承接确需复用的现成能力,不能用于转移普通业务逻辑以规避 Dever 约束,开发规则仍见 [项目开发](development.md)。
|
||||
|
||||
普通 Linux 二进制不需要 Worker 协议。领域 `port.dever`:
|
||||
|
||||
源码:[二进制 Port](../assets/examples/command/module/document/tool/port.dever)
|
||||
|
||||
```dever
|
||||
execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error
|
||||
```
|
||||
|
||||
`adapter.dever`:
|
||||
|
||||
源码:[二进制 Adapter](../assets/examples/command/module/document/tool/adapter.dever)
|
||||
|
||||
```dever
|
||||
external command "bin/tool" {}
|
||||
```
|
||||
@@ -44,3 +52,5 @@ external command "bin/tool" {}
|
||||
将相应架构 ELF 放在领域的 bin/tool,App 调用 `port.execute(["--version"], null)`。结果有 code、stdout、stderr,字节显式转文本;参数不是 shell 字符串。非零退出码仍是结果,启动失败/超时/超限是错误。文件、网络、子进程能力显式声明且受配置授权,不能关闭沙箱来掩盖部署问题。
|
||||
|
||||
测试必须使用 Port fake,不执行真实第三方程序。跨到 ARM 时必须提供目标架构的库和二进制,Dever 不转换已有 x86 ELF。
|
||||
|
||||
[command 项目](../assets/examples/command/) 包含 App、CMD、配置和 fake 测试,可直接 check/test。App 将 stdout/stderr 转成 Text,保留退出码后输出 JSON。本示例不附带 bin/tool;真实 run/build 前必须放入自己需要的目标架构 ELF 和依赖,不能把 fake 结果当成真实程序输出。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 常用标准库真实签名
|
||||
|
||||
下列签名随 skill 版本维护,参数按位置传入;括号里的输出名也是类型化多输出/Worker/wire 合同的一部分。这里只列常用子集,不复制完整语言手册。每段 `signatures` 对应同名官方 library 包,维护者用根 `test/skill_examples.py` 核对公开声明。
|
||||
|
||||
## 文本与数字
|
||||
|
||||
```signatures
|
||||
text.trim(value: Text) (result: Text)
|
||||
text.lower(value: Text) (result: Text)
|
||||
text.upper(value: Text) (result: Text)
|
||||
text.at(value: Text, index: Int) (result: Text?)
|
||||
text.slice(value: Text, start: Int, end: Int) (result: Text?)
|
||||
text.contains(value: Text, pattern: Text) (result: Bool)
|
||||
text.starts_with(value: Text, prefix: Text) (result: Bool)
|
||||
text.ends_with(value: Text, suffix: Text) (result: Bool)
|
||||
text.split(value: Text, separator: Text) (result: List<Text>)
|
||||
text.replace(value: Text, pattern: Text, replacement: Text) (result: Text)
|
||||
text.join(values: List<Text>, separator: Text) (result: Text)
|
||||
int.parse(value: Text) (result: Int?)
|
||||
int.to_text(value: Int) (result: Text)
|
||||
decimal.parse(value: Text) (result: Decimal?)
|
||||
decimal.round(value: Decimal, places: Int) (result: Decimal)
|
||||
decimal.from_int(value: Int) (result: Decimal)
|
||||
decimal.to_text(value: Decimal) (result: Text)
|
||||
float.parse(value: Text) (result: Float?)
|
||||
float.from_int(value: Int) (result: Float)
|
||||
uuid.parse(value: Text) (result: Uuid?)
|
||||
uuid.to_text(value: Uuid) (result: Text)
|
||||
dever.id.from_text(text: Text) (id: Id)
|
||||
dever.id.to_text(id: Id) (text: Text)
|
||||
```
|
||||
|
||||
Int/Decimal/Float/Uuid parse 失败返回 null,必须用互斥分句处理;不要假定解析失败是 0,也不要把 Uuid、Id 和各 Model 的名义 id 混用。Text 索引按 Unicode 标量值;slice 为起点含、终点不含。
|
||||
|
||||
## Bytes、时间与密码
|
||||
|
||||
```signatures
|
||||
dever.bytes.from_text(text: Text) (bytes: Bytes)
|
||||
dever.bytes.to_text(bytes: Bytes) (text: Text)
|
||||
dever.bytes.from_ints(values: List<Int>) (bytes: Bytes)
|
||||
dever.bytes.length(bytes: Bytes) (length: Int)
|
||||
dever.bytes.at(bytes: Bytes, index: Int) (value: Int?)
|
||||
dever.bytes.slice(bytes: Bytes, start: Int, end: Int) (result: Bytes)
|
||||
dever.bytes.concat(left: Bytes, right: Bytes) (bytes: Bytes)
|
||||
dever.time.now() (output: DateTime)
|
||||
dever.time.parse_datetime(text: Text) (output: DateTime) pure
|
||||
dever.time.format_datetime(value: DateTime) (output: Text) pure
|
||||
dever.time.parse_date(text: Text) (output: Date) pure
|
||||
dever.time.format_date(value: Date) (output: Text) pure
|
||||
dever.time.duration(millis: Int) (output: Duration) pure
|
||||
dever.time.add(value: DateTime, duration: Duration) (output: DateTime) pure
|
||||
dever.time.subtract(value: DateTime, duration: Duration) (output: DateTime) pure
|
||||
dever.time.difference(later: DateTime, earlier: DateTime) (output: Duration) pure
|
||||
dever.crypto.password_hash(password: Secret) (value: Text)
|
||||
dever.crypto.password_verify(password: Secret, hash: Text) (value: Bool)
|
||||
dever.crypto.token(bytes: Int) (value: Secret)
|
||||
dever.crypto.sha256(bytes: Bytes) (value: Bytes) pure
|
||||
dever.crypto.hmac_sha256(key: Secret, bytes: Bytes) (value: Bytes) pure
|
||||
dever.crypto.constant_time_eq(left: Bytes, right: Bytes) (equal: Bool) pure
|
||||
```
|
||||
|
||||
与数字 parse 不同,时间 parse 直接返回 DateTime/Date,非法输入传播对应 `DateTimeResult.Failed` / `DateResult.Failed`;format 失败属于 TextResult。Bytes.to_text 的非法 UTF-8 传播 DecodeResult.Failed;from_ints/slice 传播 BufferResult.Failed。密码调用分别传播 HashResult/VerifyResult,token 传播 TokenResult;这些 choice 位于调用所在标准包,完整捕获使用 `result(call)` 和成功/失败分句。不要用普通 Text 打印 Secret。密码 hash/verify 在 `blocking(...)` 中执行且不能处于数据库事务内;真正写入放显式短 transaction 并重验状态。
|
||||
|
||||
## JSON、HTTP 与日志
|
||||
|
||||
```signatures
|
||||
dever.json.parse(source: Text) (document: dever.json.value.Document)
|
||||
dever.json.stringify(document: dever.json.value.Document) (text: Text)
|
||||
dever.json.node(document: dever.json.value.Document, id: Int) (result: dever.json.value.Value?)
|
||||
dever.json.property(document: dever.json.value.Document, id: Int, name: Text) (result: Int?)
|
||||
dever.http.default_limits() (result: Limits)
|
||||
dever.http.send(address: Text, port: Int, request: Request, limits: Limits) (response: Response)
|
||||
dever.log.info(message: Text, fields: Map<Text, Text>) ()
|
||||
dever.log.warn(message: Text, fields: Map<Text, Text>) ()
|
||||
dever.log.error(message: Text, fields: Map<Text, Text>) ()
|
||||
```
|
||||
|
||||
HTTP 签名的 Limits/Request/Response 是 `dever.http` 包类型。Request 字段为 `method: Text`、`target: Text`、`headers: List<dever.http.Header>`、`body: Bytes`;Header 为 `name: Text`、`value: Bytes`;Response 为 `status: Int`、`headers`、`body`。send 返回成功 Response,失败传播 ResponseResult.Failed;它是 Adapter 内的外部 I/O,不是 App 内直接请求外网的入口。自动 HTTP API 使用 App 的 wire 合同,无需拼 Request/Response。JSON parse 返回的 Document 是带 root/nodes 的类型化树,不是任意 Map;普通 API/CMD 直接使用自动 codec。
|
||||
|
||||
日志是同步阻塞能力;已推断为挂起的数据库 App/Job 内通过 `blocking(dever.log.info(...))` 调用,示例见 Job 的 store。不要将编译器要求的边界改成吞掉日志或数据库错误。
|
||||
|
||||
## 请求与权限
|
||||
|
||||
```signatures
|
||||
dever.api.request_id() (id: Text)
|
||||
dever.api.header(name: Text) (value: Text?)
|
||||
dever.api.set_header(name: Text, value: Text) ()
|
||||
dever.auth.id() (value: Text)
|
||||
dever.auth.session() (value: Text)
|
||||
dever.auth.permissions() (values: List<Permission>)
|
||||
dever.auth.save_role(id: Text, name: Text, all_permissions: Bool, permission_keys: List<Text>) ()
|
||||
dever.auth.disable_role(role_id: Text) ()
|
||||
dever.auth.issue_cookie(subject: Text, session: Text, tenant: Text?) ()
|
||||
dever.auth.clear_cookie() ()
|
||||
dever.site.key() (value: Text)
|
||||
```
|
||||
|
||||
Permission 是 `dever.auth.Permission`,字段 key/component/domain/site/action/method 均为 Text。身份读取、权限管理、请求 Header/Cookie 的入口限制仍适用,签名存在不代表任意角色可调用。敏感 Header 不能经普通 header 当 Text 读取。
|
||||
|
||||
以下是编译器专有操作,不按普通 library 声明中的 Int 适配桩推断业务类型:`dever.auth.user_id()` / `tenant_id()` 的结果分别是 verify Identity 对应的名义 Model ID 可空值;`grant_role(user_id, role_id)` / `revoke_role(user_id, role_id)` 的 user_id 必须是该站点的 User Model ID。不要为通过类型检查转换成裸 Int。
|
||||
|
||||
`dever.job.enqueue(job.name, payload, key)` 返回 Id,key 为 Text;零 payload Job 为 `enqueue(job.name, key)`。`enqueue_at` 在末尾增加 DateTime。普通测试中的 `dever.test.drain_jobs(limit: Int)` 返回 Int,`advance_clock(millis: Int)` 无输出;完整声明与调用见 [Job 项目](examples.md#持久-job)。这些静态操作不需要 import,也不能用动态字符串替代 Job 目标。
|
||||
Reference in new issue
Block a user