feat: release Dever 0.1.4 with enforceable project guidance

This commit is contained in:
Antigravity committed 2026-10-09 05:33:57 +00:00
1 parent 201bdee372
commit 89629c2197
86 files changed
+2798 -338

No files matched your search

@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
cmd summarize = app.summarize
@@ -0,0 +1,18 @@
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)
}
}
@@ -0,0 +1,11 @@
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)
}
@@ -0,0 +1,43 @@
type State {
Draft
Published
Rejected(reason: Text)
}
clauses() () {
assert_eq(label(State.Draft), "draft")
assert_eq(label(State.Published), "published")
assert_eq(label(State.Rejected("needs revision")), "needs revision")
assert_eq(display(null), "missing")
assert_eq(display("present"), "present")
assert_eq(grade(59), "fail")
assert_eq(grade(60), "pass")
}
label(state: State.Published) (text: Text) {
text = "published"
}
label(state: State.Rejected(reason)) (text: Text) {
text = reason
}
label(state: other) (text: Text) {
text = "draft"
}
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"
}
@@ -0,0 +1,32 @@
summary() () {
articles = [
app.Article {
title = " First "
published = true
},
app.Article {
title = "Hidden"
published = false
},
app.Article {
title = " "
published = true
},
app.Article {
title = "Second"
published = true
}
]
actual = app.summarize(articles)
assert_eq(actual.titles, ["First", "Second"])
assert_eq(actual.title_length, 11)
each(assert_clean_title, actual.titles)
empty = app.summarize([])
assert_eq(empty.titles, [])
assert_eq(empty.title_length, 0)
}
assert_clean_title(title: Text) () {
assert(title != "")
assert_eq(title, text.trim(title))
}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
external command "bin/tool" {}
@@ -0,0 +1 @@
cmd version = app.version
@@ -0,0 +1,14 @@
type Version {
code: Int
stdout: Text
stderr: Text
}
version() (version: Version) {
output = port.execute(["--version"], null)
version = Version {
code = output.code
stdout = dever.bytes.to_text(output.stdout)
stderr = dever.bytes.to_text(output.stderr)
}
}
@@ -0,0 +1 @@
execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error
@@ -0,0 +1,16 @@
version() () {
output = app.version()
assert_eq(output.code, 0)
assert_eq(output.stdout, "tool 1.0\n")
assert_eq(output.stderr, "")
}
port.execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) {
assert_eq(args, ["--version"])
assert_eq(stdin, null)
output = dever.process.Output {
code = 0
stdout = dever.bytes.from_text("tool 1.0\n")
stderr = dever.bytes.from_text("")
}
}
@@ -0,0 +1,4 @@
{
"lib": ["pip:markdown@3.8.2"],
"log": { "level": "info" }
}
@@ -0,0 +1,3 @@
external pip "python/render.py" {
lib "markdown@3.8.2"
}
@@ -0,0 +1 @@
cmd render = app.render
@@ -0,0 +1,8 @@
type RenderResult {
Rendered(html: Text)
error Rejected(message: Text)
}
render(source: Text) (html: Text) {
html = port.render(source)
}
@@ -0,0 +1,17 @@
package main
import (
"bytes"
"context"
"github.com/yuin/goldmark"
)
func Render(ctx context.Context, payload any, setting any) (any, error) {
var output bytes.Buffer
source := payload.(map[string]any)["source"].(string)
if err := goldmark.Convert([]byte(source), &output); err != nil {
return nil, err
}
return map[string]any{"html": output.String()}, nil
}
@@ -0,0 +1,5 @@
import { marked } from 'marked';
export async function render(payload, setting, signal) {
return { html: await marked.parse(payload.source) };
}
@@ -0,0 +1 @@
render(source: Text) (html: Text) fails app.RenderResult
@@ -0,0 +1,5 @@
import markdown
async def render(payload, setting):
return {"html": markdown.markdown(payload["source"])}
@@ -0,0 +1,16 @@
rejected() () {
verify(result(app.render("rejected input")))
}
port.render(source: Text) (html: Text) {
assert_eq(source, "rejected input")
fail(app.RenderResult.Rejected("invalid document"))
}
verify(value: app.RenderResult.Rejected(message)) () recover("test verifies declared rejection") {
assert_eq(message, "invalid document")
}
verify(value: app.RenderResult.Rendered(html)) () recover("test verifies declared rejection") {
assert(false)
}
@@ -0,0 +1,8 @@
rendered() () {
assert_eq(app.render("# Report"), "<h1>Report</h1>")
}
port.render(source: Text) (html: Text) {
assert_eq(source, "# Report")
html = "<h1>Report</h1>"
}
@@ -0,0 +1,22 @@
{
"database": {
"default": {
"type": "sqlite",
"path": "data/db/platform.db",
"tenant_directory": "data/tenants"
}
},
"tenant": { "database": "default" },
"auth": {
"providers": {
"session": {
"verify": "access.session.verify",
"jwtSecret": "replace-this-example-secret-before-deployment",
"ttlSeconds": 3600
}
}
},
"sites": { "admin": { "path": "admin", "auth": "session" } },
"http": { "host": "127.0.0.1", "port": 8080 },
"log": { "level": "info" }
}
@@ -0,0 +1,60 @@
type Identity {
id: Text
user_id: access.user.model.id?
tenant_id: access.tenant.model.id?
}
verify(claims: dever.auth.Claims) (identity: Identity) {
identity = find_session(uuid.parse(claims.session), claims.tenant, claims)
}
find_session(session_id: Uuid, selected_key: Text, claims: dever.auth.Claims) (
identity: Identity
) {
stored = model.first(
{
where = uuid == session_id and subject == claims.subject and site == claims.site and tenant_key == selected_key and revoked == false and expires_at > dever.time.now(
)
}
)
identity = verified(stored)
}
find_session(session_id: null, selected_key: Text?, claims: dever.auth.Claims) (
identity: Identity
) {
fail(dever.api.Error.Unauthorized)
}
find_session(session_id: Uuid, selected_key: null, claims: dever.auth.Claims) (
identity: Identity
) {
fail(dever.api.Error.Unauthorized)
}
verified(stored: model.Session) (identity: Identity) {
access.user.require_active(stored.user_id)
access.tenant.require_active(stored.tenant_id, stored.tenant_key)
require_membership(
model.membership.exists(
{
where = user_id == stored.user_id and tenant_id == stored.tenant_id and active == true
}
)
)
identity = Identity {
id = stored.subject
user_id = stored.user_id
tenant_id = stored.tenant_id
}
}
verified(stored: null) (identity: Identity) {
fail(dever.api.Error.Unauthorized)
}
require_membership(active: true) () {}
require_membership(active: false) () {
fail(dever.api.Error.Unauthorized)
}
@@ -0,0 +1,10 @@
global type Session {
uuid: Uuid generated unique
subject: Text
user_id: access.user.model.id
tenant_id: access.tenant.model.id
tenant_key: Text
site: Text
expires_at: DateTime
revoked: Bool default false
}
@@ -0,0 +1,7 @@
global type Membership {
user_id: access.user.model.id
tenant_id: access.tenant.model.id
active: Bool default true
}
unique(user_id, tenant_id)
@@ -0,0 +1,9 @@
require_active(tenant_id: model.id, tenant_key: Text) () {
domain.require_active(
model.exists(
{
where = id == tenant_id and key == tenant_key and active == true
}
)
)
}
@@ -0,0 +1,5 @@
require_active(active: true) () {}
require_active(active: false) () {
fail(dever.api.Error.Unauthorized)
}
@@ -0,0 +1,4 @@
global type Tenant {
key: Text unique
active: Bool default true
}
@@ -0,0 +1,9 @@
require_active(user_id: model.id) () {
domain.require_active(
model.exists(
{
where = id == user_id and active == true
}
)
)
}
@@ -0,0 +1,5 @@
require_active(active: true) () {}
require_active(active: false) () {
fail(dever.api.Error.Unauthorized)
}
@@ -0,0 +1,4 @@
global type User {
uuid: Uuid generated unique
active: Bool default true
}
@@ -0,0 +1,3 @@
normalize(value: Text) (title: Text) pure {
title = text.trim(value)
}
@@ -0,0 +1,4 @@
type Article {
title: Text(1, 200) create = domain.normalize(input) replace = domain.normalize(input)
body: Text
}
@@ -0,0 +1 @@
public get ready = app.status
@@ -0,0 +1,3 @@
status() (healthy: Bool) {
healthy = true
}
@@ -0,0 +1,3 @@
title() () {
assert_eq(domain.normalize(" Draft "), "Draft")
}
@@ -0,0 +1,12 @@
{
"database": { "default": { "type": "sqlite", "path": "data/jobs.db" } },
"runtime": { "mode": "worker", "shutdown_ms": 30000 },
"job": {
"workers": 1,
"poll_ms": 1000,
"lease_ms": 60000,
"retry_base_ms": 1000,
"retry_max_ms": 300000
},
"log": { "level": "info" }
}
@@ -0,0 +1,3 @@
cmd submit = app.submit
cmd total = app.total
@@ -0,0 +1,29 @@
type Request {
key: Text
title: Text
}
submit(input: Request) (id: Id) {
id = dever.job.enqueue(job.store, input, input.key)
}
store(input: Request) () {
stored = model.create(
{
request_key = input.key
title = input.title
}
)
blocking(
dever.log.info(
"Report archived",
{
"request_key" = stored.request_key
}
)
)
}
total() (count: Int) {
count = model.count()
}
@@ -0,0 +1,5 @@
database default
job store(input: app.Request) () retry(3) timeout(30000) {
app.store(input)
}
@@ -0,0 +1,4 @@
type Archive {
request_key: Text unique
title: Text
}
@@ -0,0 +1,12 @@
queued() () {
request = app.Request {
key = "test-report"
title = "Report"
}
first_id = app.submit(request)
assert_eq(app.submit(request), first_id)
assert_eq(dever.test.drain_jobs(5), 1)
assert_eq(app.total(), 1)
dever.test.advance_clock(1000)
assert_eq(dever.test.drain_jobs(5), 0)
}
+12 -12
View File
@@ -1,14 +1,14 @@
# Dever 应用开发
<!-- dever-language:start -->
# Dever 开发规则
在本项目运行 `dever skill path .`,读取返回目录的 `SKILL.md` 和 `references/development.md`,使用与项目版本匹配的规则。AI 尚未接入 skill 时,可用 `dever skill install <新的AI技能目录>/dever-language` 创建稳定入口。这里使用 Dever 语言,不是 Go Dever 框架;命令缺失时按 README 的安装入口处理,不猜语法或用 dever-go 替代。
每次开发、修改或审查前,在项目根目录执行 `dever skill path .`,完整读取返回目录中的 `SKILL.md`、`references/development.md` 和本次工作相关的参考文件。使用项目对应版本的规则;命令缺失时按 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。
- 默认只实现当前接口和语法。除非用户明确要求,禁止添加旧参数、旧配置、旧接口的兼容分支、别名或备用实现。改接口时同步修改本项目调用方,删除本次废弃路径。锁定编译器版本不等于保留兼容代码。
- 修改前先查实际调用链和已有能力。复用现成实现;不重复手写解析、分页、JSON 拼装或鉴权,不添加空转发层,不按行数或函数数量机械拆文件。函数按完整业务职责组织。
- 源码放 `module/<组件>/<领域>/`,只创建需要的 `app/domain/model/api/port/adapter/job`。普通 CRUD 用 Model 与 REST;App 编排业务,Domain 写业务规则,外部能力经 Port/Adapter。跨领域只调用 App,不建 `utils/common/service` 杂物目录。
- 不写 `main.dever`、`package`、`import` 或 `expose`。API、CMD、Job 是入口。保持项目源码格式一致;Markdown 的说明必须与代码一致。
- 部署配置只放 `config/setting.json`,不增加环境变量配置,不写死真实地址或凭据。业务数据放 Model;用户、租户和权限来自可信上下文,禁止按特殊账号或租户 ID 开权限特例。正常字面量和明确业务常量可以保留。
- 禁止假调用、吞错、假成功、弱化测试、擅自修改接口基线或关闭检查;不把业务搬到第三方语言来绕过约束。只清理本次引入或废弃的代码,保留无关修改。
- 修改后运行范围内的 `dever fmt . --check`、`dever check .` 和定向测试。测试放 `test/<组件>/<领域>/`,外部能力使用 Port fake;例如 `dever test . --filter news.article`。每例默认 60 秒,可用 `--timeout-ms` 调整。遵守用户对全量测试、服务、真实数据库和压测的限制。
- 如实报告验证范围:`check` 不运行程序,fake 不证明真实服务可用,规则写在 AGENTS 中不等于编译器已全部强制检查。`run/build` 不隐式联网;未经要求不提交或推送 Git。
<!-- dever-language:end -->
+2 -2
View File
@@ -15,9 +15,9 @@ dever build . --output hello-app
问候结果为 `Hello, Dever`。输入由 `api` 的 CMD 声明解析并传给 `app.greet`;业务不读取环境变量,配置在 `config/setting.json`。
以上命令适用于这个最小模板;项目扩大后按改动选择验证,`dever test .` 会执行整个项目的应用套件。`check` 静态执行与 run/build 相同的应用入口可达性及冗余硬检查,无需准备运行资源;它不执行应用,不能代替运行或业务验收。
这些命令适用于最小模板。项目扩大后用 `dever test . --filter 组件.领域` 运行相关测试;不带筛选会执行全部测试。`check` 检查代码、配置和无法从业务入口调用到的代码,不运行应用,不能代替业务验收。
AI 开发规范见本项目 `AGENTS.md`。运行 `dever skill path .` 获取项目对应版本的完整 `dever-language` skill,开发前读取其中 `references/development.md`。AI 的稳定 skill 入口可用 `dever skill install <新的AI技能目录>/dever-language` 安装;核心与配套 skill 通过 `dever update` 同步更新,已锁定版本的项目仍使用自己的版本。
AI 开发规则已写在 `AGENTS.md`。AI 开始任务前运行 `dever skill path .`,读取对应版本的完整指引。已有项目或需要刷新规则时,在项目根目录执行 `dever skill install`,无需指定目录;它只更新 Dever 规则块,保留其他内容。`dever update` 同步更新核心和 skill,锁定版本的项目仍使用自己的版本。
首次安装使用 `curl -fsSL https://git.dever.cc/shemic/dever/raw/branch/main/install | bash`。开发机须为 Linux x86_64、glibc 2.39+,并具备 Bash、curl、Python 3.9+、支持 Ed25519 的 OpenSSL 和管理员权限;非 root 用户需要 sudo。完整条件见 [安装指引](https://git.dever.cc/shemic/dever/src/branch/main/skills/references/install.md)。下载安装无需 token 或项目配置。