feat: unify source and skills for Dever 0.1.2 installation
This commit is contained in:
1 parent
a2e0436ed4
commit
19f122b4b4
47 files changed
+1951
-63
No files matched your search
-1
Submodule skills deleted from 27ac8f21c2.
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: dever-language
|
||||
description: 开发、检查、测试或维护 Dever 语言应用(.dever、.dever.md),创建新项目,安装或更新 dever 命令,编写 App、Domain、Model、API、CMD、Job、Port/Adapter,接入 Dever Package 或 Python/JS/Go Lib 时使用。也用于 Dever 应用目录、权限、租户和命名约束检查。不要用于 Go Dever 框架、dever-go 或普通 Go 组件开发。
|
||||
---
|
||||
|
||||
# Dever 语言开发
|
||||
|
||||
Dever 是独立语言。应用源码使用 `.dever` 或 `.dever.md`,不沿用 Go Dever 的 service、Page JSON 或 Provider 目录;第三方语言实现经 Port/Adapter 接入。
|
||||
|
||||
## 先确定版本和范围
|
||||
|
||||
1. 读取用户项目的 `AGENTS.md`、现有目录和 `config/setting.json`。回答、分析、诊断只做只读检查;用户要求修改时再实施,不擅自安装软件、改服务或改全局命令。
|
||||
2. 若已安装机器版 Dever,新项目运行 `dever skill path`,已有项目运行 `dever skill path <project-root>`,读取返回目录中的 `SKILL.md` 与本次需要的参考文件。已经在那个目录读取本 skill 时不再递归加载。它由 `dever update` 与核心一起更新;已有项目按其可选 dever.version 选择对应指引,不能把活动版本新增语法强加给旧项目。
|
||||
3. 尚无 `dever` 时按 [安装与更新](references/install.md) 引导安装;不能把 `dever-go` 当作 Dever 语言命令。公开发行包缺失时如实说明,不编造成功、不自动编译一套缺少 runtime 的开发二进制。
|
||||
4. 新项目使用 `dever new <new-directory>`;Markdown 项目加 `--markdown`。不要手工猜模板,不覆盖已有目录。项目模板的用途和检查命令见 [项目开发](references/development.md)。
|
||||
|
||||
开发、重构或审查应用代码前,必须读取 [项目开发](references/development.md) 中的文件与函数组织、配置与硬编码、禁止绕过检查、验证范围。安装或纯语法问答只加载与请求相关的参考。
|
||||
|
||||
## 按需要阅读
|
||||
|
||||
| 工作 | 先读 |
|
||||
| --- | --- |
|
||||
| 新项目、目录、命名、测试与交付 | [development.md](references/development.md) |
|
||||
| 类型、分句、函数、集合、失败和并发 | [language.md](references/language.md) |
|
||||
| HTTP/CMD、Model、权限、租户、Job | [application.md](references/application.md) |
|
||||
| 标准能力、外部生态、Package | [libraries.md](references/libraries.md) |
|
||||
| Markdown 源码 | [markdown.md](references/markdown.md) |
|
||||
| 安装命令、安装 skill、更新 | [install.md](references/install.md) |
|
||||
|
||||
只加载当前任务相关参考。当前版本的编译器检查和已验证模板决定语法;参考中没有的调用先查官方对应版本源码,不能按其它语言习惯杜撰方法。
|
||||
|
||||
## 必须遵守的应用边界
|
||||
|
||||
- 配置只在 `config/setting.json`;不通过环境变量读取配置、密码、数据库 URL 或选择工具链。不要把 Dever 的官方仓库地址、更新配置写入业务项目。
|
||||
- 应用目录是 `module/<component>/<domain>/<role>.dever`;role 只有 `app/domain/model/port/adapter/api/job`。复杂职责再按主题拆分,不机械地每个函数建一个文件,不建 `common/utils/helper/base/service` 杂物目录。
|
||||
- 不写 `package/import/exposes/main`。应用入口由 API、CMD、Job 声明生成。App 是对外领域能力;跨领域只调用目标 App,不直接调用其私有 Model、Domain、Adapter。
|
||||
- API 只声明绑定或 REST,App 编排,Domain 保存内聚的业务规则,Model 声明持久化合同,Port/Adapter 隔离外部 I/O。已有标准能力直接复用,禁止重复手写 ID 解析、分页裁剪、JSON 拼装等碎片函数。
|
||||
- 函数使用命名输出和互斥、穷尽的相邻分句;没有 `if/else/switch/match/return`、任意循环或递归。不要靠分句顺序隐藏重叠规则。
|
||||
- HTTP 默认有权限,匿名入口才写 `public`。不添加 `auth.require`、客户端自报身份/权限或每个 API 重复鉴权。站点按配置里的 API 目录匹配,租户按数据库隔离。
|
||||
- 测试放在 `test/<component>/<domain>/<topic>.dever`,入口为文件同名 `topic() ()`。外部调用使用 case-local Port fake,不在普通测试启动真实 Lib 或访问真实业务数据库。
|
||||
- `run/build` 离线。依赖准备使用显式 `dever package` / `dever lib` 命令;已有锁在新机器用 `dever lib install`,ARM 应用目标用 `dever target add linux-aarch64`。这些命令按需准备机器共享资源,不扫描宿主 Python、Node、Go 或 PATH 来充当运行环境。
|
||||
|
||||
## 工作方式
|
||||
|
||||
1. 修改前查相关领域、实际调用链、标准能力和既有实现。对非平凡改动简要说明缺少什么行为、应该由哪一层负责、哪些文件需要改变;已有实现满足要求时保留它。
|
||||
2. 实施时以完整业务职责组织文件和函数。抽取须有业务含义、明确不变量、实际复用或降低复杂流程理解成本的作用;不按行数或函数个数机械拆分。只清理本次引入或废弃的重复与死代码,保留无关改动。
|
||||
3. 完成后审查新增调用链、公开合同、配置与可信身份来源,检查本次引入的诊断并执行范围内验证。禁止假调用、吞错、假成功、擅自修改接口基线或弱化测试来通过检查;详见开发指引。
|
||||
|
||||
`fmt --check`、`check`、`test` 和 `run/build` 的覆盖范围不同。`check` 与 `run/build` 共用应用入口可达性和冗余硬检查,保持静态、离线,不执行应用;具体见 [验证范围](references/development.md#验证范围)。`dever test` 没有筛选参数,已有大项目不擅自全量测试;新模板的小套件可完整验证。已有授权覆盖的验证直接执行,启动服务、接真实数据库或更改系统安装须遵守用户的范围限制。
|
||||
|
||||
交付说明实际改动、验证结果与未验证边界。规则写入 skill 不等于新增编译器检查,能编译不等于业务验收、性能验收或已发布。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Dever 语言开发"
|
||||
short_description: "安装与更新 Dever,创建项目,按语言约束开发、检查和测试应用"
|
||||
default_prompt: "使用 $dever-language 创建并验证一个符合当前语言规范的 Dever 项目。"
|
||||
@@ -0,0 +1 @@
|
||||
7889e0edaf5fbf1060bef4ad6e27106128485c8412a6f6cd4457dc67e112986e
|
||||
@@ -0,0 +1,2 @@
|
||||
/data/
|
||||
/hello-app
|
||||
@@ -0,0 +1,14 @@
|
||||
# Dever 应用开发
|
||||
|
||||
在本项目运行 `dever skill path .`,读取返回目录的 `SKILL.md` 和 `references/development.md`,使用与项目版本匹配的规则。AI 尚未接入 skill 时,可用 `dever skill install <新的AI技能目录>/dever-language` 创建稳定入口。这里使用 Dever 语言,不是 Go Dever 框架;命令缺失时按 README 的安装入口处理,不猜语法或用 dever-go 替代。
|
||||
|
||||
- 源码放在 `module/<组件>/<领域>/`,按职责使用 `app`、`domain`、`model`、`api`、`port`、`adapter`、`job`;只创建实际需要的角色。
|
||||
- 不写 `main.dever`、`package`、`import` 或 `expose`。API/CMD/Job 是入口;跨领域调用 App。
|
||||
- 部署配置只用 `config/setting.json`,不增加环境变量配置或写死真实地址、凭据。用户、租户和权限来自可信上下文,不用特殊账号 ID 判断权限;正常字面量与明确业务常量可以保留。
|
||||
- 修改前先读实际调用链并查找复用点。文件按内聚业务主题组织,函数须有业务含义、不变量、实际复用或隔离复杂流程的作用;不按行数拆分,不机械地一个函数一个文件,不建 `utils/common` 等通用桶。
|
||||
- 普通 CRUD 用 Model 与 REST,增加业务合同或编排才写 App,不叠加空转发层。API 只声明入口,Domain 保存业务规则,外部能力经 Port/Adapter。
|
||||
- 不用假调用、吞错、假成功、修改接口基线、弱化测试或转移到第三方语言绕过检查。只清理本次引入或废弃的代码,保留无关修改。
|
||||
- 保持当前项目源码格式一致。Markdown 源码只编译顶层 `dever` 围栏,声明文档必须与源码一致。
|
||||
- 修改后执行范围内的 `dever fmt . --check`、`dever check .` 和适当验证。check 静态执行与 run/build 相同的应用入口可达性及冗余硬检查,不准备外部依赖或执行应用;报告实际覆盖范围,不能把 check 成功当作全部验收。
|
||||
- 普通测试位于 `test/<组件>/<领域>/`,用文件同名零输入零输出函数,外部能力用 Port fake。`dever test .` 会运行全部应用测试,已有大项目不默认全量执行;新模板的小套件可完整验证。遵守用户对生产数据、服务、真实数据库和压测的限制。
|
||||
- 安装、升级、依赖准备与业务运行分开;`run/build` 不隐式联网。没有用户要求,不提交或推送 Git。
|
||||
@@ -0,0 +1,24 @@
|
||||
# Dever 应用
|
||||
|
||||
这是无需数据库、鉴权或第三方工具的最小 CMD 项目。
|
||||
|
||||
在项目目录运行:
|
||||
|
||||
```sh
|
||||
dever check .
|
||||
dever fmt . --check
|
||||
dever test .
|
||||
dever run . -- hello.greeting.greet '{"name":"Dever"}'
|
||||
dever build . --output hello-app
|
||||
./hello-app hello.greeting.greet '{"name":"Dever"}'
|
||||
```
|
||||
|
||||
问候结果为 `Hello, Dever`。输入由 `api` 的 CMD 声明解析并传给 `app.greet`;业务不读取环境变量,配置在 `config/setting.json`。
|
||||
|
||||
以上命令适用于这个最小模板;项目扩大后按改动选择验证,`dever test .` 会执行整个项目的应用套件。`check` 静态执行与 run/build 相同的应用入口可达性及冗余硬检查,无需准备运行资源;它不执行应用,不能代替运行或业务验收。
|
||||
|
||||
AI 开发规范见本项目 `AGENTS.md`。运行 `dever skill path .` 获取项目对应版本的完整 `dever-language` skill,开发前读取其中 `references/development.md`。AI 的稳定 skill 入口可用 `dever skill install <新的AI技能目录>/dever-language` 安装;核心与配套 skill 通过 `dever update` 同步更新,已锁定版本的项目仍使用自己的版本。
|
||||
|
||||
首次安装入口为 `https://git.dever.cc/shemic/dever/install`,源码仓库为 `ssh://git@git.dever.cc:2222/shemic/dever.git`。安装前确认该站已发布对应平台的脚本和签名包;部署完成后可用 `curl -fsSL https://git.dever.cc/shemic/dever/install | bash`,机器安装需要管理员权限。仓库可访问不代表二进制已经发布。
|
||||
|
||||
普通 `.dever` 与 Markdown `.dever.md` 共享语言规则。用 `dever new <新目录> --markdown` 创建 Markdown 项目。目录必须不存在,生成器不会覆盖已有项目。
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
cmd greet = app.greet
|
||||
@@ -0,0 +1,17 @@
|
||||
# 问候命令
|
||||
|
||||
将命令行 JSON 输入交给 App。
|
||||
|
||||
- 包:`hello.greeting.api`
|
||||
- 公开类型:无
|
||||
- 公开方法:无
|
||||
- 使用:无
|
||||
|
||||
## greet
|
||||
|
||||
声明问候命令。
|
||||
|
||||
- 声明:`cmd greet`
|
||||
|
||||
```dever
|
||||
{{source}}```
|
||||
@@ -0,0 +1,3 @@
|
||||
greet(name: Text) (message: Text) {
|
||||
message = "Hello, " + name
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# 问候
|
||||
|
||||
提供可复用的问候用例。
|
||||
|
||||
- 包:`hello.greeting.app`
|
||||
- 公开类型:无
|
||||
- 公开方法:
|
||||
- `greet`
|
||||
- 使用:
|
||||
- `hello.greeting.greet(name)`
|
||||
|
||||
## greet
|
||||
|
||||
根据名字生成问候。
|
||||
|
||||
- 函数:`greet`
|
||||
- 输入:
|
||||
- `name: Text`:名字
|
||||
- 输出:
|
||||
- `message: Text`:问候
|
||||
|
||||
```dever
|
||||
{{source}}```
|
||||
@@ -0,0 +1,4 @@
|
||||
greet() () {
|
||||
message = app.greet("Dever")
|
||||
assert_eq(message, "Hello, Dever")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# 问候测试
|
||||
|
||||
检查问候用例的输出。
|
||||
|
||||
- 包:`hello.greeting.greet`
|
||||
- 公开类型:无
|
||||
- 公开方法:无
|
||||
- 使用:无
|
||||
|
||||
## greet
|
||||
|
||||
调用真实 App 并断言输出。
|
||||
|
||||
- 函数:`greet`
|
||||
- 输入:无
|
||||
- 输出:无
|
||||
|
||||
```dever
|
||||
{{source}}```
|
||||
@@ -0,0 +1,67 @@
|
||||
# 应用接口、数据与后台任务
|
||||
|
||||
## API 和 CMD
|
||||
|
||||
`module/news/article/api/admin/manage.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
|
||||
```
|
||||
|
||||
API 无函数体,继承 App 输入/输出签名。支持 get/post/put/delete,当前没有 patch、旧 get_ 前缀或源码内 site 声明。`cmd greet = app.greet` 生成 `component.domain.greet` 命令,参数来自单个 JSON 对象;main.dever 不存在,也不读取命令行参数。
|
||||
|
||||
GET/DELETE 从 query 读标量参数,POST/PUT 从 JSON body 读参数;额外、重复或缺少必填字段均拒绝。无需自己读 body 或构造 dever.http.Response。成功统一为 `{"code":0,"message":"ok","data":...}`;错误 data 为 null,code 对应状态码。接口必须绑定一个可序列化输出,输出名可自定。
|
||||
|
||||
`dever.api.Error` 中 Invalid/Unauthorized/Forbidden/NotFound/Conflict/TooManyRequests 对应 400/401/403/404/409/429;未知内部失败记录脱敏日志并返回安全的 500。
|
||||
|
||||
写 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`。
|
||||
|
||||
所有 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 或返回自报权限。
|
||||
|
||||
认证成功的受保护 App 可读取 `dever.auth.id()`、`session()`、`user_id()`、`tenant_id()`、`dever.site.key()`;来源是隐藏可信上下文,不能让客户端提交 user_id/tenant_id 当作身份。名义 ID 必须与站点 verify 的类型对应,可空 ID 经分句处理后才能使用。public/CMD/Job/普通测试不会自动获得身份读取能力。
|
||||
|
||||
核心生成 `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)`,不等同于接口角色权限。
|
||||
|
||||
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:
|
||||
|
||||
```dever
|
||||
type Article {
|
||||
title: Text(1, 200)
|
||||
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 明确编排,不能自动推测关联图级联写入。
|
||||
|
||||
纯粹 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 私有字段。
|
||||
|
||||
不为每个 CRUD 机械建立 App、Domain、Port 和 Adapter;Model 的数据库操作无需再套一层 Port/repository。实际新增业务合同或编排时才增加对应实现。完整反重复、反硬编码及检查边界见 [项目开发](development.md)。
|
||||
|
||||
## 租户、任务与部署配置
|
||||
|
||||
配置只有 database 租户隔离:`tenant.database` 选控制连接,`global type` 使用平台库,其余持久 Model 与 Job 使用租户库。没有 field/table/mode 配置。租户缺失、未迁移或 schema 过期明确失败,不回退平台库。
|
||||
|
||||
运维命令:`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。
|
||||
|
||||
部署配置的真实结构可查官方 CMS `config/setting.json`,只添加项目用到的 database、tenant、auth、sites、http、job、runtime、log。最小 CMD 项目只需模板配置。密码、token 与真实数据库连接不提交 Git。
|
||||
@@ -0,0 +1,128 @@
|
||||
# 项目开发
|
||||
|
||||
新建最小项目:
|
||||
|
||||
```sh
|
||||
dever new my_app
|
||||
dever fmt my_app --check
|
||||
dever check my_app
|
||||
dever test my_app
|
||||
dever run my_app -- hello.greeting.greet '{"name":"Dever"}'
|
||||
dever build my_app --output my_app/program
|
||||
./my_app/program hello.greeting.greet '{"name":"Dever"}'
|
||||
```
|
||||
|
||||
创建 Markdown 项目用 `dever new my_app --markdown`。两种模板使用相同代码和行为,Markdown 另加编译器校验的说明。目标目录必须不存在,父目录必须已存在;不接受向现有项目合并或覆盖。build 输出也必须不存在。
|
||||
|
||||
模板只包含 CMD、App、配置、一个测试和开发指引,无需数据库或 HTTP 服务。实际项目在它的目录合同上扩展,替换演示领域,删除不再使用的示例与测试。添加 HTTP 时须完成 [站点与身份配置](application.md),不能只把 cmd 换成 get。
|
||||
|
||||
```text
|
||||
my_app/
|
||||
AGENTS.md
|
||||
README.md
|
||||
config/setting.json
|
||||
module/
|
||||
news/article/
|
||||
app.dever
|
||||
domain.dever
|
||||
model.dever
|
||||
port.dever
|
||||
adapter.dever
|
||||
job.dever
|
||||
api/
|
||||
admin/manage.dever
|
||||
front/browse.dever
|
||||
test/news/article/publish.dever
|
||||
```
|
||||
|
||||
这是可用职责位置,不是要求创建全部文件。component、domain 和文件主题使用有业务意义的 ASCII 小写蛇形命名;类型用 PascalCase,方法和字段用 snake_case。不要增加任意第三层领域目录。组件按业务能力划分,领域按一致的数据和业务边界划分。组件、领域和主题不能命名为 `common/shared/utils/helper/helpers/base/service/services` 等编译器禁止的通用桶。
|
||||
|
||||
| 位置 | 责任和访问规则 |
|
||||
| --- | --- |
|
||||
| App | 跨领域公开能力,校验与业务编排;同领域用 `app.publish`,外部用 `news.article.publish` |
|
||||
| Domain | 同领域内部业务规则;纯计算优先放这里,不承接数据库/外部 I/O |
|
||||
| Model | 存储字段、索引、关系、迁移、Seed、静态 SQL;同领域 App 使用 ORM |
|
||||
| Port | 外部能力的类型、操作与失败合同 |
|
||||
| Adapter | 实现 Port;私有,不反调 App/Domain/Model |
|
||||
| API | HTTP/CMD 入口绑定与自动 REST,无业务函数体 |
|
||||
| Job | 持久任务声明,绑定同领域 App |
|
||||
| Test | 文件同名、零输入零输出的唯一测试入口 |
|
||||
|
||||
## 文件与函数组织
|
||||
|
||||
简单领域保留角色单文件;复杂角色可以拆为 `app/publish.dever`、`app/search.dever` 等内聚主题,调用名不带文件名。非 API topic 目录不递归。App/Domain/Port/Adapter/Job 单文件与同名目录二选一,选择目录时至少有两个内聚主题文件;Model/API 主文件与 topic 目录可按编译器合同共存,不受这个最少文件数限制。只有 `api/` 可按站点和资源继续嵌套,目录会影响 URL。
|
||||
|
||||
一个文件可以包含多个相关类型、函数和分句。按完整业务主题命名,不机械地一个函数一个文件,也不为尚不存在的需求创建空 App、Domain、Port 或 Adapter。
|
||||
|
||||
抽取函数应表达独立业务规则、维护明确的不变量、复用已有逻辑,或隔离难以理解的复杂流程。只使用一次的函数也可以有这些作用;不能只因几行代码能起名字就再加一层。职责内聚的代码不按行数强拆,也不通过把所有逻辑塞进一个长函数来规避碎片化。
|
||||
|
||||
| 情况 | 处理方式 |
|
||||
| --- | --- |
|
||||
| `parse_id` 重新手写数字解析并把失败悄悄变成 0 | 复用已有解析、名义 ID 和明确失败合同,不复制算法 |
|
||||
| `trim_name` 只原样转发一次标准 trim,没有新增合同 | 在使用处调用标准能力 |
|
||||
| `page_slice` 重复 Model 已有的分页规则 | 使用 Model 分页,不在各领域复制一份 |
|
||||
| `publish` 组织发布条件、状态变化和后续任务 | 保留完整 App 能力,把内聚业务规则放同领域 Domain |
|
||||
| 单次使用的规则需要独立表达有效期或状态不变量 | 可以抽取并按业务含义命名 |
|
||||
| 两处相似代码承担不同规则、未来变化原因不同 | 保持各自归属,不为表面相似增加通用调度层 |
|
||||
|
||||
相同规则先复用现有所有者;只有真实共享的逻辑才抽象。跨领域共享能力由负责该业务的 App 提供,不直接调用对方私有实现,不为缩短调用名建立空转发链。普通 CRUD 直接用 Model 和 `rest model`;需要超出 Model 字段合同的输入转换、业务校验、关联操作或输出合同才增加 App 编排,具体见 [应用规则](application.md#modelcrud-与业务编排)。
|
||||
|
||||
## 配置与硬编码
|
||||
|
||||
| 数据或规则 | 归属 |
|
||||
| --- | --- |
|
||||
| 随部署变化的服务地址、端口、数据库连接、凭据 | `config/setting.json`,不增加环境变量配置,不提交真实秘密 |
|
||||
| 当前用户、租户、权限及资源归属 | 可信认证上下文、核心授权与 Model 策略,不接受客户端自报身份 |
|
||||
| 有固定业务含义的状态、范围和规则 | 类型、互斥分句、字段约束或内聚 Domain 定义,复用同一规则 |
|
||||
| 需要用户或运营维护的业务数据 | 项目 Model 与相应业务流程,不把具体记录写进源码分支 |
|
||||
| 测试 ID、样例地址和固定结果 | 测试夹具,不作为生产失败时的兜底 |
|
||||
|
||||
禁止按某个账号、租户、记录的裸 ID、名字或地址开特例,例如用 `user_id == 1` 代替管理员授权。普通查询参数可以标识目标记录,但不能因此成为操作者身份或访问权限。
|
||||
|
||||
避免硬编码不等于搬走所有字面量。局部的 0、1、提示文字和稳定协议常量可以保留;有业务意义、难以理解或反复出现的值由现有类型或领域定义表达。只有确需运行时变化的值才增加配置。官方发行源是工具链固定合同,不能塞进业务 setting.json。
|
||||
|
||||
## 禁止绕过检查
|
||||
|
||||
- 不添加假入口、无意义调用或仅供测试触达的调用,掩盖生产入口不可达的 App/Domain。删除本次废弃代码或接通真实业务调用链。
|
||||
- 不把重复业务逻辑转移到 Adapter、Python、JS 或 Go 来避开 Dever 检查。外部实现的质量同样需要审查。
|
||||
- 不吞掉失败、用空值或假数据冒充成功;有业务依据的恢复须使用明确结果合同,并验证恢复行为。
|
||||
- 不为通过检查擅自改 `dever.api`、删失败测试、弱化断言或关闭权限、类型及沙箱限制。确需改变公开合同,先说明调用方影响,在用户授权范围内同步实现、基线与测试。
|
||||
- 普通应用开发不修改编译器或核心库来容纳违规代码;语言或核心维护须有相应任务授权。只处理当前改动相关问题,不借机重写无关代码。
|
||||
|
||||
## 测试与检查
|
||||
|
||||
`test/hello/greeting/greet.dever`:
|
||||
|
||||
```dever
|
||||
greet() () {
|
||||
assert_eq(hello.greeting.greet("Dever"), "Hello, Dever")
|
||||
}
|
||||
```
|
||||
|
||||
模板中的实际字符串以生成文件为准。测试只执行文件同名函数,不需要 main 或注解。`assert` 接受 Bool,`assert_eq` 要求同类型。测试可访问同领域 App/Domain,跨领域仅 App,不能直接写 Model CRUD。使用 Model 时运行器使用临时 SQLite;不使用 Model 时不需要数据库。它不会读取生产 setting.json 或生产 data。对业务失败使用 `result(call)` 配合 choice 分句验证。
|
||||
|
||||
常用命令:`fmt --check` 查格式,`check` 查语法/类型/目录/分句/配置,`api` 输出 API 合同,`test` 运行应用套件,`run` 开发执行,`build` 生成独立程序。不要发明 `test --filter`、`run api` 或 `dever init`。
|
||||
|
||||
### 验证范围
|
||||
|
||||
| 检查 | 能证明的范围 |
|
||||
| --- | --- |
|
||||
| `dever fmt <root> --check` | 源码格式;不证明类型或业务正确 |
|
||||
| `dever check <root>` | 静态语法、类型、目录、命名、分句、相关配置、已有 API 基线,以及应用入口可达性和冗余硬检查;不执行应用 |
|
||||
| `dever test <root>` | 当前项目全部应用测试的实际断言;没有筛选参数,不证明生产入口完整 |
|
||||
| `dever run/build` | 通过与 check 相同的应用硬检查,再准备本地编译资源并执行或编译;成功不等于部署或性能验收 |
|
||||
| AI 审查 | 业务含义、职责拆分、复用、硬编码与验证范围,不能替代程序检查 |
|
||||
|
||||
`check/run/build` 共用 C016,拒绝生产入口不可达的 App/Domain,以及符合现有判定条件的私有空转发和 Model CRUD 包装;并非任意角色、所有函数或全部 CRUD 包装都能检出。`check` 不准备 Worker 或 runtime pack,不进行原生编译,不启动应用、服务或数据库。`test` 保持独立的测试入口与 Port fake 检查,不执行生产入口可达性检查;不能用测试调用掩盖生产死代码,也不能把静态检查当作运行验收。
|
||||
|
||||
W001–W005 等诊断包含未使用赋值、部分冗余条件、相同私有实现、重复纯调用和空转发建议,警告本身不会使命令失败。审查本次引入的诊断,修正有证据的问题;不能声称编译器能判定全部硬编码或业务价值。skill 中的开发要求不自动变成编译器硬错误。
|
||||
|
||||
选择与改动相符且已获授权的检查。新模板的小套件可以完整运行;已有大项目不默认运行全量 `dever test`,也不默认启动服务、连接真实数据库或压测。需要但尚未运行的检查要说明原因和剩余证据,不能伪报通过。已有 `dever.api` 时保留基线约束;`dever api <root> --output <新文件>` 可以导出待比较合同,不覆盖原文件。
|
||||
|
||||
源码放 module/,测试放 test/,部署配置放 config/,运行数据放 data/。生成程序从二进制同级 config/setting.json 读取配置。应用无需打包编译器;使用外部 Lib 时相关 runtime/依赖由 build 嵌入。只读 check 不证明真实数据库或部署验收。
|
||||
|
||||
## 核心与业务的区别
|
||||
|
||||
统一源码仓库地址为 `ssh://git@git.dever.cc:2222/shemic/dever.git`,迁移与发行状态见 [安装指引](install.md)。`crates/dever-core` 负责语言检查/编译,`crates/dever-runtime` 负责运行时,`crates/dever-cli` 负责命令,`library/` 是随版本提供的标准 Dever 源码。维护中的完整业务示例是 `examples/cms/dever` 与 `examples/cms/md`;`examples/old` 是历史回归,不作新项目模板。
|
||||
|
||||
开发普通应用无需复制或修改这些核心目录。可复用业务组件作为 Dever Package;外部 Python/JS/Go 生态称 Lib。多个项目共享机器安装的 Dever。
|
||||
@@ -0,0 +1,79 @@
|
||||
# 安装与更新
|
||||
|
||||
安装入口为 `https://git.dever.cc/shemic/dever/install`,官方二进制源为同站 `/shemic/dever/dist`。这些地址由发行工具固定,不写入业务项目配置,不通过环境变量选择。Git 源码可访问不代表安装端点或二进制已发布;服务器尚未部署时如实报告,不能把登录 HTML 当作脚本。
|
||||
|
||||
## 安装 dever
|
||||
|
||||
先检查 `dever --help`。机器版提供 new、install、update、target、version、skill;仅源码开发二进制不管理机器安装。不要把已有同名命令直接删掉或覆盖,先识别归属。
|
||||
|
||||
初次安装需要 Linux、Bash、Python 3.9 或更新版本的 `/usr/bin/python3` 和支持 Ed25519 的 `/usr/bin/openssl`;管道下载还需要 curl。非 root 安装通过 `/usr/bin/sudo` 提权,帮助不提权。这些工具用于校验和安装,生成的应用不依赖系统 Python、Node、Go 或 Rust。检查安装脚本并取得机器安装授权后执行:
|
||||
|
||||
还须满足所选发行包的主机 ABI 要求;当前准备的 Linux x86_64 编译器要求 glibc 2.39。ARM64 应用交叉构建可在 x86_64 主机使用,不能据此宣称 ARM64 宿主编译器包已经发布。
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.dever.cc/shemic/dever/install | bash
|
||||
```
|
||||
|
||||
无需先克隆源码或安装 skill。查看参数用 `bash -s -- --help`;固定版本用 `bash -s -- --version <准确版本>`,替换上面命令尾部的 `bash` 即可。本地已下载脚本可用 `bash install --help` 检查。
|
||||
|
||||
该命令下载当前平台最新已签名基础包。脚本包含独立固定公钥,先校验发行元数据,再验证引导程序及其私有库摘要,由引导程序解压 Zstandard 包;无需系统 zstd。安装 `/opt/dever`、`/usr/local/bin/dever`、机器共享服务和必要沙箱策略,默认启动服务。`--no-start` 可仅安装。没有对应正式资产时明确失败,不修改已安装版本。
|
||||
|
||||
基础包包含 Dever 编译器、本机应用编译资源、四种数据库运行库和配套 skill。Python、Node、Go、第三方原生构建工具与 ARM 应用目标按需安装,保存在机器共享资源目录;同一版本的多个用户和项目共用一份。
|
||||
|
||||
## 将 skill 接入 AI
|
||||
|
||||
安装核心后,将稳定入口装到用户选择的 AI 技能目录:
|
||||
|
||||
```sh
|
||||
dever skill install <新的AI技能目录>/dever-language
|
||||
```
|
||||
|
||||
目录必须尚不存在,不能覆盖已有 skill。入口每次解析与核心配套的完整 skill,重载 AI 工具的技能列表后使用。这是 Dever 语言的 `dever-language`,不是 Go 框架的 `shemic-dever`。不自动修改机器上其他用户的 AI 目录。
|
||||
|
||||
## 按需准备资源
|
||||
|
||||
首次添加或更新 Lib 时,`dever lib add/update <project-root> ...` 自动准备相应生态,只有进入源码构建流程时才准备构建工具。已有 `dever.lock` 的项目在新机器运行 `dever lib install <project-root>`,按锁恢复依赖,保持配置和锁文件不变;没有 Lib 的 Python/JS/Go Adapter 也可通过该命令准备环境。缺少锁时先显式 `dever lib update <project-root>` 生成锁。不要把 install 当作重新选版本的 update。
|
||||
|
||||
在 x86_64 机器准备 ARM64 应用目标:
|
||||
|
||||
```sh
|
||||
dever target add linux-aarch64
|
||||
dever lib install <project-root> --target linux-aarch64
|
||||
dever build <project-root> --target linux-aarch64 --output <新文件>
|
||||
```
|
||||
|
||||
只用 Dever、没有外部依赖的项目省略 lib install。目标 Lib 必须已有匹配目标的锁;首次生成 ARM 锁使用 lib add/update 的同名 target 参数。准备会显式联网,后续 run/build 离线;缺资源时执行提示的准备命令,不改用系统解释器。target add 准备当前活动版本,锁定其它编译器版本的项目先准备并选择对应版本。
|
||||
|
||||
已准备本地发行包时:
|
||||
|
||||
```sh
|
||||
bash install --release /可信发行目录 --trusted-key /独立确认的公钥文件
|
||||
```
|
||||
|
||||
测试或镜像制作增加 `--system-root /已有的自有镜像目录`;不会启动宿主服务。安装器生成的临时 `config/setting.json` 仅供机器 bootstrap 使用,包含本地发行路径、安装根和公钥路径;不是业务配置,不写发行仓库地址或环境变量。
|
||||
|
||||
公钥可公开提交;签名私钥绝不能进 skill、源码仓库或发行包。只有已独立确认的公钥才可作为 `--trusted-key`,不能从未知下载包顺手取一把钥匙作为信任依据。
|
||||
|
||||
已持有完整 skill 时仍可直接运行 `sudo python3 <skill目录>/scripts/install.py`,它与根 Bash 入口复用同一安装实现。
|
||||
|
||||
## 一条命令更新核心与 skill
|
||||
|
||||
```sh
|
||||
sudo dever update
|
||||
dever version
|
||||
dever skill path
|
||||
```
|
||||
|
||||
update 只访问内置官方发行源,先准备核心、同版本完整 skill,以及当前活动版本已安装的扩展,再切换活动版本。未使用的扩展不会一起下载;任一准备失败保留原活动版本。普通 check/test/run/build 不检查更新、不隐式联网。
|
||||
|
||||
从旧 GitHub 发行源迁移时,需要在新源正式包发布后执行一次新的 `install`。旧启动器和守护进程内置旧地址,普通 update 不负责替换这两个稳定程序;只修改仓库 remote 或重新发布旧包不能完成迁移。新 install 成功后再用 `dever update`。
|
||||
|
||||
本仓库直接安装的 skill 在开始开发时先运行 `dever skill path` 并加载该版本的完整 skill,不必每个项目 git pull。也可以用 `dever skill install <新的AI技能目录>/dever-language` 安装稳定的薄入口,之后每次自动解析当前机器版本;已有目录不会被覆盖。不要把新入口嵌套装进已经存在的完整 skill 目录。
|
||||
|
||||
项目可选 `config/setting.json` 的 `dever.version` 锁定准确 `major.minor.patch`,未锁定时使用机器活动版本。`dever install 版本` 准备指定版本;`dever use 版本` 显式切换活动版本。update 不改业务项目文件或数据库。锁定项目用 `dever skill path <project-root>` 读取对应指引,不能仅凭活动版指南采用新语法。
|
||||
|
||||
## 源码维护者
|
||||
|
||||
统一源码仓库为 `ssh://git@git.dever.cc:2222/shemic/dever.git`。`skills/` 是主仓库中的普通目录,与核心、模板和安装器一起提交、校验及发行;无需初始化 submodule 或另行克隆 skill 仓库。普通应用开发者无需克隆核心或编译 Rust。
|
||||
|
||||
根 `install` 从 `skills/scripts/install.py` 与 `skills/assets/release.pub` 生成;修改安装器后运行 `python3 sdk/install-entry.py`,交付前运行 `python3 sdk/install-entry.py --check`。禁止单独修改生成物中的安装算法。首个新源发行包发布前,默认下载会明确失败;使用真实签名本地包可验证安装,但不能代表线上安装或更新已经通过。
|
||||
@@ -0,0 +1,83 @@
|
||||
# 核心语法
|
||||
|
||||
以下片段说明语法。将它们放进正确角色文件并补完整调用链后检查,不能当作免配置 HTTP 项目。
|
||||
|
||||
## 类型和函数
|
||||
|
||||
```dever
|
||||
type Greeting {
|
||||
message: Text
|
||||
}
|
||||
|
||||
greet(name: Text) (response: Greeting) {
|
||||
response = Greeting { message = "Hello, " + text.trim(name) }
|
||||
}
|
||||
```
|
||||
|
||||
函数是 `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 用分支构造:
|
||||
|
||||
```dever
|
||||
type State {
|
||||
Draft
|
||||
Published
|
||||
Rejected(reason: Text)
|
||||
}
|
||||
|
||||
label(state: State.Published) (text: Text) {
|
||||
text = "published"
|
||||
}
|
||||
|
||||
label(state: State.Rejected(reason)) (text: Text) {
|
||||
text = reason
|
||||
}
|
||||
|
||||
label(state: other) (text: Text) {
|
||||
text = "draft"
|
||||
}
|
||||
```
|
||||
|
||||
## 分支与边界
|
||||
|
||||
没有 if、else、switch、match、条件表达式或递归。同名、同参数数量的分句必须连续,相同输出合同,互斥且穷尽;没有先匹配谁的顺序优先级。`other` 是同一参数位置所有明确模式的补集。
|
||||
|
||||
```dever
|
||||
display(value: Text) (text: Text) {
|
||||
text = value
|
||||
}
|
||||
|
||||
display(value: null) (text: Text) {
|
||||
text = "missing"
|
||||
}
|
||||
|
||||
grade(score: Int < 60) (text: Text) {
|
||||
text = "fail"
|
||||
}
|
||||
|
||||
grade(score: Int >= 60) (text: Text) {
|
||||
text = "pass"
|
||||
}
|
||||
```
|
||||
|
||||
支持完整类型、choice 分支、null、Bool/Text/数值字面量、Int/Decimal 范围与 other 模式;多输入检查全部输入组合。任一函数都必须为命名输出赋值。不要写 `return` 提前退出,按分句表示结果与失败。
|
||||
|
||||
字面量分句适合明确的状态和规则,不意味着可以按某个用户 ID、租户名字或样例数据写业务特例。运行时配置与可信身份的归属见 [配置与硬编码](development.md#配置与硬编码)。
|
||||
|
||||
集合遍历使用语言的 `map/filter/reduce/each` 等核心操作及静态 handler,不能加入 for/while。handler 的签名和可达调用链由编译器检查,不是运行期闭包或动态插件。具体签名查当前版本 `library/` 与核心语言指南,不杜撰 lambda 语法。
|
||||
|
||||
## 失败和能力
|
||||
|
||||
业务错误通过 choice 的 `error` 分支声明,`fail` 传播,`result(call)` 显式捕获;普通调用直接得到成功值。不能用字符串/任意 JSON 代替类型化失败,不能捕获后悄悄吞掉错误。错误映射到 HTTP 的方式见 application.md。
|
||||
|
||||
纯 Domain 函数可声明 `pure`,编译器检查其调用链;要从 Model REST 字段来源调用的业务转换必须是合规的同领域 pure Domain。外部文件/网络 I/O 经 Port/Adapter,不把系统调用塞入普通 App 或 Domain。
|
||||
|
||||
Dever 推断挂起,无需 async/await。直接调用按顺序完成;`run` 显式启动结构化子任务,`wait/stop` 消费 Task,任务不能逃逸所属作用域。Group/Channel/Stream 具有静态资源约束;不能跨挂起、线程或请求随意保存句柄。`result` 不改变调度,也不绕开 effect 检查。
|
||||
|
||||
`Secret` 不可打印、比较或转为普通 Text。密码使用现有 crypto 能力;不要自制哈希。声明类型、分句和 effect 报错时修正真实边界,不添加无意义中转函数、泛用返回包装或吞错分句来绕过检查。
|
||||
|
||||
更多完整语法以官方核心仓库同版本 `LANGUAGE.md` 为准;新建项目先使用本 skill 的已验证模板。
|
||||
@@ -0,0 +1,46 @@
|
||||
# 标准能力与依赖
|
||||
|
||||
内置 package 不需要 import、下载或把标准库复制到 module/。先查已有能力,避免项目里堆积 parse_id、page_slice、trim 包装函数。
|
||||
|
||||
| 能力 | 入口 |
|
||||
| --- | --- |
|
||||
| 文本 | `text.trim/lower/upper/contains/starts_with/ends_with/split/replace/join` |
|
||||
| 数字 | `int.parse/to_text`、`decimal.parse/round/from_int`、`float.parse`、`math` |
|
||||
| 字节 | `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 |
|
||||
| HTTP/网络/文件 | `dever.http`、`dever.net`、`dever.io`,业务外部 I/O 按 Port 边界使用 |
|
||||
| 请求与身份 | `dever.api`、`dever.auth`、`dever.site`,受入口可达性限制 |
|
||||
|
||||
这是查找索引,不保证省略的参数/返回值。精确函数签名在官方核心仓库同版本 `library/`,复杂标准能力先读取对应文件和 LANGUAGE.md。`parse` 常返回可空值,不能假定失败得到 0。新版本发布前 skill 索引与库签名须一起验证。
|
||||
|
||||
## Dever Package
|
||||
|
||||
Package 是可复用 Dever 组件。通过项目 setting.json 中的 `package.registry` 与 `package.use` 声明;显式 `dever package add|list|update|remove|doctor <root> ...` 管理。`dever.lock` 固定版本、摘要和传递依赖;Package 不能覆盖本地同名组件。不要在业务源码中引入 import 或自建 vendor 查找规则。
|
||||
|
||||
## 外部 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。
|
||||
|
||||
锁记录原始来源、摘要、固定构建输入和产物。新机器缺少已构建产物时,install 使用固定输入重建并核对输出摘要;第三方构建不能重现相同字节时会明确失败,不能用更新锁或偷偷重新解析版本冒充恢复。检查详细错误后,由用户决定是否执行 update 重新生成锁。
|
||||
|
||||
Lib 必须通过领域 Port/Adapter 使用。Port 声明类型与失败合同,Adapter 实现它;App 只调用 Port。声明的类型、返回值、业务失败、超时和 capability 使用统一 Worker 协议,各语言 SDK 不替代编译器类型检查。Python/JS/Go SDK 与可用 runtime 必须匹配 Dever 发行版本。
|
||||
|
||||
Dever 不检查第三方语言源码内部的函数拆分、重复实现和业务硬编码;Port 类型与 Worker 协议验证不能代替这部分审查。外部生态承接确需复用的现成能力,不能用于转移普通业务逻辑以规避 Dever 约束,开发规则仍见 [项目开发](development.md)。
|
||||
|
||||
普通 Linux 二进制不需要 Worker 协议。领域 `port.dever`:
|
||||
|
||||
```dever
|
||||
execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error
|
||||
```
|
||||
|
||||
`adapter.dever`:
|
||||
|
||||
```dever
|
||||
external command "bin/tool" {}
|
||||
```
|
||||
|
||||
将相应架构 ELF 放在领域的 bin/tool,App 调用 `port.execute(["--version"], null)`。结果有 code、stdout、stderr,字节显式转文本;参数不是 shell 字符串。非零退出码仍是结果,启动失败/超时/超限是错误。文件、网络、子进程能力显式声明且受配置授权,不能关闭沙箱来掩盖部署问题。
|
||||
|
||||
测试必须使用 Port fake,不执行真实第三方程序。跨到 ARM 时必须提供目标架构的库和二进制,Dever 不转换已有 x86 ELF。
|
||||
@@ -0,0 +1,15 @@
|
||||
# Markdown 源码
|
||||
|
||||
使用 `dever new <new-directory> --markdown` 获得经过验证的模板;不要只把任意 Markdown 中的代码围栏改名当作完整程序。
|
||||
|
||||
文件后缀 `.dever.md`;编译器只编译顶层 `dever` 或 `typescript dever` 围栏,且校验说明与代码合同。普通 `.md` 不是源码,两种源码格式的类型、目录、访问、权限约束一致。
|
||||
|
||||
- 每个文件一个一级标题,随后写用途、包、公开类型、公开方法、使用;空项写“无”。包说明来自路径,不是源码 package 声明。
|
||||
- 每个逻辑声明有自己的二级标题、用途和唯一代码块,一级标题下不直接放源码。
|
||||
- 类型写 `- 类型:` 与字段/分支说明;函数写 `- 函数:`、输入、输出,名称、类型、顺序与代码一致。每个字段说明包含非空中文用途。
|
||||
- 同名同参数数量的所有分句放同一章节、同一围栏;不同签名或类型分开。
|
||||
- API 写 `- 声明:cmd greet`、`post publish`、`rest model` 等,与代码一致,不虚构公开方法。
|
||||
- App 自动公开其类型/函数;Domain、Adapter、API 和测试没有公开方法。调用清单隐藏 app role 和 app topic。
|
||||
- 测试同样需要完整说明,唯一入口仍是文件同名零输入零输出函数。
|
||||
|
||||
维护源码与说明后一起跑 fmt --check、check。完整声明说明协议见官方核心仓库同版本 MARKDOWN-SYNTAX.md。
|
||||
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install an authenticated Dever release; no application configuration is read."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
RELEASES = "https://git.dever.cc/shemic/dever/dist"
|
||||
MAX_METADATA = 2 * 1024 * 1024
|
||||
MAX_RELEASE = 2 * 1024 * 1024 * 1024
|
||||
ASSET_HOSTS = {"git.dever.cc"}
|
||||
|
||||
|
||||
class OfficialRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, request, stream, code, message, headers, address):
|
||||
target = urllib.parse.urlsplit(address)
|
||||
if (target.scheme != "https" or target.hostname not in ASSET_HOSTS
|
||||
or target.username or target.password or target.port not in (None, 443)
|
||||
or target.fragment):
|
||||
raise ValueError("发行下载重定向离开官方 HTTPS 站点")
|
||||
return super().redirect_request(request, stream, code, message, headers, address)
|
||||
|
||||
|
||||
def download(address):
|
||||
# Installation does not inherit HTTP proxy/environment configuration.
|
||||
client = urllib.request.build_opener(
|
||||
urllib.request.ProxyHandler({}), OfficialRedirect(),
|
||||
urllib.request.HTTPSHandler(context=ssl.create_default_context()),
|
||||
)
|
||||
try:
|
||||
return client.open(address, timeout=60)
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code == 404:
|
||||
raise ValueError("该版本/平台尚无官方发行包,未修改已安装的 Dever") from error
|
||||
raise ValueError(f"官方发行下载失败:HTTP {error.code}") from error
|
||||
|
||||
|
||||
def platform_name():
|
||||
host = os.uname()
|
||||
names = {"x86_64": "linux-x86_64", "aarch64": "linux-aarch64"}
|
||||
if host.sysname != "Linux" or host.machine not in names:
|
||||
raise ValueError("安装器当前只支持 Linux x86_64/aarch64,且需对应平台已发布工具包")
|
||||
return names[host.machine]
|
||||
|
||||
|
||||
def prerequisites(platform):
|
||||
if sys.version_info < (3, 9):
|
||||
raise ValueError("安装需要 /usr/bin/python3 3.9 或更新版本")
|
||||
if not os.access("/usr/bin/openssl", os.X_OK):
|
||||
raise ValueError("安装需要 /usr/bin/openssl(支持 Ed25519)")
|
||||
try:
|
||||
libc = os.confstr("CS_GNU_LIBC_VERSION") or ""
|
||||
except (OSError, ValueError):
|
||||
libc = ""
|
||||
match = re.fullmatch(r"glibc ([0-9]+)\.([0-9]+)", libc)
|
||||
if not match:
|
||||
raise ValueError("官方 Linux 工具包需要 glibc,当前系统 ABI 不支持")
|
||||
if platform == "linux-x86_64" and tuple(map(int, match.groups())) < (2, 39):
|
||||
raise ValueError("当前 Linux x86_64 工具包需要 glibc 2.39 或更新版本")
|
||||
|
||||
|
||||
def read_bounded(path, limit):
|
||||
with path.open("rb") as stream:
|
||||
contents = stream.read(limit + 1)
|
||||
if len(contents) > limit:
|
||||
raise ValueError("发行元数据超过大小限制")
|
||||
return contents
|
||||
|
||||
|
||||
def metadata(release, source, selection, platform):
|
||||
for filename in ("manifest.json", "manifest.sig"):
|
||||
if source:
|
||||
contents = read_bounded(source / filename, MAX_METADATA)
|
||||
else:
|
||||
with download(f"{RELEASES}/{selection}/dever-{platform}.{filename}") as stream:
|
||||
contents = stream.read(MAX_METADATA + 1)
|
||||
if len(contents) > MAX_METADATA:
|
||||
raise ValueError("发行元数据超过大小限制")
|
||||
(release / filename).write_bytes(contents)
|
||||
|
||||
|
||||
def authenticate(release, key_path, scratch, platform, requested):
|
||||
key = bytes.fromhex(read_bounded(key_path, 256).decode().strip())
|
||||
signature = bytes.fromhex(read_bounded(release / "manifest.sig", 256).decode().strip())
|
||||
if len(key) != 32 or len(signature) != 64:
|
||||
raise ValueError("发行公钥或签名长度错误")
|
||||
public_der = scratch / "public.der"
|
||||
public_der.write_bytes(bytes.fromhex("302a300506032b6570032100") + key)
|
||||
signature_file = scratch / "signature.bin"
|
||||
signature_file.write_bytes(signature)
|
||||
verified = subprocess.run(
|
||||
["/usr/bin/openssl", "pkeyutl", "-verify", "-pubin", "-keyform", "DER",
|
||||
"-inkey", str(public_der), "-rawin", "-in", str(release / "manifest.json"),
|
||||
"-sigfile", str(signature_file)],
|
||||
env={}, capture_output=True, timeout=15, check=False,
|
||||
)
|
||||
if verified.returncode:
|
||||
raise ValueError("发行签名校验失败,未执行安装程序")
|
||||
manifest = json.loads((release / "manifest.json").read_text())
|
||||
if (set(manifest) != {"format", "version", "platform", "artifacts", "extensions"}
|
||||
or manifest.get("format") != "dever-release-v2" or manifest.get("platform") != platform
|
||||
or not isinstance(manifest["extensions"], list) or len(manifest["extensions"]) > 12):
|
||||
raise ValueError("发行格式或平台不匹配")
|
||||
version = manifest.get("version", "")
|
||||
if not re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", version):
|
||||
raise ValueError("发行版本不是准确的 major.minor.patch")
|
||||
if requested != "latest" and version != requested:
|
||||
raise ValueError("发行版本与请求不匹配")
|
||||
artifacts = manifest.get("artifacts", [])
|
||||
if not 1 <= len(artifacts) <= 4096:
|
||||
raise ValueError("发行文件数超限")
|
||||
catalog = {}
|
||||
total = 0
|
||||
for artifact in artifacts:
|
||||
name, size, digest = artifact["path"], artifact["bytes"], artifact["sha256"]
|
||||
if (set(artifact) != {"path", "bytes", "sha256"}
|
||||
or not name or name.startswith("/")
|
||||
or any(part in ("", ".", "..") for part in name.split("/"))
|
||||
or any(character in name for character in ("\\", ":", "\0"))
|
||||
or str(PurePosixPath(name)) != name
|
||||
or name in catalog or name in ("manifest.json", "manifest.sig")):
|
||||
raise ValueError("发行包含不安全或重复路径")
|
||||
if type(size) is not int or size < 0 or not re.fullmatch(r"[0-9a-f]{64}", digest):
|
||||
raise ValueError("发行文件长度或摘要错误")
|
||||
total += size
|
||||
if total > MAX_RELEASE:
|
||||
raise ValueError("发行解压长度超过 2 GiB")
|
||||
catalog[name] = artifact
|
||||
if not {"dever-core", "bootstrap/dever", "skills/dever-language/SKILL.md"} <= catalog.keys():
|
||||
raise ValueError("首装包缺少核心、bootstrap 启动器或配套 skill")
|
||||
return version, catalog, key
|
||||
|
||||
|
||||
def copy_artifact(stream, destination, artifact):
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
remaining = artifact["bytes"]
|
||||
digest = hashlib.sha256()
|
||||
with destination.open("xb") as output:
|
||||
while remaining:
|
||||
contents = stream.read(min(remaining, 1024 * 1024))
|
||||
if not contents:
|
||||
raise ValueError("发行文件被截断")
|
||||
output.write(contents)
|
||||
digest.update(contents)
|
||||
remaining -= len(contents)
|
||||
if digest.hexdigest() != artifact["sha256"]:
|
||||
raise ValueError("发行文件摘要校验失败")
|
||||
executable = artifact["path"] in {"dever-core", "bootstrap/dever", "bootstrap/deverd"}
|
||||
destination.chmod(0o755 if executable else 0o644)
|
||||
|
||||
|
||||
def payload(release, source, version, platform, catalog, trusted_key=None):
|
||||
if source:
|
||||
for name, artifact in catalog.items():
|
||||
path = source / name
|
||||
for ancestor in (path, *path.parents):
|
||||
if ancestor.is_symlink():
|
||||
raise ValueError("本地发行文件不能经过符号链接")
|
||||
if ancestor == source:
|
||||
break
|
||||
if not path.is_file() or path.stat().st_size != artifact["bytes"]:
|
||||
raise ValueError("本地发行文件类型或长度错误")
|
||||
with path.open("rb") as stream:
|
||||
copy_artifact(stream, release / name, artifact)
|
||||
return
|
||||
if trusted_key is None:
|
||||
raise ValueError("网络发行解压需要独立信任公钥")
|
||||
# Only signed bytes are executable. Keep the helper separate so Rust can
|
||||
# create_new every archive member and enforce one complete codec/TAR contract.
|
||||
with tempfile.TemporaryDirectory(prefix=".extract-", dir=release.parent) as directory:
|
||||
scratch = Path(directory)
|
||||
helper = scratch / "helper"
|
||||
base = f"{RELEASES}/{version}/dever-{platform}"
|
||||
for name, artifact in catalog.items():
|
||||
if name == "bootstrap/dever" or name.startswith("bootstrap/lib/"):
|
||||
with download(f"{base}.blob-{artifact['sha256']}") as stream:
|
||||
copy_artifact(stream, helper / name, artifact)
|
||||
if stream.read(1):
|
||||
raise ValueError("bootstrap 下载超过签名长度")
|
||||
compressed = scratch / "base.tar.zst"
|
||||
download_archive(f"{base}.tar.zst", compressed)
|
||||
configuration = scratch / "extract.json"
|
||||
configuration.write_text(json.dumps({
|
||||
"release": str(release), "archive": str(compressed),
|
||||
"trusted_key": str(trusted_key),
|
||||
}))
|
||||
subprocess.run([str(helper / "bootstrap/dever"), "--dever-extract", str(configuration)],
|
||||
env={}, check=True, timeout=180)
|
||||
|
||||
|
||||
def download_archive(address, destination):
|
||||
remaining = MAX_RELEASE
|
||||
with download(address) as stream, destination.open("xb") as output:
|
||||
while contents := stream.read(min(remaining + 1, 1024 * 1024)):
|
||||
remaining -= len(contents)
|
||||
if remaining < 0:
|
||||
raise ValueError("发行下载超过 2 GiB")
|
||||
output.write(contents)
|
||||
|
||||
|
||||
def install(arguments, default_key=None):
|
||||
if os.geteuid() != 0:
|
||||
raise ValueError("机器安装需要管理员权限;先检查脚本,再用 sudo python3 执行")
|
||||
platform = platform_name()
|
||||
prerequisites(platform)
|
||||
if arguments.version != "latest" and not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", arguments.version):
|
||||
raise ValueError("--version 需要 latest 或准确版本")
|
||||
system_root = arguments.system_root.absolute()
|
||||
if not system_root.is_dir() or system_root.is_symlink():
|
||||
raise ValueError("--system-root 必须是已存在的真实目录")
|
||||
source = arguments.release.absolute() if arguments.release else None
|
||||
if source and (not source.is_dir() or source.is_symlink()):
|
||||
raise ValueError("--release 必须是已存在的真实发行目录")
|
||||
# The root-owned staging also keeps the pinned key outside the downloaded release.
|
||||
with tempfile.TemporaryDirectory(prefix=".dever-install-", dir=system_root) as directory:
|
||||
scratch = Path(directory)
|
||||
release = scratch / "release"
|
||||
release.mkdir()
|
||||
metadata(release, source, arguments.version, platform)
|
||||
key_path = arguments.trusted_key
|
||||
if key_path is None:
|
||||
key_path = scratch / "independent-release.pub"
|
||||
key_path.write_text(default_key)
|
||||
version, catalog, key = authenticate(release, key_path, scratch, platform, arguments.version)
|
||||
trusted = scratch / "release.pub"
|
||||
trusted.write_text(key.hex() + "\n")
|
||||
payload(release, source, version, platform, catalog, trusted)
|
||||
configuration = scratch / "config"
|
||||
configuration.mkdir()
|
||||
(configuration / "setting.json").write_text(json.dumps({
|
||||
"release": str(release), "system_root": str(system_root),
|
||||
"trusted_key": str(trusted),
|
||||
"activate_service": system_root == Path("/") and not arguments.no_start,
|
||||
}))
|
||||
subprocess.run([str(release / "bootstrap/dever"), "--dever-install", str(scratch)],
|
||||
env={}, check=True, timeout=180)
|
||||
print(f"Dever {version} 已安装到 {system_root / 'opt/dever'}")
|
||||
|
||||
|
||||
def main(default_key=None):
|
||||
parser = argparse.ArgumentParser(prog="dever install", description=__doc__)
|
||||
parser.add_argument("--version", default="latest")
|
||||
parser.add_argument("--release", type=Path, help="使用本地已签名发行目录,不下载")
|
||||
key_path = None if default_key is not None else Path(__file__).resolve().parents[1] / "assets/release.pub"
|
||||
parser.add_argument("--trusted-key", type=Path, default=key_path)
|
||||
parser.add_argument("--system-root", type=Path, default=Path("/"), help="已有的自有安装镜像目录;默认安装当前机器")
|
||||
parser.add_argument("--no-start", action="store_true", help="不启动当前机器 systemd 服务")
|
||||
arguments = parser.parse_args()
|
||||
try:
|
||||
install(arguments, default_key)
|
||||
except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as error:
|
||||
parser.exit(1, f"Dever 安装失败:{error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user