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
@@ -28,7 +28,7 @@ Applies to AST/HIR contract metadata, typed LLVM/resource/coroutine lowering, pr
|
||||
- Application tests use `test/<component>/<domain>/<topic>.dever[.md]`; the filename selects the one ordinary zero-input, zero-output `<topic>() ()` entry. Test helpers/types remain private to that file. Tests call same-domain App/Domain and cross-domain App only; they cannot call Model operations, Port, Adapter, API or another test, and production cannot call tests.
|
||||
- Test assertions are compiler-owned `assert(Bool)` and `assert_eq(actual, expected)`. Equality requires the same comparable type; only `null`/empty expressions that already require context receive the left type, so numeric promotion does not make unequal source types valid. Business failures continue through `result(call)` and explicit recovery.
|
||||
- CLI: `check <root>`, `test <root>`, `run <root> [--tenant <id>] [-- <cmd> <json>]`, `build <root> --output <new-file>`, `tenant migrate <root> <id>`, `tenant owner <root> <id> <site> <user-id>` and `tenant component enable|disable <root> <id> <component>`. Run/build collect checked API/CMD/Job declarations into one generated entry and reject application `main.dever`. Test checks `module/ + test/` before running stable-sorted cases; check/run/build load only module/. Deployment and database settings come only from `config/setting.json`.
|
||||
- CLI check and application compilation share `Program::application_errors()` before resource preparation: C016 rejects unreachable App/Domain functions and recognized trivial private forwarders or Model CRUD wrappers, not every wrapper shape. Check remains static and offline, with no Worker preparation, runtime pack, native compilation, service or database execution. Test retains its case-local roots and Port fakes without applying production reachability; core library checking still permits exports whose consumers are unknown.
|
||||
- CLI check and application compilation share `Program::application_errors()` before resource preparation: C016 rejects unreachable App/Domain functions and recognized trivial private forwarders or Model CRUD wrappers, not every wrapper shape. Within production-reachable App/Domain, it also rejects same-owner private implementations with identical full contracts/bodies and empty transitive effects, and repeated pure calls with unchanged inputs whose previous result remains available. Reuse the W003 structural key and W004 version/result proof rather than interpreting warning text. Do not promote effectful W003 matches: the structural key does not encode every Model query property. Keep different owners/contracts, mutations, Adapter and bundled code outside these two hard rules. Check remains static and offline, with no Worker preparation, runtime pack, native compilation, service or database execution. Test retains its case-local roots and Port fakes without applying production application errors; core library checking still permits exports whose consumers are unknown.
|
||||
- Module API files contain declarations such as `get profile = app.profile`, `post publish = app.publish`, `cmd bootstrap = app.bootstrap` and `rest model`; they do not contain handler function bodies. HTTP routes inherit the App signature and return the fixed envelope. `public` is valid only on one HTTP declaration. Recursive `api/<site-path>/<topic>.dever` supplies configured site ownership without source `site` statements.
|
||||
- Authentication providers, sites, external HTTP origins, Cookie names/lifetimes and tenant routing come only from `config/setting.json`. Verify is one read-only App function from `Claims` to an App-owned record with exactly `id: Text`, `user_id: ModelId?`, and `tenant_id: ModelId?`; it cannot return permissions. A protected site's generated entry verifies its configured provider, enters trusted tenant scope, checks every reachable tenant component and authorizes the exact permission before reading business input or dispatching App. A `public` declaration ignores an ambient browser Cookie, but an explicitly supplied invalid `Authorization` credential remains an authentication error.
|
||||
|
||||
@@ -82,7 +82,7 @@ Facts, effects and failures share `contracts/dependencies.rs` for function/stati
|
||||
|
||||
Domain dependency edges come only from resolved type, function and handler references. Physical role files in one domain share one dependency boundary; cross-domain cycles remain invalid and function recursion remains independently invalid. Source code does not duplicate edges in a header, and dependency inference does not install or download packages.
|
||||
|
||||
API snapshots sort semantic names and contracts, not source paths/IDs. A baseline is optional, explicit and create-new; checks never update it. Protect its edits outside the language. CLI suppresses bundled-source suggestions only; Program retains diagnostics and bundled hard errors remain visible. Entry-specific export advice must not infer unknown library consumers.
|
||||
API snapshots sort semantic names and contracts, not source paths/IDs. A baseline is optional, explicit and create-new; checks never update it. Protect its edits outside the language. Program retains all warnings. CLI suppresses bundled-source suggestions and W003/W004/W005 already represented by a C016 with the same primary and nonempty ordered related spans. Different evidence or warning codes remain visible, as do bundled hard errors. Baseline failure takes precedence and retains original warnings; API/Test do not apply production-error suppression. Entry-specific export advice must not infer unknown library consumers.
|
||||
|
||||
Test authority comes from compiler-assigned source origin, never a user-controlled path string. A checked `TestCase` holds the private function ID and database-effect metadata; native compilation accepts only that checked identity and does not relax normal entry visibility. Each case runs serially in its own native process and temporary project. Successful child output stays captured; failed stdout/stderr is reported in labelled sections, later tests continue, and any failed case makes the command fail. Tests cannot reach `dever.api.serve()`.
|
||||
|
||||
@@ -125,6 +125,7 @@ Sensitivity belongs to the cached type graph in `types.rs`: Secret and private M
|
||||
| Invalid Model or transaction database contract | C014 |
|
||||
| Contract analysis exceeds bounded work | C015, no executable Program |
|
||||
| Proven redundant check / duplicate private implementation / repeated pure call / trivial private forwarding | W002 / W003 / W004 / W005 |
|
||||
| Production App/Domain orphan, recognized forwarding/CRUD wrapper, same-owner identical private effect-free implementation or proven reusable pure call | C016 before resource preparation; libraries/Test retain warnings |
|
||||
| Unused or overwritten write | Existing W001, no duplicate W006 |
|
||||
| Unneeded exposure for a selected application | W007, not a hard error |
|
||||
| API drift or unreadable baseline | CLI failure before native compilation |
|
||||
@@ -164,6 +165,8 @@ LLVM changes select root `llvm_lowering` and embedded-bridge `llvm_native`: real
|
||||
|
||||
Select `structured_concurrency`, `async_native`, `contract_syntax`, `contract_facts`, `contract_semantics`, `contract_api`, `contract_redundancy`, `contract_execution`, `error_effects` according to changes. Assert suspension propagation through ordinary calls and concrete handler substitution, non-transferable liveness, worker boundaries, API omission, alias mutation, handler variance, short-circuit/empty traversal, overwrite/projection obligations, bounded shared graphs and unchanged native results. Error capture tests cover native/reference wrapping, explicit outer identity, ambiguity and discarded captures. Parser tests must reject the removed package-header dependency clause; formatter round trips must prove it cannot be emitted. Do not run the entire suite or service tests by default.
|
||||
|
||||
For C016 duplication changes, `contract_redundancy` must exercise actual production roots, identical private pure implementations and reusable pure calls, unchanged library warnings, and exclusions for effects, input/field/result mutation, owners and output/recovery contracts. CLI `llvm_cli` must reject both proven cases identically through check/run/build before missing runtime packs, database access or output creation; show each hard error and its evidence once, keep distinct-proof/unrelated warnings, and preserve baseline/API/Test behavior. Statically check maintained CMS plain/Markdown and skill examples with application errors; no native execution or database is required for this gate.
|
||||
|
||||
For this source/API contract select `source_architecture`, `source_visibility`, `contract_execution`, `markdown_source`, `api_declarations`, `rest_model`, `native_entry`, `api_routes`, `auth_runtime`, `authorization_runtime`, `secure_contracts`, `tenant_database`, `durable_jobs` and `upload_boundary` as needed. Assert App aliases, topic hiding, role call direction, Model ownership, layout rejection, generated roots, strict inputs, JSON envelopes, Markdown parity, site/auth ownership, exact permission keys, duplicate-key rejection, public-to-tenant rejection, REST row scope and Upload cleanup. Authentication coverage includes public Cookie isolation, explicit invalid Authorization, exact Origin, staged metadata ordering, generated System roots, site-scoped multi-role union, Owner isolation and User Job current-role checks. Actual loopback HTTP verification requires an explicitly scoped service check.
|
||||
|
||||
Application-test changes select `application_testing`, `formatter` and `cms_project`. Assert stable discovery, plain/Markdown duplicates, filename entry validation, both visibility directions, assertion typing/rendered source locations, existing result-choice recovery, failure continuation/output capture, zero-test success, per-case SQLite Seed isolation, ignored deployment settings, production commands ignoring test sources and module/test formatting preflight.
|
||||
|
||||
@@ -42,6 +42,6 @@
|
||||
- `examples/README.md`: current CMS commands and the archive boundary. Historical rejection fixtures keep their original diagnostics only when invoked by dedicated compiler-level tests, not as current application projects.
|
||||
- `test/dever-tests/tests/`: permanent focused tests. Parser, checker and formatter share `test/dever-tests/fixtures/core/complete_package.dever`; public tests do not depend on private task records.
|
||||
|
||||
Core foundations, formatting, optional reference evaluation, official library follow-through and the SQLite/PostgreSQL static ORM have concrete implementations. Compiler distribution and self-hosting remain unshipped; validation evidence belongs to the active task, not this directory map. Compiler phase APIs are bootstrap internals, not an alternative program source format.
|
||||
Core foundations, formatting, optional reference evaluation, official library follow-through and the SQLite/PostgreSQL static ORM have concrete implementations. Linux x86_64 compiler distribution is shipped; other host distributions and self-hosting remain unfinished. Validation evidence belongs to task records, not this directory map. Compiler phase APIs are bootstrap internals, not an alternative program source format.
|
||||
|
||||
Use one owning implementation for each language rule. Do not introduce a generic parser framework, service layer or backend registry for a single concrete implementation.
|
||||
@@ -12,6 +12,8 @@ Explicit LLVM Worker acceptance consumes the prepared immutable `target/native-r
|
||||
|
||||
Explicit single-libtest acceptance uses `run_command(..., single_harness=True)` and requires the final parent summary to contain exactly one passed test, no failures and no ignored tests. Nested probe summaries must not be added to that parent count; ordinary Cargo multi-harness commands retain aggregate counting. A zero-test parent, failed parent or missing summary is not a pass even if a child probe succeeded. `test/ci/test_gate.py` owns these rejection cases.
|
||||
|
||||
CI source fingerprints include `skills/`: its instructions, embedded project templates and shipped examples are product inputs, so changed or deleted files invalidate prior evidence. The Python stage executes the existing `test/skill_examples.py` for relocated local links, release file budgets, standard-library signatures and tutorial-source bindings; do not duplicate its validators or treat zero-test/failed execution as success. Each `dever` fence in `skills/references/*.md` must immediately follow a `源码:[label](relative-path)` link and a blank line. Its nonempty bytes must uniquely match a continuous excerpt in a shipped template/example module or test source. Missing bindings, changed documentation/source, ambiguous excerpts and unclosed fences fail. Rust `skill_examples` checks the complete projects, production roots, configuration, case-local effects and formatting, including the template; its inventory must cover every example directory. Collections use one-output `each` for mapping, zero-output `each` for actions, and `filter/reduce`; there is no `map` operation. Focused CI wiring checks use `test/ci/test_gate.py` and the actual skill checker, retain its observed test count, and reject deliberate link/signature/tutorial drift through a nonzero exit and the specific failed/error case. Run the collection cases and ordinary-command fake case in owned temporary projects; do not invoke real third-party binaries, databases, services or the full gate.
|
||||
|
||||
The shared `test/native-runtime-abi/managed-driver.c` runs one warmup and 64 repeated entries on joined host threads, then checks strict allocation balance after each join/TLS destruction. Rust's bounded-channel producer may first allocate its 48-byte thread-local Context only after warmup when the queue fills; measuring before the host thread exits misreports that cache as a leak. Preserve process initialization before any thread, original result checks and strict zero allocation delta; do not add retries, random warmups or an allowed-leak threshold. This measures entry-plus-host-thread cleanup, not unbounded TLS growth on a long-lived embedding thread. `test/ci/test_driver.py` compiles the shared driver with the explicit existing author C tool and verifies late TLS cleanup succeeds while intentional per-entry leaks and changed results fail. Suite probes include the same driver instead of copying its ledger.
|
||||
|
||||
Markdown source uses pinned pulldown-cmark in the compiler only. Preserve the `.dever` path and existing semantic/contract checks; shared Markdown extraction/formatting rules and the focused `markdown_source` target are specified in [Toolchain And Official Library Contracts](toolchain-and-library.md).
|
||||
|
||||
@@ -32,7 +32,7 @@ Process main is separate from callable roots. Initialize Unix SIGPIPE policy bef
|
||||
|
||||
- First installation: root `install` is the sole installer source and supports `bash install` and `curl -fsSL https://git.dever.cc/shemic/dever/raw/branch/main/install | bash`. Its Bash entry forwards `--version`, `--release`, `--trusted-key`, `--system-root`, and `--no-start` to its embedded Python implementation. Maintain this file directly; skills provide guidance, not another installer, and there is no generation step.
|
||||
- Project creation: `dever new <new-directory> [--markdown]`; `project::create(&Path, bool)` consumes the single template source under the ordinary `skills/assets/template` directory in the same repository revision. The machine launcher selects the active core without reading nonexistent target configuration.
|
||||
- AI guidance: `dever skill path [project-root]` resolves and verifies the active or project-pinned release's `skills/dever-language`; `dever skill install <new-directory>` atomically publishes a stable loader that invokes the absolute machine launcher. Skill resolution is a public read-only operation and must not open the root-only install lock.
|
||||
- AI guidance: `dever skill path [project-root]` resolves and verifies the active or project-pinned release's `skills/dever-language`. `dever skill install` takes no arguments: from a project root, select its compiler version and merge that signed skill's `assets/template/AGENTS.md` into the project's `AGENTS.md`. Reuse the same template as `dever new`; do not copy a full skill or create a second binding command. Create a missing file, append a missing managed block, or replace one complete `<!-- dever-language:start -->` / `<!-- dever-language:end -->` block while preserving all other content. Repeated installs are idempotent. Reject a non-project directory, symbolic links, nonregular/invalid UTF-8 AGENTS, malformed/duplicate markers and missing signed templates before mutation; publish through an owned atomic staging file. The old skill-directory argument is removed without an alias. Skill resolution is a public read-only operation and must not open the root-only install lock; machine installation/update does not scan or rewrite user projects.
|
||||
|
||||
- Application target: `dever build <root> --output <new-file> [--target linux-x86_64|linux-aarch64]`; explicit dependency preparation accepts the same target on `dever lib add/install/update/doctor`. Omission selects `BuildTarget::host()`. `CompileRequest.target: BuildTarget` is required and serialized canonically; test requests must use the host.
|
||||
|
||||
@@ -64,6 +64,7 @@ Process main is separate from callable roots. Initialize Unix SIGPIPE policy bef
|
||||
- `format::format(&SourceFile) -> Result<String, Vec<Diagnostic>>`; `dever fmt <project-root> [--check]` prepares both `module/` and optional `test/` before committing either source set.
|
||||
- `SourceMap::load(root)` accepts `.dever` and `.dever.md`; plain `.md` is not program source. It derives package identity from the relative filename, and both source formats use the same `parse/check/api/run/build` contracts.
|
||||
- `SourceMap::load_project(module_root, test_root)` preserves separate display/logical paths and assigns Test origin internally. A missing test root is empty; an existing non-directory or unreadable root is an explicit load error.
|
||||
- `dever test <root> [--filter <component[.domain[.case]]>] [--timeout-ms <positive-ms>]` checks all sources and compiles one complete indexed suite. Filter matches complete dot-separated identity or a segment prefix, never a substring/glob. Preserve slash-separated report names and original indices. Default case deadline is 60000 ms, starting at process launch, not compilation; unselected cases count as not run and no match is a command error. Linux runner uses an owned process group, NOWAIT leader observation and temporary subreaper ownership: terminate the group before releasing the leader PID, then reap only that group and restore prior state. File output capture avoids pipe backpressure/descendant EOF hangs. Timeout fails that case and later selected cases continue; production config and real Lib remain outside ordinary Test graphs.
|
||||
- Static API declarations are generated into the async application entry; application source does not call `dever.api.serve()`. Runtime `api::Inputs` binds GET/DELETE query parameters, POST/PUT JSON objects or a statically selected POST multipart shape, and runtime response helpers build the fixed JSON envelope. `config/setting.json` alone supplies HTTP, site/auth and upload limits; application config never uses environment variables.
|
||||
- Optional feature `reference`: `reference::evaluate(&Program, entry) -> Result<Evaluation, String>`, with captured stdout and structured source-span fault. No interpreter production CLI path.
|
||||
- `parallel_each(handler, List|Bytes|Stream, workers[, context]) -> ()`; zero-output synchronous handler, 1–256 workers, no recursively nested Stream in worker input/context. In async code the handler must be a named function with no transitive concurrency effect.
|
||||
@@ -249,6 +250,7 @@ Create a response-body cancellation owner only after the pending-to-committed st
|
||||
- Signature/payload/skill corruption, incomplete archive or absent official asset: explicit failure, no active-version switch or fallback. Publishing source code is not evidence that a downloadable binary release exists.
|
||||
- Truncated Bash body: no installer or sudo execution. Missing prerequisites: fail before metadata download. Failed authentication through Bash: preserve existing active journal/core/bootstrap and clean only owned staging. Tests load the embedded Python from the actual root entry and exercise Bash file/pipe execution; retain a check against the independently provisioned public key. Successful same-version bootstrap may append an activation record; assert selected-version identity rather than a byte-identical journal.
|
||||
- Project has a pinned version: `skill path <project>` resolves that exact installed version. Foreign users must read matching guidance without obtaining the administrator install lock.
|
||||
- Test filter/timeout is missing, repeated, non-UTF-8 or malformed: reject before execution. No selected case: fail before compilation. Assertion, process error or timeout: retain labelled captured output, continue later selected cases and return nonzero. Cleanup only the case's process group; never reap unrelated children or leave adopted descendants to PID 1.
|
||||
|
||||
Cross build: unknown/duplicate/missing target arguments fail before execution; absent target packs, wrong ELF/archive machine, unsigned target leaves, incompatible lock/runtime/build receipt and changed target inputs fail before cache reuse or linking. Sandbox loader mode must survive both release packing and embedding; a hash-valid non-executable loader is not a usable Worker resource.
|
||||
|
||||
@@ -320,6 +322,8 @@ Cache miss -> compile and publish; changed inputs -> separate key; incomplete/mo
|
||||
|
||||
Onboarding base: `dever new app` followed by `run app -- hello.greeting.greet '{"name":"Dever"}'` returns the standard envelope with `data:"Hello, Dever"`. Good: --markdown generates equivalent checked declarations; two projects/users share core and skill. Installation good: standalone Bash installs a verified local release into an owned image, then pipe reinstall preserves it. Bad: manual main.dever scaffolding, Go service/Page JSON templates, update URLs in project setting.json, a Git login page served as install, or a global skill copied once and allowed to drift from the installed compiler.
|
||||
|
||||
Test base: no filter executes all cases serially; no test directory reports zero success. Good: `--filter user.account.register --timeout-ms 10000` selects the original case index while other source errors still fail checking. Bad: using substring matching, renumbering filtered cases, counting no match as success, or timing out only the leader and leaking descendants.
|
||||
|
||||
Cross build good: an x86 core uses ARM profiles, ARM Worker assets and x86 Go tools emitting ARM objects. Base: omitted target retains native behavior. Bad: replacing global host identity, executing an ARM build tool on x86, labeling x86 native extensions ARM, or deriving target loader permissions from the host.
|
||||
|
||||
Native author base: fixed inputs and temporary key produce a verifiable manifest. Good: two different output names have identical manifests/signatures and payload digests, source mutation does not affect either published copy, and optimized profile programs run through the managed compiler. Bad: hard-link output to build caches, accept a debug all-features archive as four trimmed profiles, or call an unused PostgreSQL configuration a real database test.
|
||||
@@ -344,7 +348,9 @@ Managed base: a zero-Lib Python/JavaScript Adapter still locks its signed runtim
|
||||
|
||||
## 6. Required Tests
|
||||
|
||||
For onboarding select CLI `new_project`, lib `release_source`, shared-toolchain version/skill tests and native-release skill authoring tests, plus `python3 -B -m unittest discover -s test/onboarding -v`. Assert both formats check/format, no-overwrite concurrency, real CMD/Test/build output, exact signed extraction, corrupt-download retention, project-pinned guidance and foreign-user reads. The foreign-user test lowers only an owned client UID. Bootstrap script tests mock final installation; report that separately from real native-template execution. Do not activate host services or run full release/CI suites as an onboarding check.
|
||||
For onboarding select CLI `new_project`, lib `release_source`, shared-toolchain version/skill tests and native-release skill authoring tests, plus `python3 -B -m unittest discover -s test/onboarding -v`. Assert both formats check/format, no-overwrite concurrency, real CMD/Test/build output, exact signed extraction, corrupt-download retention, project-pinned guidance and foreign-user reads. Project skill installation must cover actual no-argument launcher dispatch from an owned cwd, create/append/replace/idempotence, preservation of user instructions, pinned template selection and failures before writes; no global AI directory or service is needed. The foreign-user test lowers only an owned client UID. Bootstrap script tests mock final installation; report that separately from real native-template execution. Do not activate host services or run full release/CI suites as an onboarding check.
|
||||
|
||||
Test-control changes select CLI bin `test_arguments`, `case_process_captures_output_times_out_and_reaps_only_owned_descendants`, and `llvm_cli::test_filter_still_checks_unselected_sources_and_reports_no_match_before_compilation`. The opt-in `llvm_test_controls_preserve_indices_prefix_boundaries_and_timeout_continuation` consumes an explicitly prepared compiler-relative runtime pack. Assert actual non-first case execution, identity prefix boundaries, no-match failure, source errors outside the selection, timeout output/continuation, large output and owned child/grandchild reap with an unrelated child preserved. Never rebuild runtime archives recursively from tests.
|
||||
|
||||
For installer-only changes select `test_install*.py` and `bash -n install`; also run `toolchain::release_source` when its Rust download owner changes. Assert actual Bash file/pipe help, isolated Python, literal argument/exit-status preservation, non-root sudo dispatch, independent-key authentication, new origin paths and rejected redirects. `verify_install_entry.py` is an opt-in actual signed-payload check: source-free entry, image-only first install, repeat install and corrupt-signature preservation. It uses an existing authenticated release/extractor and no host services; old payloads do not establish new-binary or online update acceptance.
|
||||
|
||||
@@ -406,6 +412,8 @@ Command execution observes guard exec through a private CLOEXEC pipe. Bind the P
|
||||
|
||||
Wrong: update a skill symlink separately after changing the active core, treat a cached older version as a successful failed update, or delete a public output tree after a publication race. Correct: resolve core and signed skill from one active journal, return the original download failure, and publish exclusively owned staging with NOREPLACE.
|
||||
|
||||
Test wrong: filter the checked source set, use blocking Command.output without a deadline, or reap every child after killing a test. Correct: select stable case indices after full checking, capture in the case-owned directory, enforce the deadline and reap only its retained process group.
|
||||
|
||||
Installer wrong: recreate a separate skill installer or generator, take its key from the downloaded package, or assume changing Git remote changes an old launcher. Correct: maintain the sole root install source with its independent key, verify its actual Bash behavior, and migrate changed stable bootstrap binaries through a newly built signed release.
|
||||
|
||||
Cross-target wrong: check ELF headers but use the host loader name to assign executable mode. Correct: validate target architecture and independently preserve each declared executable's mode across signed packaging, embedding and extraction; exercise the resulting Worker bootstrap under the target kernel.
|
||||
|
||||
Generated
+6
-6
@@ -430,7 +430,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-backend-bridge"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dever-core",
|
||||
@@ -443,7 +443,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-cli"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"csv",
|
||||
@@ -471,7 +471,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-core"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"dever-runtime",
|
||||
"pulldown-cmark",
|
||||
@@ -481,7 +481,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-runtime"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64 0.22.1",
|
||||
@@ -514,7 +514,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-sandbox"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"command-fds",
|
||||
"goblin",
|
||||
@@ -527,7 +527,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-tests"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"dever-core",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ members = ["crates/dever-core", "crates/dever-runtime", "crates/dever-cli", "cra
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
publish = false
|
||||
|
||||
+6
-7
@@ -467,13 +467,13 @@ File 的 create_new/open/read/write/close 与共享别名、同步 Stream 的共
|
||||
|
||||
`llvm::emit_application` 已接通独立 CMD 与 HTTP/REST 应用入口。CMD 保持完整名称和单个 JSON 对象输入;混合 HTTP/CMD 项目执行 CMD 时不绑定监听器。`llvm/api.rs`、`api_context.rs`、`rest.rs` 与 `application_commands.rs` 生成静态路由、具体输入/输出、Claims/Identity 及权限描述符,复用已有 JWT/业务 verify、站点、RBAC、database-only 租户、Cookie/Header、Upload 与日志实现,不另建协议栈或向业务传入动态 Value。非 public 路由缺少认证或精确权限时,源码与私有 ABI 两处均拒绝;权限 key 仍不含 HTTP method。
|
||||
|
||||
完整应用和可达日志的 CMD 共用 invocation-owned `application::Session`,只读取一次 `config/setting.json`。数据库、迁移临时池、租户 manager 和 lifecycle 均由该 owner 持有;子任务先排空,再在同一 runtime 中关闭连接池、刷新日志。POST/PUT/DELETE 输入解码后开始事务,输出编码后提交,成功才发布响应 metadata。租户管理入口沿用 migrate/owner/component 合同,缺少 ready marker、租户身份或启用组件不能回退平台库。Upload 经 Port/Adapter 消费到存储,部分解码失败清理 affine owner。私有 CLI 和 Linux managed daemon 编译已接 LLVM;正式目标 packs 未交付。
|
||||
完整应用和可达日志的 CMD 共用 invocation-owned `application::Session`,只读取一次 `config/setting.json`。数据库、迁移临时池、租户 manager 和 lifecycle 均由该 owner 持有;子任务先排空,再在同一 runtime 中关闭连接池、刷新日志。POST/PUT/DELETE 输入解码后开始事务,输出编码后提交,成功才发布响应 metadata。租户管理入口沿用 migrate/owner/component 合同,缺少 ready marker、租户身份或启用组件不能回退平台库。Upload 经 Port/Adapter 消费到存储,部分解码失败清理 affine owner。私有 CLI 和 Linux managed daemon 编译已接 LLVM;签名发行包提供 Linux x86_64 编译资源,Linux ARM64 应用资源通过显式 target 扩展准备。
|
||||
|
||||
`llvm/external.rs` 接通 checked external Adapter;选中的 Setting 成功解码后才启动 Worker,具名输入/输出和声明业务错误复用 typed wire codec。缺失可空输出字段、额外字段、错类型和未声明错误均为运行故障。`llvm::emit_application_with_resources` 消费原打包器的 `EmbeddedResource`,与 Rust backend 共用摘要/去重,运行时复用安全提取和受管 launch;不查找宿主解释器。`runtime-external` 与编译器 SDK 分离。`component::Session` 由每次 task 根持有,传播到子任务、blocking 和 API;supervisor 挂在根 Scope,短命调用者退出不关闭共享 Worker。根退出先停止接收和关闭 Worker,再排空 Scope,typed 原始错误保留并附加清理原因。显式 shutdown 被取消时仍保留 Task owner,根可继续排空;普通 Test 仍只有 fake。
|
||||
|
||||
`llvm/jobs.rs` 接通 Job-only、API/worker/all、持久 payload、事务内入队、每次 User verify/精确权限/组件重验与租户迁移。`job::resources::Resources` 持有本次入口的 StorageBinding、Spec 和 Clock,复用已有 store/worker/cron/lease fencing;同步 handler 共用既有 bounded blocking,异步 handler 共用 typed coroutine。服务策略只在实际启动服务时校验,入队 CMD 和租户管理无需 HTTP/worker 配置。编译期的租户组件归属不等于部署启用了 tenant;两后端共用 `job::require_components`,仅在启用租户时要求身份并查询组件状态。Job 保持原有 HTTP 请求/身份 getter 禁用规则,不伪造请求上下文。
|
||||
|
||||
`llvm::emit_test_suite` 复用每个 checked `test_program` 的 fake、effect/failure 和可达闭包,在同一 IR 中按声明符号隔离各 case。稳定 index 选择私有文件名入口;fault 按同一 index 查询大小、渲染和释放,业务载荷保持具体类型。普通测试用独立进程和临时 SQLite;不读取部署配置、启动生产 Adapter/Lib 或服务,无数据库用例也不初始化无关 Model。断言保留源码位置,虚拟时钟和 drain 复用生产队列;根 Scope 排空后关闭 Session,再释放注册表、时钟和入口锁。测试 CLI 切换仍属于后续统一后端阶段。
|
||||
`llvm::emit_test_suite` 复用每个 checked `test_program` 的 fake、effect/failure 和可达闭包,在同一 IR 中按声明符号隔离各 case。稳定 index 选择私有文件名入口;fault 按同一 index 查询大小、渲染和释放,业务载荷保持具体类型。普通测试用独立进程和临时 SQLite;命令读取配置和锁中的依赖信息,但不连接部署数据库或启动生产 Adapter/Lib、服务,无数据库用例也不初始化无关 Model。断言保留源码位置,虚拟时钟和 drain 复用生产队列;根 Scope 排空后关闭 Session,再释放注册表、时钟和入口锁。测试 CLI 已使用同一 LLVM 后端;`test_runner` 按身份筛选执行索引,负责每例超时、输出捕获及自身进程组回收。
|
||||
|
||||
`llvm/database.rs`、`database_rows.rs`、`orm*.rs`、`database_schema.rs` 和 `transaction.rs` 接通 Model;API 应用通过 Session 同时支持平台库与租户物理库。SQL/DDL 与关联查询复用 `native/orm.rs` 的纯构造函数,不生成 Rust、不复制数据库驱动。持久化固定标量在 ABI 边界转换为既有 `orm::Value`,业务记录和错误仍保持具体类型。Row/Rows/RowStream/Related 是不透明所有者;ToOne 保留可空关联,ToMany 按 998 个父键分块并使用既有 per-parent 限额 SQL。启动按实际数据库分组先迁移全部 Model,再建立外键;数据迁移与 Seed 复用原历史校验、事务和顺序规则。
|
||||
|
||||
@@ -491,7 +491,7 @@ Linux `deverd` 已接可信编译 IPC。managed core 提交逻辑源码、无秘
|
||||
|
||||
原生发行包作者入口 `sdk/native-release.rs` 从独立作者根的 `config/setting.json` 读取显式输入及摘要,复用既有 runtime manifest、验证器和 Ed25519 发行协议。制作器复制并复核四 profile、CRT 和核心/LLVM 文件,在私有 staging 中签名,再以不覆盖方式原子发布;私钥不进入产物。`runtime-pack` Cargo profile 为 base/sqlite/postgres/both 分别生成保留链接符号的优化 archive,共享一个构建缓存。相同输入和密钥的重复打包一致,不等同于跨机器源码构建一致。
|
||||
|
||||
作者配置的可选 `runtimes` 已接 Python/Node/Go 完整运行/构建输入,制作器与 Worker 共用 manifest 和文件闭包校验。排序且固定元数据的 gzip/tar、registry 描述统一进入顶层发行签名;Go SDK、Adapter 和依赖的编译共用 `-trimpath`,避免随机临时目录改变 Worker 与共享缓存身份。LLVM 的二进制资源附件复用既有摘要去重,在 verify 前初始化常量,IR 仍限8 MiB;附件最多4096项、单项128 MiB/总256 MiB,共享编译请求384 MiB、产物320 MiB,不透明 artifact 仍64 MiB。Linux x86_64 三生态签名安装、受管 `check/run/build`、三条共享缓存及移除源码/机器安装后无系统语言环境的独立执行已通过,包含Python/Node源码构建原生依赖与离线收据重放。Linux签名首装、旧签名模板升级、篡改拒绝、双项目及真实双UID共享核心亦已通过自有完整验收。Linux root 沙箱、真实第三方高级依赖、双 CMS/PG 和 LLVM 容量报告已补齐;公开首次安装包、六平台产品代码/资产、受限 AppArmor 非 root 入口与完整质量门仍未完成。各阶段证据及限制见当前生产化任务。
|
||||
作者配置的可选 `runtimes` 已接 Python/Node/Go 完整运行/构建输入,制作器与 Worker 共用 manifest 和文件闭包校验。排序且固定元数据的 gzip/tar、registry 描述统一进入顶层发行签名;Go SDK、Adapter 和依赖的编译共用 `-trimpath`,避免随机临时目录改变 Worker 与共享缓存身份。LLVM 的二进制资源附件复用既有摘要去重,在 verify 前初始化常量,IR 仍限8 MiB;附件最多4096项、单项128 MiB/总256 MiB,共享编译请求384 MiB、产物320 MiB,不透明 artifact 仍64 MiB。Linux x86_64 三生态签名安装、受管 `check/run/build`、三条共享缓存及移除源码/机器安装后无系统语言环境的独立执行已通过,包含Python/Node源码构建原生依赖与离线收据重放。Linux签名首装、旧签名模板升级、篡改拒绝、双项目及真实双UID共享核心亦已通过自有完整验收。Linux root 沙箱、真实第三方高级依赖、双 CMS/PG 和 LLVM 容量报告已补齐;受限 AppArmor 非 root 入口和定向 CI 质量门也已完成。公开 Linux x86_64 v0.1.3 发行包的匿名下载、在线首装和更新已验收;Linux ARM64 应用交叉构建已支持,其他宿主平台的产品代码及完整发行资产仍待适配。长时间压力测试不能由这些定向结果替代。
|
||||
|
||||
database 租户模式把 verify 返回的内部 tenant ModelId 放入同一 task-local 调用域。`global type` 继续使用平台连接,其他 Model、自动 REST 与 Job 通过 `StorageBinding` 选择已显式迁移且 fingerprint 匹配的租户物理库;缺少上下文或 ready marker 直接失败。runtime tenant manager 对 `(connection, tenant_id)` 池设置总量与 idle 回收,生成的 `tenant migrate` 入口按稳定顺序迁移全部 tenant Model/Job 后才写本地和 control marker;PostgreSQL 先通过控制库幂等建租户库。User Job 持久化 Provider/站点/session/租户声明和已通过的能力名,Worker 重新调用静态 verify 并使用最新权限复核,不能把历史任务降级为 System。table/field 模式尚未实现,不能在固定表 SQL 上只补一个配置分支。
|
||||
|
||||
@@ -534,7 +534,7 @@ Dever 的默认工作流是本地离线:
|
||||
- `run` 在临时位置生成程序、执行并清理;
|
||||
- `build` 只写入明确指定且尚不存在的输出路径,避免误覆盖文件。
|
||||
|
||||
`run` 和 `build` 需要当前机器已经具备与编译器匹配的本地原生后端。正式发行工具应随发行包提供或在安装时明确检查它,不能在一次构建过程中临时联网补齐。当前尚未交付面向最终用户的完整发行工具链。
|
||||
`run` 和 `build` 使用与编译器匹配、经签名校验的本地原生资源,不能在一次构建过程中临时联网补齐。Linux x86_64 已提供面向最终用户的二进制发行工具链、首次安装入口、共享服务和版本化 skill;应用开发不需要系统 Rust、LLVM、Python、Node 或 Go。额外生态和目标资源由显式准备命令安装,普通 run/build 保持离线。
|
||||
|
||||
内置标准包 AST 在进程内缓存;每线程只保留上一份按有序路径和完整文本精确匹配的检查结果,含错误与警告。新的 CLI 进程仍完整检查源码和 API 基线,没有持久化 HIR 格式。
|
||||
|
||||
@@ -597,11 +597,10 @@ Dever 的默认工作流是本地离线:
|
||||
|
||||
## 19. 当前边界
|
||||
|
||||
本文描述的是当前语言和已经接通的编译架构,不代表所有生态能力都已完成。SQLite/PostgreSQL 类型安全 ORM 已使用编译器拥有的 `ModelSchema`/`QueryPlan`、具体生成代码和薄方言 executor 接入;Model、事务、迁移、Seed、关联和 typed native SQL 的合同见 [语言开发指南](LANGUAGE.md#13-modelorm-与数据库) 与 [Database Guidelines](.trellis/spec/backend/database-guidelines.md)。当前仍未交付的主要部分包括:
|
||||
本文描述的是当前语言和已经接通的编译架构,不代表所有生态能力都已完成。SQLite/PostgreSQL 类型安全 ORM 已使用编译器拥有的 `ModelSchema`/`QueryPlan`、具体生成代码和薄方言 executor 接入;Model、事务、迁移、Seed、关联和 typed native SQL 的合同见 [语言开发指南](LANGUAGE.md#13-modelorm-与数据库) 与 [Database Guidelines](.trellis/spec/backend/database-guidelines.md)。Package 的注册表解析与锁定、Linux 签名安装和更新流程已实现。当前仍未交付的主要部分包括:
|
||||
|
||||
- WebSocket-over-HTTP/2、子协议/扩展、CONNECT、推送及更完整的 HTTP 生态;
|
||||
- package 注册表和正式安装发行流程;
|
||||
- 跨平台发行验证;
|
||||
- ARM64 原生宿主编译器、macOS/Windows 产品适配与完整发行;Linux x86_64 到 ARM64 的应用交叉构建已实现;
|
||||
- 用 Dever 自身实现编译器。
|
||||
|
||||
这些能力应建立在现有语义和系统原语边界之上,不能通过在 CLI 或运行时中增加隐藏业务逻辑来绕开语言设计。
|
||||
|
||||
+47
-41
@@ -72,7 +72,7 @@ Linux 开发环境运行构建产物:
|
||||
```
|
||||
|
||||
- `new`:从随版本验证的模板原子创建新项目,不覆盖已有目录;`--markdown` 生成等价 Markdown 源码。
|
||||
- 机器版 `update`:从官方仓库的 Releases 显式下载并校验,统一更新活动核心与对应 skill;不修改业务配置。`skill path [project-root]` 返回经验证的配套指引,`skill install <new-directory>` 安装稳定的 AI 加载入口。项目可锁定 dever.version,普通项目命令不会触发更新。
|
||||
- 机器版 `update`:从官方仓库的 Releases 下载并校验,更新核心与配套 skill,不改业务项目文件。`skill path [project-root]` 返回对应版本的指引。在项目根目录运行 `skill install`,自动向 `AGENTS.md` 写入或更新 Dever 规则块,保留其他内容,无需指定技能目录。`new` 自动生成同一份规则。项目可用 `dever.version` 锁定版本;普通项目命令不会触发更新。
|
||||
- `check`:加载生产源码,完成语法、名称、类型、分句及应用入口可达性和冗余硬检查;不执行应用或准备运行资源。
|
||||
- `fmt --check`:只检查格式,不修改文件。
|
||||
- `fmt`:统一格式化项目 `module/` 和存在的 `test/`;需要修复格式时单独执行 `dever fmt my_app`。
|
||||
@@ -105,7 +105,7 @@ greet(name: Text) (response: Greeting) {
|
||||
- App 能力的输入、输出和向外传播的错误都属于公开契约,不能引用 Domain、Adapter 等私有类型。业务错误类型应声明在 App,Domain 可通过 `app.ErrorType.Variant` 使用它。
|
||||
- `domain`、`model`、`port`、`adapter`、`api` 和 `job` 都是领域私有实现。跨领域、跨组件只能调用目标 App;API 和 Job 入口不是普通源码调用目标。
|
||||
- App 可调用本领域 Domain、Port 及其他领域 App;API 只能调用同领域 App。Domain 只能调用同领域 Domain。Adapter 只能调用同文件 helper 与标准能力,不能反调 App、Domain、Model、Port 或其他 Adapter。Model 操作仍只能由所属领域 App 使用静态 DSL 执行。
|
||||
- 同一 role 的单文件与同名目录二选一。非 API 的 topic 目录保持一层;只有 `api/` 可递归。role 目录只有一个源码文件时必须合回 role 文件,避免无意义拆分。
|
||||
- App、Domain、Port、Adapter、Job 的单文件与同名目录二选一,目录至少包含两个内聚主题文件。Model、API 可以同时使用角色主文件和主题目录,不受最少文件数限制。非 API 的主题目录保持一层,只有 `api/` 可递归。
|
||||
- 一个文件可放一组内聚类型和函数,不按函数机械拆文件。禁止用 `common`、`shared`、`utils`、`helper`、`base`、`service` 作为业务源码桶。
|
||||
- `port` 只声明类型和无函数体的操作合同,必须写 `fails app.ErrorChoice`;错误 choice 至少包含一个显式 error 分支,也可引用公开标准错误 choice。输入必须是无约束的明确值类型,输出同样不加范围约束。当前 Port 不接受 handler 参数。App/Domain 的外部文件、网络 I/O 必须经过 Port;日志、时间、随机数等已有能力保持原约束。
|
||||
- 没有 `import`、`use`、通配导入或别名。
|
||||
@@ -315,7 +315,11 @@ register() () {
|
||||
|
||||
`assert` 只接受 Bool;`assert_eq` 要求两侧类型完全相同且可比较,不做隐式数值转换。失败报告测试身份、原始源码位置和实际/期望值。业务失败继续用 `result(call)` 与 choice 分句验证,不增加异常断言语法。
|
||||
|
||||
`dever test <project-root>` 在执行任何用例前检查全部生产和测试源码,并把所有测试入口编译成一个可缓存的原生测试程序;每个测试仍在独立原生进程与临时项目中运行。不使用 Model 的测试不读取数据库配置或初始化数据库;使用 Model 的测试走正式迁移、Seed、事务和关闭流程,但配置由运行器在临时目录生成且只使用 SQLite。测试命令不会读取项目 `config/setting.json` 或项目 `data/`,也不从环境变量接收数据库覆盖;成功和失败都会清理临时文件。首版不提供共享 setup、筛选、并行、重试或自动超时。
|
||||
`dever test <project-root>` 在执行任何用例前检查全部生产和测试源码,并把所有测试入口编译成一个可缓存的原生测试程序;每个测试仍在独立原生进程与临时项目中运行。不使用 Model 的测试不初始化数据库;使用 Model 的测试走正式迁移、Seed、事务和关闭流程,但数据库配置由运行器在临时目录生成且只使用 SQLite。CLI 仍读取 `config/setting.json` 与锁中的依赖元数据,测试执行不会连接其中的部署数据库或使用项目 `data/`,也不从环境变量接收数据库覆盖;成功和失败都会清理临时文件。
|
||||
|
||||
定向运行使用 `--filter <component[.domain[.topic]]>`,例如 `--filter user.account` 或 `--filter user.account.register`。它按完整身份或点分段前缀选择用例,不使用子串或通配符;无匹配时明确失败。筛选不跳过其他源码的检查,不改变原生 suite 的索引;未选择用例计入 `not run`。报告仍显示原有的 `component/domain/topic` 路径身份。
|
||||
|
||||
每例默认最多运行 60 秒,`--timeout-ms <正整数毫秒>` 可覆盖;限时不包含测试程序的编译。超时用例失败,运行器终止并回收本例拥有的进程组和后代,然后继续后续选中用例。任一用例失败或超时,命令返回非零。测试仍按顺序执行,不提供共享 setup、并行或重试。
|
||||
|
||||
## 3. 类型与值
|
||||
|
||||
@@ -840,7 +844,7 @@ dever.json.element(dever.json.value.Document, array_id: Int, index: Int) -> Int?
|
||||
|
||||
`Document` 用整数 ID 保存一棵 JSON 树:`root` 是根节点 ID;`nodes` 的 ID 从 0 连续递增;子节点 ID 小于父节点 ID。Object 将属性名映射到子节点 ID,Array 保存有序子节点 ID。`property` 和 `element` 返回节点 ID,再通过 `node` 取得 Value;缺失、类型不符或越界时返回 `null`。JSON 的 `null` 则是实际存在的 `Value.Null`,两者不同。
|
||||
|
||||
数字保留原始 JSON 文本,避免先丢失精度。解析失败位置是从 0 开始的 Unicode codepoint 偏移,包括输入末尾位置。重复键、非法转义、未配对代理项、多余输入和超过 256 层的嵌套会传播 `ParseResult.Failed`。手工构造 Document 时,`stringify` 会检查根节点、连续 ID、子节点引用和输出规模,失败时传播 `WriteResult.Failed`;两者都可用 `result` 显式捕获。完整的解析和写回分句见 [标准库示例](examples/old/dever/library/module/main.dever)。
|
||||
数字保留原始 JSON 文本,避免先丢失精度。解析失败位置是从 0 开始的 Unicode codepoint 偏移,包括输入末尾位置。重复键、非法转义、未配对代理项、多余输入和超过 256 层的嵌套会传播 `ParseResult.Failed`。手工构造 Document 时,`stringify` 会检查根节点、连续 ID、子节点引用和输出规模,失败时传播 `WriteResult.Failed`;两者都可用 `result` 显式捕获。当前函数和失败类型见 [JSON 标准包](library/dever/json.dever)及 [JSON 值类型](library/dever/json/value.dever)。
|
||||
|
||||
### HTTP
|
||||
|
||||
@@ -903,7 +907,7 @@ start(listener: dever.system.Listener) () {
|
||||
}
|
||||
```
|
||||
|
||||
先用 `dever.net.listen(...)` 创建 Listener,再调用 serve;编译器会自动等待网络结果。完整启动分句见 [HTTP 示例](examples/old/dever/http/module/main.dever)。serve 持续接收连接,可作为 `server = run(dever.http.serve(...))` 启动,再用 `stop(server)` 取消并等待子任务清理。调用者持有的 Listener 别名仍由调用者关闭。
|
||||
在 Adapter 中先用 `dever.net.listen(...)` 创建 Listener,再调用 serve;编译器会自动等待网络结果。serve 持续接收连接,可作为 `server = run(dever.http.serve(...))` 启动,再用 `stop(server)` 取消并等待子任务清理。调用者持有的 Listener 别名仍由调用者关闭。普通应用的 HTTP 入口用 [模块 API](#模块-api) 声明,无需自行编写服务启动函数。
|
||||
|
||||
- 服务端先预留任务容量,再接受连接;没有无界应用队列或每连接独占线程。每条 HTTP/1 连接按顺序执行 handler。
|
||||
- 握手超时或底层 I/O 错误关闭该连接;正文超时返回 408,handler 超时返回 504,正文超限返回 413。HTTP/1 随后关闭连接,HTTP/2 结束对应流。写入停滞超过期限关闭连接。
|
||||
@@ -961,7 +965,7 @@ WebSocket Limits 默认 `message_bytes = 1048576`、`idle_ms = 60000`、`write_m
|
||||
|
||||
应用需持续调用 receive/messages 才能读取协议帧;收到 Ping 时引擎先发送 Pong,再交给应用。库不建立额外的定时 Ping 任务,可通过 send(Ping(...)) 实现应用保活。close 校验合法状态码和最多 123 UTF-8 字节的原因,等待对端关闭握手;关闭会唤醒等待中的读写。当前不提供关闭帧元数据、子协议协商或压缩扩展。
|
||||
|
||||
完整示例见 [live_http](examples/old/dever/live_http/module/main.dever):`/stream` 分段输出,`/events` 推送三条 SSE 事件,`/ws` 回显文本和二进制消息。
|
||||
这些底层操作用于 Adapter,类型和函数定义见 [HTTP](library/dever/http.dever)、[SSE](library/dever/sse.dever)和 [WebSocket](library/dever/websocket.dever)标准包。
|
||||
|
||||
### HTTP 连接池与流式客户端
|
||||
|
||||
@@ -1026,7 +1030,7 @@ route(request: dever.http.Request, context: Context) (response: dever.http.Respo
|
||||
|
||||
http_serve_live、http_serve_tls 和 http_serve_live_tls 也接受这个末尾参数;live handler 的输入顺序为 request、reply、context,TLS 配置位于 context 之前。这里直接绑定静态 handler,不引入闭包或动态路由解释器。
|
||||
|
||||
优雅停机先调用 `dever.net.close_listener(listener)` 并处理结果,再等待 server Task。HTTP 停止接收新连接,HTTP/2 同时发送 GOAWAY,已开始的响应继续完成;WebSocket/SSE 会话仍由现有 handler 拥有。客户端收到 GOAWAY 后不再向该连接派发新请求,也不自动重放已发送请求。使用 `timeout(server, grace_ms, expired)` 给排空设置上限,expired 是零输入、零输出的静态 handler;到期先停止剩余任务并等待清理。需要立即停止时使用 `stop(server)`。完整可运行用法见 [backend 示例](examples/old/dever/backend/module/main.dever)。
|
||||
优雅停机先调用 `dever.net.close_listener(listener)` 并处理结果,再等待 server Task。HTTP 停止接收新连接,HTTP/2 同时发送 GOAWAY,已开始的响应继续完成;WebSocket/SSE 会话仍由现有 handler 拥有。客户端收到 GOAWAY 后不再向该连接派发新请求,也不自动重放已发送请求。使用 `timeout(server, grace_ms, expired)` 给排空设置上限,expired 是零输入、零输出的静态 handler;到期先停止剩余任务并等待清理。需要立即停止时使用 `stop(server)`。自动 API 服务的启动与停机由核心负责,配置见 [模块 API](#模块-api)。
|
||||
|
||||
### 模块 API
|
||||
|
||||
@@ -1390,61 +1394,63 @@ TCP 的 `connect`、`connect_timeout`、`listen`、`accept`、`read`、`write`
|
||||
|
||||
## 15. 完整可运行示例
|
||||
|
||||
[示例索引](examples/README.md) 列出可运行示例及编译拒绝反例,提供每个项目的入口、预期结果和运行边界。每个示例目录都是独立项目根,源码位于其 `module/`。
|
||||
完整业务示例是 [CMS](examples/README.md),同时维护普通源码和 Markdown 两套实现。它展示账号、站点、权限、租户隔离、文章编辑与发布、文件上传和定时任务。每套都是独立项目,源码在 `module/`,测试在 `test/`。
|
||||
|
||||
当前完整示例位于 [examples/old/dever/library/module/main.dever](examples/old/dever/library/module/main.dever)。它把以下能力放在同一个程序中:
|
||||
在仓库根目录检查两套实现:
|
||||
|
||||
- `package` 与公开零输入入口;
|
||||
- 命名输出;
|
||||
- `List<Int>` 和 `reduce`;
|
||||
- JSON 解析、选择型失败分句与重新编码;
|
||||
- Text、Int 和 Unicode。
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
dever check examples/old/dever/library
|
||||
dever fmt examples/old/dever/library --check
|
||||
dever run examples/old/dever/library
|
||||
```sh
|
||||
dever check examples/cms/dever
|
||||
dever check examples/cms/md
|
||||
dever fmt examples/cms/dever --check
|
||||
dever fmt examples/cms/md --check
|
||||
dever test examples/cms/dever --filter news.article.publish
|
||||
dever test examples/cms/md --filter news.article.publish
|
||||
```
|
||||
|
||||
输出:
|
||||
这些测试使用临时 SQLite,不连接部署数据库。实际运行 CMS 会使用部署配置和数据,先按示例说明完成密钥、账号、租户数据库和 Owner 配置。
|
||||
|
||||
```text
|
||||
json = {"message":"你好 Dever","total":6}
|
||||
```
|
||||
只学习某项能力时,用随 skill 提供的独立小项目:
|
||||
|
||||
其他针对性示例:
|
||||
| 场景 | 项目 |
|
||||
| --- | --- |
|
||||
| HTTP、自动 REST、身份核验 | [http-rest](skills/assets/examples/http-rest/) |
|
||||
| 持久 Job、事务入队、虚拟时钟测试 | [jobs](skills/assets/examples/jobs/) |
|
||||
| Python、JavaScript、Go 接入与 Port fake | [external-lib](skills/assets/examples/external-lib/) |
|
||||
|
||||
- [examples/old/dever/core/module/main.dever](examples/old/dever/core/module/main.dever):用户 type、选择型、可空值、数值范围分句、List、Map、值语义和数值规则。
|
||||
- [examples/old/dever/stream/module/main.dever](examples/old/dever/stream/module/main.dever):文件分块读取、Stream 和 `reduce`。
|
||||
- [examples/old/dever/contracts/module/main.dever](examples/old/dever/contracts/module/main.dever):受约束的端口、明确失败、主动恢复、纯函数和包依赖。
|
||||
- [examples/old/dever/http/module/main.dever](examples/old/dever/http/module/main.dever):Hyper HTTP/1 服务及静态 route。
|
||||
- [examples/old/dever/async/module/main.dever](examples/old/dever/async/module/main.dever):Task、Group、Channel、非阻塞定时、`parallel` 和 `blocking`。
|
||||
- [examples/old/dever/backend/module/main.dever](examples/old/dever/backend/module/main.dever):源码运行参数、共享 Context、HTTP 连接池和优雅停机;请求一次后自动退出。
|
||||
- [examples/cms/dever/module/news/article/api/admin/manage.dever](examples/cms/dever/module/news/article/api/admin/manage.dever):生成的 admin REST/业务入口、站点认证和租户文章事务。
|
||||
各项目的命令和验证范围见 [示例指引](skills/references/examples.md)。新建最小项目使用 `dever new`,不从历史代码猜语法。
|
||||
|
||||
## 16. 当前未实现
|
||||
`examples/old/` 只保存历史语言回归材料,不是当前应用模板,不能直接按应用执行 `check/run/build`。其中的 `main.dever`、`package` 和旧可见性写法不用于新项目。
|
||||
|
||||
以下能力不能写进当前 Dever 程序:
|
||||
## 16. 语言边界与平台支持
|
||||
|
||||
### 不提供的语法
|
||||
|
||||
Dever 用分句、类型化失败和集合操作表达控制流程。以下写法不属于当前语言,检查时会明确报错:
|
||||
|
||||
- `if`/`else`、`switch`、`match` 和条件表达式;条件永久使用 function 分句。
|
||||
- `for`、`while`、`break`、`continue` 和用户递归;重复使用集合/Stream 组合器。
|
||||
- class、继承、interface、method、annotation、macro、unsafe 和 FFI。
|
||||
- lambda、闭包、一般化一等 function、用户泛型 type/function。
|
||||
- exception、`throw` 和 `catch`;可恢复错误使用推导式默认传播、`result(call)` 和 `fail(...)`。
|
||||
|
||||
### 当前能力边界
|
||||
|
||||
Linux x86_64 已提供签名安装包、机器共享编译服务和配套 skill,也能生成 Linux ARM64 应用。API/CMD、Model、权限、数据库租户隔离、Job、应用测试和外部 Lib 都使用同一套语言合同;运行、构建不隐式下载依赖。
|
||||
|
||||
当前限制如下:
|
||||
|
||||
- 通用动态 Json 值、Set 和 BigInt;持久化 JSON 使用 Model 的 `Json` 字段,普通 JSON 处理使用 `dever.json.Document`。
|
||||
- CONNECT、非空 trailer、服务器推送、WebSocket-over-HTTP/2,以及 WebSocket 子协议、压缩扩展和关闭帧元数据。
|
||||
- 正式签名 runtime/build packs、共享 daemon 编译及跨平台发行验收。现有 LLVM 内核覆盖受管值、静态 handler、Result、文件/流、真实协程与并发组合、TCP/HTTP/TLS/SSE/WebSocket 及系统能力;独立应用入口已接通 CMD→App→Dever/external Port/Adapter、SQLite/PostgreSQL Model/事务,以及 HTTP/REST、可信身份、自动权限、database-only 租户与组件状态、Cookie/Header、Upload 存储和日志配置。持久 Job 和应用 Test 复用现有语义,Test 保持一次编译及每用例进程/fake/数据库/时钟隔离。external 复用统一 Worker 协议、类型化数据与声明业务错误,以及校验后提取的内嵌资源。Worker 与其他资源归属每次入口:退出先关闭 Worker,再排空子任务并关闭数据库等资源。私有 `dever run/build/test` 默认使用 LLVM 与显式 runtime pack,不读取后端环境变量或调用宿主 Rust/Cargo/cc;managed `dever-core` 仍等待可信 daemon 编译入口,不能把本机定向验收当作正式或全平台发行。
|
||||
- table/field 多租户隔离模式,以及按租户业务配置动态选择已编译 Adapter;当前只支持 database 隔离和进程级 Adapter 选择。
|
||||
- GPU capability、macOS/Windows 外部程序沙箱和完整宿主发行;当前也不提供 ARM64 宿主编译器安装包。
|
||||
- 编译器自举。
|
||||
|
||||
编译器会拒绝尚未支持的语法,不会把它们静默转换成默认行为。
|
||||
未支持的能力明确失败,不使用默认值或关闭隔离来继续运行。具体安装与沙箱条件见 [安装指引](skills/references/install.md)。
|
||||
|
||||
## 17. 开发建议
|
||||
|
||||
1. 先把领域状态建模为字段型和选择型 type。
|
||||
2. 用小型具名 function 组合业务逻辑;把每个分支写成穷尽、互斥的分句。
|
||||
2. 函数按完整业务职责命名,分支写成穷尽、互斥的分句。需要表达独立规则或复用时再抽取,不机械拆成碎片函数。
|
||||
3. 需要重复时选择 `each`、`filter`、`find`、`sum`、`reduce` 或 `reduce_until`;只有副作用可并行且顺序不重要时使用 `parallel_each`。
|
||||
4. 用可空值表达简单缺失;需要区分多种失败原因时定义选择型结果。
|
||||
5. 在系统边界保留 Bytes、资源和显式失败,不隐式吞掉 I/O、JSON 或 HTTP 错误。
|
||||
@@ -1501,7 +1507,7 @@ configured_or_default(result: domain.PortResult.Failed(message)) (value: domain.
|
||||
}
|
||||
```
|
||||
|
||||
完整的 domain 定义见 [contracts 示例](examples/old/dever/contracts/module/domain.dever)。`recover` 允许这个函数结束时消费失败义务,因此应该放在实际决定恢复策略的小函数上,不能为通过编译给全部函数加同一个声明。
|
||||
例中的 `domain.Port` 是包含 `value: Int >= 1 and <= 65535` 的 record;`domain.PortResult` 有 `Ready(port: Port)` 和 `error Failed(message: Text)` 两个分支。`recover` 允许这个函数结束时消费失败义务,应放在实际决定恢复策略的函数上,不能为通过编译给全部函数加同一个声明。
|
||||
|
||||
失败义务会跟随 record、choice 载荷和集合。未知集合的局部查找不能证明剩余元素已处理;可能覆盖旧值的 Map 更新也不能静默丢掉旧失败。可用完整遍历处理元素、继续返回原集合,或在真实恢复边界声明策略。空遍历不能消费没有传给 handler 的错误上下文,短路跳过的调用也不能算处理。分析是保守的,未实现任意程序的语义等价证明。
|
||||
|
||||
@@ -1514,9 +1520,9 @@ dever check my_app
|
||||
|
||||
首次生成需要新路径;已存在的文件不会覆盖。`check`、`run`、`build` 自动检查项目根已有的 `dever.api`,变化时打印增删项并失败。快照记录公开名称、类型、可见字段、构造边界、约束、效果和恢复策略。正常修改私有实现不扩大公开接口。更新基线需要单独生成新文件并审查差异,不会自动“修复”基线。基线必须由项目审查或权限保护;如果允许 AI 同时任意修改源码和基线,它就不是授权边界。未提供基线时不会限制接口变化。
|
||||
|
||||
`check`、`run`、`build` 共用应用硬检查 C016:拒绝从生产入口不可达的 App/Domain 函数,以及符合现有判定条件的私有空转发和 Model CRUD 包装,不能识别全部包装形态。检查先于 Worker 和运行库准备,`check` 保持静态、离线。`test` 按自己的测试入口检查,不套用生产可达性,测试调用不能使生产死代码通过 `check`。
|
||||
`check`、`run`、`build` 共用应用硬检查 C016:拒绝从生产入口不可达的 App/Domain 函数,以及符合现有判定条件的私有空转发和 Model CRUD 包装。对于生产入口可达的 App/Domain,还拒绝同一源码文件内契约和实现完全相同、没有副作用(包括内部调用)的私有函数,以及参数未变且前一次结果仍可复用的重复纯调用。有副作用、契约不同、参数或前一次结果已被修改的情况不按这两条重复规则拒绝。检查先于 Worker 和运行库准备,`check` 保持静态、离线。`test` 按自己的测试入口检查,不套用生产应用硬检查,测试调用不能使生产死代码通过 `check`。
|
||||
|
||||
编译器还会建议清理有证据的重复范围判断(W002)、相同私有实现(W003)、可复用的纯调用(W004)和没有增加契约的私有转发(W005)。未使用赋值沿用 W001,不叠加第二套规则。指定应用入口时,W007 提醒没有外部使用需求的公开函数;普通库检查不推测未知调用方。这些警告本身不改变执行、不作为硬错误;上述应用检查会另外拒绝其中符合 C016 判定条件的转发。检查不按行数或函数个数强迫抽象。官方源码执行同样的契约检查,CLI 默认只展示当前用户源码的建议;官方硬错误不会隐藏。
|
||||
编译器还会建议清理有证据的重复范围判断(W002)、相同私有实现(W003)、可复用的纯调用(W004)和没有增加契约的私有转发(W005)。未使用赋值沿用 W001,不叠加第二套规则。指定应用入口时,W007 提醒没有外部使用需求的公开函数;普通库检查不推测未知调用方。这些警告本身不改变执行、不作为硬错误;上述应用检查会另外拒绝符合 C016 判定条件的重复和转发,普通库和测试保留警告。CLI 已用相同主位置和关联源码证据报 C016 时,不再重复显示对应 W003/W004/W005;其他警告保留。检查不按行数或函数个数强迫抽象。官方源码执行同样的契约检查,CLI 默认只展示当前用户源码的建议;官方硬错误不会隐藏。
|
||||
|
||||
契约分析有明确的工作上限(当前每轮相应证明最多 16,384 个工作单元,名义类型嵌套最多 128 层)。超大嵌套失败结构或事实证明超过上限时报 C015,不会跳过检查后放行。共享结构使用缓存与写时复制,普通重复引用不会直接展开成指数数量的副本。
|
||||
|
||||
|
||||
@@ -1,39 +1,57 @@
|
||||
# Dever
|
||||
|
||||
Dever 是面向应用开发的编译型语言,提供类型检查、命名输出、穷尽分句、值与资源语义、结构化并发,以及 JSON、HTTP/1.1、HTTP/2、SSE、WebSocket、TLS 和 SQLite/PostgreSQL 类型安全 ORM。源码支持 `.dever` 和带说明的 `.dever.md`,两者使用相同语言规则。
|
||||
Dever 是用于开发应用的编译型语言,自带 API、数据库操作、权限、租户和后台任务。源码可以写成 `.dever`,也可以把代码和说明放在 `.dever.md` 中。
|
||||
|
||||
## 安装与开始
|
||||
|
||||
当前开发机支持 Linux x86_64,要求 glibc 2.39 或更新版本。首次安装需要 Bash、curl、`/usr/bin/python3` 3.9+、支持 Ed25519 的 `/usr/bin/openssl` 和管理员权限;非 root 用户通过 `/usr/bin/sudo` 提权。
|
||||
目前支持 Linux x86_64 开发机,要求 glibc 2.39+。首次安装需要 Bash、curl、Python 3.9+、支持 Ed25519 的 OpenSSL 和管理员权限。详细条件见 [安装指引](skills/references/install.md)。
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.dever.cc/shemic/dever/raw/branch/main/install | bash
|
||||
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"}'
|
||||
```
|
||||
|
||||
发行包可从 [Releases](https://git.dever.cc/shemic/dever/releases) 下载。安装使用 `/opt/dever` 和 `/usr/local/bin/dever`,并安装共享服务;详情见 [安装与更新](skills/references/install.md)。应用开发无需克隆编译器源码或安装 Rust、LLVM、Python、Node、Go 开发环境。
|
||||
这会创建一个问候程序,再把它编译成独立可执行文件。开发应用无需克隆编译器源码或安装 Rust、LLVM、Node、Go。安装包也可从 [Releases](https://git.dever.cc/shemic/dever/releases) 下载。
|
||||
|
||||
`dever new my_app --markdown` 创建等价的 Markdown 项目。项目目录必须是新目录;`build` 输出必须是新文件,父目录必须存在。生成程序从可执行文件同级的 `config/setting.json` 读取配置,部署时保留这个目录结构。独立程序无需 Dever 编译器或应用源码;使用外部 Lib 时,所需运行资源随程序嵌入。
|
||||
用 `dever new my_app --markdown` 创建 Markdown 项目。新项目目录、构建输出文件都不能已存在,父目录必须存在。部署时把 `config/setting.json` 放在程序同级;目标机器无需安装 Dever。
|
||||
|
||||
## 让 AI 开发
|
||||
|
||||
新项目已生成 `AGENTS.md`。已有项目在根目录执行:
|
||||
|
||||
```sh
|
||||
dever skill install
|
||||
```
|
||||
|
||||
命令自动使用项目对应版本的 skill,把开发规则写入 `AGENTS.md`;保留其他内容,重复执行只更新 Dever 规则块。不需要指定 AI 技能目录或复制文档。
|
||||
|
||||
规则要求 AI 先读开发指引,禁止擅自加旧版本兼容、重复实现、空包装和业务硬编码。AGENTS 负责指导 AI,编译器检查能判定的违规;不能保证 AI 永远不会写错代码。
|
||||
|
||||
## 检查与更新
|
||||
|
||||
```sh
|
||||
dever fmt my_app --check
|
||||
dever check my_app
|
||||
dever test my_app --filter hello.greeting
|
||||
```
|
||||
|
||||
`fmt` 查格式,`check` 查源码与配置,`test` 执行所选测试。测试默认每例最多 60 秒,可加 `--timeout-ms 5000` 调整。`check` 不启动程序,普通测试不连接部署数据库;验证范围见 [开发指引](skills/references/development.md#验证范围)。
|
||||
|
||||
```sh
|
||||
sudo dever update
|
||||
dever version
|
||||
dever skill install <新的AI技能目录>/dever-language
|
||||
```
|
||||
|
||||
核心与配套 AI skill 一起更新。已有项目可在 `config/setting.json` 的 `dever.version` 锁定准确版本,并用 `dever skill path <project-root>` 查找对应指引。普通 `check/test/run/build` 不检查更新、不隐式联网。
|
||||
`update` 一起更新 Dever 与配套 skill。项目可在 `config/setting.json` 的 `dever.version` 锁定版本;`dever skill path my_app` 查看它使用的指引。普通 `check/test/run/build` 不检查更新、不隐式联网。
|
||||
|
||||
## 应用开发
|
||||
|
||||
应用按 `module/<component>/<domain>/<role>` 组织;role 为 App、Domain、Model、Port、Adapter、API 或 Job。API 声明入口,App 编排业务,Domain 表达内聚规则,Model 定义持久化合同,Port/Adapter 接入外部能力。跨领域只调用目标 App,其他角色保持领域私有。
|
||||
代码放在 `module/<组件>/<领域>/`。API 声明入口,App 编排业务,Domain 写业务规则,Model 定义数据,Port/Adapter 接入外部能力,Job 声明后台任务。只创建需要的文件,跨领域调用 App。
|
||||
|
||||
源码不写 `package`、`import` 或 `main`,入口由 API、CMD 和 Job 声明生成。HTTP 默认要求认证和权限,仅单条 `public` HTTP 声明允许匿名访问。配置集中在 `config/setting.json`,不从环境变量读取。应用测试放在 `test/<component>/<domain>/<topic>`。
|
||||
不需要 `package`、`import` 或 `main`。HTTP 默认有权限检查,匿名接口显式写 `public`。配置放 `config/setting.json`,测试放 `test/<组件>/<领域>/`。
|
||||
|
||||
- [语言开发指南](LANGUAGE.md):语法、标准包、并发、Model、权限、租户、Job 和外部调用。
|
||||
- [应用开发规范](skills/references/development.md):目录、函数职责、复用、配置、测试和交付。
|
||||
@@ -41,12 +59,11 @@ dever skill install <新的AI技能目录>/dever-language
|
||||
- [Markdown 源码规范](MARKDOWN-SYNTAX.md):说明与代码的一致性要求。
|
||||
- [CMS 示例](examples/README.md):同步维护的普通源码和 Markdown 实现。
|
||||
- [External Worker SDK](sdk/README.md):Python、JavaScript 和 Go Adapter 开发。
|
||||
|
||||
`dever fmt <root> --check` 检查格式;`dever check <root>` 静态检查语言、目录、配置、API 基线及生产入口可达性,不执行应用。`dever test <root>` 执行项目全部应用测试,没有筛选参数;按改动选择验证,不默认运行大型项目的完整套件、真实数据库或性能测试。
|
||||
- [VS Code 插件](editor/vscode/README.md):安装语法高亮插件。
|
||||
|
||||
## 依赖与应用目标
|
||||
|
||||
Dever Package 复用业务组件;Python、JavaScript、Go 生态依赖称为 Lib。通过显式 `dever package` 或 `dever lib` 命令准备依赖并写入 `dever.lock`,后续运行和构建使用锁定结果。已有锁的项目在新机器用 `dever lib install <root>` 恢复,不重新选择版本。相同版本的工具和资源由多个用户、项目共享。
|
||||
Dever 组件叫 Package,Python、JavaScript、Go 依赖叫 Lib。用 `dever package` 或 `dever lib` 准备依赖,后续运行和构建使用 `dever.lock`。具体用法见 [依赖指引](skills/references/libraries.md)。
|
||||
|
||||
Linux x86_64 开发机可以生成 Linux ARM64 应用:
|
||||
|
||||
@@ -56,13 +73,7 @@ dever lib update my_app --target linux-aarch64
|
||||
dever build my_app --target linux-aarch64 --output my_app/program-arm64
|
||||
```
|
||||
|
||||
没有外部依赖时省略 `lib update`;已有匹配目标的锁时改用 `lib install`。依赖及原生扩展必须支持目标架构。省略 `--target` 时使用本机目标,`run/test` 也在本机执行。ARM64 应用目标不代表提供 ARM64 宿主编译器;macOS、Windows 宿主安装暂未提供。
|
||||
|
||||
Linux 外部调用使用 OS 沙箱,需要系统允许所需 namespace 和 `openat2`;受限 AppArmor 环境还需要安装器配置的专用策略。系统策略不满足要求时明确失败,不退回无沙箱执行。HTTP/2 暂不支持 WebSocket-over-HTTP/2、CONNECT 或服务端推送。完整能力及限制见语言指南。
|
||||
|
||||
## 编辑器
|
||||
|
||||
[VS Code 语法插件](editor/vscode/README.md) 通过独立 VSIX 安装,支持 `.dever` 和 Markdown 中的 Dever 代码块、括号配对、自动闭合、缩进及 `#` 注释。无需先安装编译器;目前不提供补全、跳转、格式化或实时诊断。`dever update` 不负责更新编辑器插件。
|
||||
没有外部依赖时省略 `lib update`;已有目标锁时改用 `lib install`。依赖必须支持 ARM64。当前不提供 ARM64、macOS 或 Windows 开发机安装包;平台和沙箱条件见 [安装指引](skills/references/install.md)。
|
||||
|
||||
## 编译器源码开发
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ mod worker;
|
||||
#[path = "../../../test/dever-cli-tests/build_arguments.rs"]
|
||||
mod build_arguments;
|
||||
|
||||
const USAGE: &str = "Usage:\n dever check <project-root>\n dever api <project-root> [--output <new-file>]\n dever fmt <project-root> [--check]\n dever test <project-root>\n dever run <project-root> [-- <component>.<domain>.<cmd> '<json-object>']\n dever run <project-root> --tenant <positive-tenant-id> -- <component>.<domain>.<cmd> '<json-object>'\n dever build <project-root> --output <new-file> [--target linux-x86_64|linux-aarch64]\n dever clean <project-root>\n dever tenant migrate <project-root> <positive-tenant-id>\n dever tenant owner <project-root> <positive-tenant-id> <site> <positive-user-id>\n dever tenant component enable <project-root> <positive-tenant-id> <component>\n dever tenant component disable <project-root> <positive-tenant-id> <component>\n dever lib add|install|list|update|remove|doctor <project-root> [--target linux-x86_64|linux-aarch64] [spec ...]\n dever package add|list|update|remove|doctor <project-root> [spec ...]\n\nApplications use API, Job, and CMD declarations under module/ and config/setting.json. Native builds and tests are offline.";
|
||||
#[cfg(test)]
|
||||
#[path = "../../../test/dever-cli-tests/test_arguments.rs"]
|
||||
mod test_arguments;
|
||||
|
||||
const USAGE: &str = "Usage:\n dever check <project-root>\n dever api <project-root> [--output <new-file>]\n dever fmt <project-root> [--check]\n dever test <project-root> [--filter <component[.domain[.case]]>] [--timeout-ms <positive-ms>]\n dever run <project-root> [-- <component>.<domain>.<cmd> '<json-object>']\n dever run <project-root> --tenant <positive-tenant-id> -- <component>.<domain>.<cmd> '<json-object>'\n dever build <project-root> --output <new-file> [--target linux-x86_64|linux-aarch64]\n dever clean <project-root>\n dever tenant migrate <project-root> <positive-tenant-id>\n dever tenant owner <project-root> <positive-tenant-id> <site> <positive-user-id>\n dever tenant component enable <project-root> <positive-tenant-id> <component>\n dever tenant component disable <project-root> <positive-tenant-id> <component>\n dever lib add|install|list|update|remove|doctor <project-root> [--target linux-x86_64|linux-aarch64] [spec ...]\n dever package add|list|update|remove|doctor <project-root> [spec ...]\n\nApplications use API, Job, and CMD declarations under module/ and config/setting.json. Native builds and tests are offline.";
|
||||
|
||||
enum Action {
|
||||
Check,
|
||||
@@ -28,7 +32,7 @@ enum Action {
|
||||
Format {
|
||||
check: bool,
|
||||
},
|
||||
Test,
|
||||
Test(test_runner::Options),
|
||||
Run {
|
||||
tenant_id: Option<i64>,
|
||||
arguments: Vec<OsString>,
|
||||
@@ -175,7 +179,7 @@ fn execute() -> Result<ExitCode, String> {
|
||||
};
|
||||
let sources = SourceMap::load_with_packages(
|
||||
&source_root,
|
||||
matches!(action, Action::Test | Action::Format { .. })
|
||||
matches!(action, Action::Test(_) | Action::Format { .. })
|
||||
.then(|| root.join("test"))
|
||||
.as_deref(),
|
||||
&packages,
|
||||
@@ -220,28 +224,52 @@ fn execute() -> Result<ExitCode, String> {
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
})?;
|
||||
// 基线先于生产检查;延后传播失败,让未被硬错误覆盖的告警仍能输出。
|
||||
let application_errors = if matches!(action, Action::Api { .. }) {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
api::check_baseline(&program, &root).and_then(|()| {
|
||||
if matches!(action, Action::Check) || action.compiles_application() {
|
||||
program.application_errors()
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
})
|
||||
};
|
||||
for warning in program.warnings() {
|
||||
if SourceMap::is_standard(warning.primary.source) {
|
||||
let reported_as_error = matches!(warning.code, "W003" | "W004" | "W005")
|
||||
&& !warning.related.is_empty()
|
||||
&& application_errors
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.any(|error| {
|
||||
error.code == "C016"
|
||||
&& error.primary == warning.primary
|
||||
&& error
|
||||
.related
|
||||
.iter()
|
||||
.map(|label| label.span)
|
||||
.eq(warning.related.iter().map(|label| label.span))
|
||||
});
|
||||
if SourceMap::is_standard(warning.primary.source) || reported_as_error {
|
||||
continue;
|
||||
}
|
||||
write!(io::stderr().lock(), "{}", warning.render(&sources))
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
let errors = application_errors?;
|
||||
if let Action::Api { output } = action {
|
||||
return api::write(&program, output.as_deref());
|
||||
}
|
||||
api::check_baseline(&program, &root)?;
|
||||
if matches!(action, Action::Check) || action.compiles_application() {
|
||||
let errors = program.application_errors()?;
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.iter().map(|error| error.render(&sources)).collect());
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(errors.iter().map(|error| error.render(&sources)).collect());
|
||||
}
|
||||
if matches!(action, Action::Check) {
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if matches!(action, Action::Test) {
|
||||
return test_runner::execute(&program, &sources);
|
||||
if let Action::Test(options) = &action {
|
||||
return test_runner::execute(&program, &sources, options);
|
||||
}
|
||||
let target = match &action {
|
||||
Action::Build { target, .. } => *target,
|
||||
@@ -258,7 +286,7 @@ fn execute() -> Result<ExitCode, String> {
|
||||
Action::Package { .. } => unreachable!("package returned before source checking"),
|
||||
Action::Api { .. } => unreachable!("api returned before baseline checking"),
|
||||
Action::Format { .. } => unreachable!("fmt returned before checking"),
|
||||
Action::Test => unreachable!("test returned before application compilation"),
|
||||
Action::Test(_) => unreachable!("test returned before application compilation"),
|
||||
Action::Run { .. }
|
||||
| Action::Build { .. }
|
||||
| Action::TenantMigrate { .. }
|
||||
@@ -346,7 +374,7 @@ fn execute() -> Result<ExitCode, String> {
|
||||
Action::Check
|
||||
| Action::Api { .. }
|
||||
| Action::Format { .. }
|
||||
| Action::Test
|
||||
| Action::Test(_)
|
||||
| Action::Lib { .. }
|
||||
| Action::Clean => {
|
||||
unreachable!("check and fmt returned before native compilation")
|
||||
@@ -506,7 +534,43 @@ fn arguments_for(arguments: &[OsString]) -> Result<(PathBuf, Action), String> {
|
||||
},
|
||||
))
|
||||
}
|
||||
[command, root] if command == "test" => Ok((root.into(), Action::Test)),
|
||||
[command, root, rest @ ..] if command == "test" => {
|
||||
let mut filter = None;
|
||||
let mut timeout = None;
|
||||
let mut options = rest.iter();
|
||||
while let Some(option) = options.next() {
|
||||
match option.to_str() {
|
||||
Some("--filter") if filter.is_none() => {
|
||||
let value = option_value(&mut options, "--filter")?
|
||||
.to_str()
|
||||
.ok_or("test filter must be UTF-8")?;
|
||||
test_runner::validate_filter(value)?;
|
||||
filter = Some(value.to_owned());
|
||||
}
|
||||
Some("--timeout-ms") if timeout.is_none() => {
|
||||
let value = option_value(&mut options, "--timeout-ms")?
|
||||
.to_str()
|
||||
.ok_or("test timeout must be a positive integer in milliseconds")?;
|
||||
let millis = value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit())
|
||||
.then(|| value.parse::<u64>().ok())
|
||||
.flatten()
|
||||
.filter(|millis| *millis > 0)
|
||||
.ok_or("test timeout must be a positive integer in milliseconds")?;
|
||||
timeout = Some(std::time::Duration::from_millis(millis));
|
||||
}
|
||||
_ => return Err("test accepts --filter and --timeout-ms exactly once; unknown or duplicate option".into()),
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
root.into(),
|
||||
Action::Test(test_runner::Options {
|
||||
filter,
|
||||
timeout: timeout.unwrap_or(test_runner::DEFAULT_TIMEOUT),
|
||||
}),
|
||||
))
|
||||
}
|
||||
[command, root] if command == "clean" => Ok((root.into(), Action::Clean)),
|
||||
[command, root, rest @ ..] if command == "build" => {
|
||||
let mut output = None;
|
||||
|
||||
@@ -3,21 +3,85 @@ use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, ExitCode};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use dever_core::hir::{Program, TestCase};
|
||||
use dever_core::source::SourceMap;
|
||||
|
||||
pub(super) fn execute(program: &Program, sources: &SourceMap) -> Result<ExitCode, String> {
|
||||
mod process;
|
||||
|
||||
#[cfg(all(test, target_os = "linux"))]
|
||||
#[path = "../../../test/dever-cli-tests/test_process.rs"]
|
||||
mod tests;
|
||||
|
||||
pub(super) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
pub(super) struct Options {
|
||||
pub filter: Option<String>,
|
||||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
pub(super) fn validate_filter(filter: &str) -> Result<(), String> {
|
||||
let segments: Vec<_> = filter.split('.').collect();
|
||||
if segments.len() <= 3
|
||||
&& segments.iter().all(|segment| {
|
||||
segment.starts_with(|character: char| character.is_ascii_lowercase())
|
||||
&& segment.split('_').all(|word| {
|
||||
!word.is_empty()
|
||||
&& word
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
|
||||
})
|
||||
})
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err("test filter must be component[.domain[.case]] using lower_snake_case names".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_cases<'a>(
|
||||
tests: &'a [TestCase],
|
||||
filter: Option<&str>,
|
||||
) -> Result<Vec<(usize, &'a TestCase)>, String> {
|
||||
let selected: Vec<_> = tests
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, test)| {
|
||||
filter.is_none_or(|filter| {
|
||||
let identity = test.name().replace('/', ".");
|
||||
identity == filter
|
||||
|| identity
|
||||
.strip_prefix(filter)
|
||||
.is_some_and(|rest| rest.starts_with('.'))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if selected.is_empty()
|
||||
&& let Some(filter) = filter
|
||||
{
|
||||
return Err(format!("no tests match filter '{filter}'"));
|
||||
}
|
||||
Ok(selected)
|
||||
}
|
||||
|
||||
pub(super) fn execute(
|
||||
program: &Program,
|
||||
sources: &SourceMap,
|
||||
options: &Options,
|
||||
) -> Result<ExitCode, String> {
|
||||
let selected = selected_cases(program.tests(), options.filter.as_deref())?;
|
||||
let not_run = program.tests().len() - selected.len();
|
||||
let mut stdout = io::stdout().lock();
|
||||
writeln!(stdout, "running {} tests", program.tests().len()).map_err(io_error)?;
|
||||
writeln!(stdout, "running {} tests", selected.len()).map_err(io_error)?;
|
||||
|
||||
let mut passed = 0;
|
||||
let mut failures = Vec::new();
|
||||
if !program.tests().is_empty() {
|
||||
if !selected.is_empty() {
|
||||
match crate::compile::test_suite(program, sources) {
|
||||
Ok(native) => {
|
||||
for (index, test) in program.tests().iter().enumerate() {
|
||||
match run_case(program, test, &native, index) {
|
||||
for (index, test) in &selected {
|
||||
match run_case(program, test, &native, *index, options.timeout) {
|
||||
Ok(()) => {
|
||||
passed += 1;
|
||||
writeln!(stdout, "test {} ... ok", test.name()).map_err(io_error)?;
|
||||
@@ -31,7 +95,7 @@ pub(super) fn execute(program: &Program, sources: &SourceMap) -> Result<ExitCode
|
||||
}
|
||||
}
|
||||
Err(message) => {
|
||||
for test in program.tests() {
|
||||
for (_, test) in &selected {
|
||||
writeln!(stdout, "test {} ... FAILED", test.name()).map_err(io_error)?;
|
||||
failures.push((test.name(), CaseFailure::message(message.clone())));
|
||||
}
|
||||
@@ -50,7 +114,7 @@ pub(super) fn execute(program: &Program, sources: &SourceMap) -> Result<ExitCode
|
||||
let status = if failed == 0 { "ok" } else { "FAILED" };
|
||||
writeln!(
|
||||
stdout,
|
||||
"\ntest result: {status}. {passed} passed; {failed} failed; 0 not run"
|
||||
"\ntest result: {status}. {passed} passed; {failed} failed; {not_run} not run"
|
||||
)
|
||||
.map_err(io_error)?;
|
||||
Ok(if failed == 0 {
|
||||
@@ -65,6 +129,7 @@ fn run_case(
|
||||
test: &TestCase,
|
||||
native: &dever_core::native::NativeProgram,
|
||||
index: usize,
|
||||
timeout: Duration,
|
||||
) -> Result<(), CaseFailure> {
|
||||
let project = TemporaryProject::new().map_err(CaseFailure::message)?;
|
||||
if test.uses_database() {
|
||||
@@ -90,20 +155,14 @@ fn run_case(
|
||||
executable.display()
|
||||
))
|
||||
})?;
|
||||
let output = Command::new(&executable)
|
||||
.arg(index.to_string())
|
||||
.current_dir(project.path())
|
||||
.output()
|
||||
.map_err(|error| CaseFailure::message(format!("cannot run test process: {error}")))?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CaseFailure {
|
||||
message: format!("test process exited with {}", output.status),
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
})
|
||||
}
|
||||
process::run(
|
||||
Command::new(&executable)
|
||||
.arg(index.to_string())
|
||||
.env_clear()
|
||||
.current_dir(project.path()),
|
||||
project.path(),
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_settings(root: &Path, test: &TestCase) -> Result<(), String> {
|
||||
@@ -145,6 +204,7 @@ fn io_error(error: io::Error) -> String {
|
||||
error.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CaseFailure {
|
||||
message: String,
|
||||
stdout: Vec<u8>,
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::CaseFailure;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) use linux::run;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(super) fn run(
|
||||
_command: &mut Command,
|
||||
_directory: &Path,
|
||||
_timeout: Duration,
|
||||
) -> Result<(), CaseFailure> {
|
||||
Err(CaseFailure::message(
|
||||
"application test processes require a supported Linux host".into(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux {
|
||||
use super::*;
|
||||
use std::fs::{self, File};
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::{Child, ExitStatus, Stdio};
|
||||
use std::time::Instant;
|
||||
|
||||
use rustix::io::Errno;
|
||||
use rustix::process::{self, Pid, Signal, WaitId, WaitIdOptions, WaitOptions};
|
||||
|
||||
pub(in crate::test_runner) fn run(
|
||||
command: &mut Command,
|
||||
directory: &Path,
|
||||
timeout: Duration,
|
||||
) -> Result<(), CaseFailure> {
|
||||
let stdout = directory.join("stdout");
|
||||
let stderr = directory.join("stderr");
|
||||
// 文件捕获不依赖 EOF;后代持有输出描述符也不会阻塞测试结束。
|
||||
let capture = |path: &Path| {
|
||||
File::create_new(path).map(Stdio::from).map_err(|error| {
|
||||
CaseFailure::message(format!(
|
||||
"cannot capture test output '{}': {error}",
|
||||
path.display()
|
||||
))
|
||||
})
|
||||
};
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(capture(&stdout)?)
|
||||
.stderr(capture(&stderr)?)
|
||||
.process_group(0);
|
||||
let subreaper = Subreaper::acquire().map_err(CaseFailure::message)?;
|
||||
let started = Instant::now();
|
||||
let child = command
|
||||
.spawn()
|
||||
.map_err(|error| CaseFailure::message(format!("cannot run test process: {error}")))?;
|
||||
let group = Pid::from_raw(child.id() as i32).expect("spawned child has a positive PID");
|
||||
let mut process = CaseProcess {
|
||||
child,
|
||||
group,
|
||||
cleaned: false,
|
||||
};
|
||||
let outcome = process.wait_until_exit(started, timeout);
|
||||
let (status, mut cleanup) = process.cleanup();
|
||||
if let Err(error) = subreaper.restore() {
|
||||
cleanup.push(error);
|
||||
}
|
||||
let mut errors: Vec<String> = outcome.err().into_iter().collect();
|
||||
if errors.is_empty()
|
||||
&& let Some(status) = status
|
||||
&& !status.success()
|
||||
{
|
||||
errors.push(format!("test process exited with {status}"));
|
||||
}
|
||||
errors.extend(cleanup);
|
||||
let mut read = |path: &Path| match fs::read(path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) => {
|
||||
errors.push(format!(
|
||||
"cannot read test output '{}': {error}",
|
||||
path.display()
|
||||
));
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
let stdout = read(&stdout);
|
||||
let stderr = read(&stderr);
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CaseFailure {
|
||||
message: errors.join("\n"),
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct CaseProcess {
|
||||
child: Child,
|
||||
group: Pid,
|
||||
cleaned: bool,
|
||||
}
|
||||
|
||||
impl CaseProcess {
|
||||
fn wait_until_exit(&self, started: Instant, timeout: Duration) -> Result<(), String> {
|
||||
loop {
|
||||
// NOWAIT 保留 leader,组清理前不会释放它的 PID 给其它进程。
|
||||
match process::waitid(
|
||||
WaitId::Pid(self.group),
|
||||
WaitIdOptions::EXITED | WaitIdOptions::NOHANG | WaitIdOptions::NOWAIT,
|
||||
) {
|
||||
Ok(Some(_)) => return Ok(()),
|
||||
Ok(None) | Err(Errno::INTR) => {}
|
||||
Err(error) => return Err(format!("cannot observe test process: {error}")),
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
if elapsed >= timeout {
|
||||
return Err(format!(
|
||||
"test process timed out after {} ms",
|
||||
timeout.as_millis()
|
||||
));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5).min(timeout - elapsed));
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) -> (Option<ExitStatus>, Vec<String>) {
|
||||
let mut errors = Vec::new();
|
||||
let killed = match process::kill_process_group(self.group, Signal::KILL) {
|
||||
Ok(()) | Err(Errno::SRCH) => true,
|
||||
Err(error) => {
|
||||
errors.push(format!("cannot terminate test process group: {error}"));
|
||||
if let Err(error) = self.child.kill() {
|
||||
errors.push(format!("cannot terminate test process: {error}"));
|
||||
}
|
||||
false
|
||||
}
|
||||
};
|
||||
let status = match self.child.wait() {
|
||||
Ok(status) => Some(status),
|
||||
Err(error) => {
|
||||
errors.push(format!("cannot reap test process: {error}"));
|
||||
None
|
||||
}
|
||||
};
|
||||
if killed {
|
||||
loop {
|
||||
// 只回收本例的进程组,不能消费编译器或其它工具的子进程。
|
||||
match process::waitpgid(self.group, WaitOptions::empty()) {
|
||||
Ok(Some(_)) | Err(Errno::INTR) => {}
|
||||
Err(Errno::CHILD) => break,
|
||||
Ok(None) => unreachable!("blocking waitpgid returns a child or ECHILD"),
|
||||
Err(error) => {
|
||||
errors.push(format!("cannot reap test descendant: {error}"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cleaned = true;
|
||||
(status, errors)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CaseProcess {
|
||||
fn drop(&mut self) {
|
||||
if !self.cleaned {
|
||||
let _ = self.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试串行执行;接管孤儿后代才能在 CLI 返回前完成 wait,而非交给 PID 1。
|
||||
struct Subreaper(Option<Pid>);
|
||||
|
||||
impl Subreaper {
|
||||
fn acquire() -> Result<Self, String> {
|
||||
let previous = process::child_subreaper()
|
||||
.map_err(|error| format!("cannot inspect test child reaper: {error}"))?;
|
||||
process::set_child_subreaper(Some(process::getpid()))
|
||||
.map_err(|error| format!("cannot acquire test child reaper: {error}"))?;
|
||||
Ok(Self(previous))
|
||||
}
|
||||
|
||||
fn restore(self) -> Result<(), String> {
|
||||
process::set_child_subreaper(self.0)
|
||||
.map_err(|error| format!("cannot restore test child reaper: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subreaper {
|
||||
fn drop(&mut self) {
|
||||
let _ = process::set_child_subreaper(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use std::process::Command;
|
||||
|
||||
use super::release::{Layout, MachineManager, Version, platform_contract};
|
||||
|
||||
const USAGE: &str = "Usage:\n dever install <version|latest>\n dever update\n dever target add linux-aarch64\n dever use <version>\n dever uninstall <version>\n dever version [project-root]\n dever cache status\n dever cache clean\n dever lib add|list|update|install|remove|doctor <project-root> [--target linux-x86_64|linux-aarch64] [spec ...]\n dever package add|list|update|remove|doctor <project-root> [spec ...]\n dever check <project-root>\n dever api <project-root> [--output <new-file>]\n dever fmt <project-root> [--check]\n dever test <project-root>\n dever run <project-root> [-- <component>.<domain>.<cmd> '<json-object>']\n dever build <project-root> --output <new-file> [--target linux-x86_64|linux-aarch64]\n dever clean <project-root>\n dever tenant ...";
|
||||
const USAGE: &str = "Usage:\n dever install <version|latest>\n dever update\n dever target add linux-aarch64\n dever use <version>\n dever uninstall <version>\n dever version [project-root]\n dever cache status\n dever cache clean\n dever lib add|list|update|install|remove|doctor <project-root> [--target linux-x86_64|linux-aarch64] [spec ...]\n dever package add|list|update|remove|doctor <project-root> [spec ...]\n dever check <project-root>\n dever api <project-root> [--output <new-file>]\n dever fmt <project-root> [--check]\n dever test <project-root> [--filter <component[.domain[.case]]>] [--timeout-ms <positive-ms>]\n dever run <project-root> [-- <component>.<domain>.<cmd> '<json-object>']\n dever build <project-root> --output <new-file> [--target linux-x86_64|linux-aarch64]\n dever clean <project-root>\n dever tenant ...";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommandResult {
|
||||
@@ -106,13 +106,18 @@ pub fn execute_in(layout: &Layout, arguments: &[OsString]) -> Result<CommandResu
|
||||
manager.resolve_skill(&version)?.display().to_string(),
|
||||
))
|
||||
}
|
||||
[skill, install, directory] if skill == "skill" && install == "install" => {
|
||||
super::skill::install(&manager, Path::new(directory))?;
|
||||
[skill, install] if skill == "skill" && install == "install" => {
|
||||
let root = std::env::current_dir()
|
||||
.map_err(|error| format!("cannot locate current project: {error}"))?;
|
||||
super::skill::install(&manager, &root)?;
|
||||
Ok(CommandResult::Message(format!(
|
||||
"installed dever-language skill entry in '{}'",
|
||||
Path::new(directory).display()
|
||||
"updated Dever development rules in '{}'",
|
||||
root.join("AGENTS.md").display()
|
||||
)))
|
||||
}
|
||||
[skill, install, ..] if skill == "skill" && install == "install" => {
|
||||
Err("run 'dever skill install' in the project root; it accepts no arguments".into())
|
||||
}
|
||||
[command, ..] if command == "new" => {
|
||||
let version = manager
|
||||
.active_version()?
|
||||
@@ -120,7 +125,7 @@ pub fn execute_in(layout: &Layout, arguments: &[OsString]) -> Result<CommandResu
|
||||
dispatch_core(&manager, &version, arguments)
|
||||
}
|
||||
[help] if help == "--help" => Ok(CommandResult::Message(format!(
|
||||
"{USAGE}\n dever new <new-directory> [--markdown]\n dever skill path [project-root]\n dever skill install <new-directory>"
|
||||
"{USAGE}\n dever new <new-directory> [--markdown]\n dever skill path [project-root]\n dever skill install"
|
||||
))),
|
||||
[] => Err(USAGE.into()),
|
||||
_ => dispatch_project_command(&manager, arguments),
|
||||
@@ -175,7 +180,7 @@ fn dispatch_core(
|
||||
Ok(CommandResult::Exited(status.code().unwrap_or(1)))
|
||||
}
|
||||
|
||||
fn select_version(manager: &MachineManager, project_root: &Path) -> Result<Version, String> {
|
||||
pub(super) fn select_version(manager: &MachineManager, project_root: &Path) -> Result<Version, String> {
|
||||
let version = match project_version(project_root)? {
|
||||
Some(version) => version,
|
||||
None => manager
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
//! A stable AI entry resolves the same active-version journal as the launcher.
|
||||
//! Project rules come from the same signed release as the project's compiler.
|
||||
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use super::release::MachineManager;
|
||||
|
||||
const START: &str = "<!-- dever-language:start -->";
|
||||
const END: &str = "<!-- dever-language:end -->";
|
||||
static NEXT_STAGING: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub(super) fn path(manager: &MachineManager) -> Result<PathBuf, String> {
|
||||
@@ -17,75 +20,141 @@ pub(super) fn path(manager: &MachineManager) -> Result<PathBuf, String> {
|
||||
manager.resolve_skill(&version)
|
||||
}
|
||||
|
||||
pub(super) fn install(manager: &MachineManager, destination: &Path) -> Result<(), String> {
|
||||
path(manager)?;
|
||||
let launcher = manager.layout().bin().join("dever");
|
||||
let launcher = launcher
|
||||
.to_str()
|
||||
.ok_or("launcher path must be UTF-8 for an AI skill")?;
|
||||
if launcher.chars().any(char::is_control) || launcher.contains('`') {
|
||||
return Err("launcher path cannot contain control characters or backticks".into());
|
||||
pub(super) fn install(manager: &MachineManager, root: &Path) -> Result<(), String> {
|
||||
let module = root.join("module");
|
||||
let metadata = fs::symlink_metadata(&module)
|
||||
.map_err(|error| format!("run 'dever skill install' in a project root with module/: {error}"))?;
|
||||
if !metadata.is_dir() {
|
||||
return Err("run 'dever skill install' in a project root with a real module/ directory".into());
|
||||
}
|
||||
let quoted = format!("'{}'", launcher.replace('\'', "'\\''"));
|
||||
let content = format!(
|
||||
"---\nname: dever-language\ndescription: Develop, review and debug Dever language applications (.dever and .dever.md), manage Dever Packages and Libs, and install or update Dever. Not the Go Dever framework.\n---\n\n# Dever language\n\nBefore every Dever task, run this read-only command:\n\n```sh\n{quoted} skill path\n```\n\nRead `SKILL.md` in the returned directory completely, then follow its instructions. Resolve its reference and asset paths against that directory. This entry follows the machine's active Dever version, so `dever update` and `dever use` update the compiler and development guidance together. If resolution fails, report the error; do not use stale guidance or infer language syntax. This entry grants no permission to install, update or change a project.\n"
|
||||
);
|
||||
let name = destination
|
||||
.file_name()
|
||||
.ok_or("skill destination requires a new directory name")?;
|
||||
let parent = destination
|
||||
.parent()
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
.unwrap_or(Path::new("."));
|
||||
let parent = fs::canonicalize(parent)
|
||||
.map_err(|error| format!("cannot resolve skill destination parent: {error}"))?;
|
||||
let destination = parent.join(name);
|
||||
let staging = stage(&parent)?;
|
||||
let entry = staging.join("SKILL.md");
|
||||
let result = (|| {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&entry)
|
||||
.map_err(|error| format!("cannot create skill entry: {error}"))?;
|
||||
file.write_all(content.as_bytes())
|
||||
.and_then(|()| file.sync_all())
|
||||
.map_err(|error| format!("cannot persist skill entry: {error}"))?;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use rustix::fs::{CWD, RenameFlags, renameat_with};
|
||||
renameat_with(CWD, &staging, CWD, &destination, RenameFlags::NOREPLACE)
|
||||
.map_err(|error| format!("cannot publish new skill directory: {error}"))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Err("skill installation is not available for this platform".into())
|
||||
}
|
||||
})();
|
||||
if result.is_err() {
|
||||
let _ = fs::remove_file(&entry);
|
||||
let _ = fs::remove_dir(&staging);
|
||||
let version = super::launcher::select_version(manager, root)?;
|
||||
let resources = super::SignedResources::load(manager.layout(), &version)?;
|
||||
resources.read("skills/dever-language/SKILL.md")?;
|
||||
let template = String::from_utf8(resources.read("skills/dever-language/assets/template/AGENTS.md")?)
|
||||
.map_err(|error| format!("signed AGENTS.md template must be UTF-8: {error}"))?;
|
||||
let block = managed_block(&template)?.ok_or("signed AGENTS.md template has no Dever rules block")?;
|
||||
if !template[..block.start].trim().is_empty() || !template[block.end..].trim().is_empty() {
|
||||
return Err("signed AGENTS.md template must contain only its Dever rules block".into());
|
||||
}
|
||||
result
|
||||
|
||||
let destination = root.join("AGENTS.md");
|
||||
let original = read_project_rules(&destination)?;
|
||||
let current = original
|
||||
.as_ref()
|
||||
.map(|(text, _)| text.as_str())
|
||||
.unwrap_or("");
|
||||
let content = match managed_block(current)? {
|
||||
Some(range) => format!(
|
||||
"{}{}{}",
|
||||
¤t[..range.start],
|
||||
&template[block],
|
||||
¤t[range.end..]
|
||||
),
|
||||
None if current.is_empty() => template,
|
||||
None => {
|
||||
let separator = if current.ends_with('\n') { "\n" } else { "\n\n" };
|
||||
format!("{current}{separator}{template}")
|
||||
}
|
||||
};
|
||||
if content == current {
|
||||
return Ok(());
|
||||
}
|
||||
let staging = Staging::create(root)?;
|
||||
let entry = staging.0.join("AGENTS.md");
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&entry)
|
||||
.map_err(|error| format!("cannot stage project rules: {error}"))?;
|
||||
file.write_all(content.as_bytes())
|
||||
.map_err(|error| format!("cannot write project rules: {error}"))?;
|
||||
if let Some((_, permissions)) = &original {
|
||||
file.set_permissions(permissions.clone())
|
||||
.map_err(|error| format!("cannot preserve AGENTS.md permissions: {error}"))?;
|
||||
}
|
||||
file.sync_all()
|
||||
.map_err(|error| format!("cannot persist project rules: {error}"))?;
|
||||
// As with source formatting, an intervening edit invalidates the prepared replacement.
|
||||
if read_project_rules(&destination)? != original {
|
||||
return Err("AGENTS.md changed during skill installation; run the command again".into());
|
||||
}
|
||||
publish(&entry, &destination, original.is_some())
|
||||
}
|
||||
|
||||
fn stage(parent: &Path) -> Result<PathBuf, String> {
|
||||
loop {
|
||||
let path = parent.join(format!(
|
||||
".dever-skill-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT_STAGING.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
let mut builder = fs::DirBuilder::new();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
builder.mode(0o700);
|
||||
}
|
||||
match builder.create(&path) {
|
||||
Ok(()) => return Ok(path),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
||||
Err(error) => return Err(format!("cannot stage skill entry: {error}")),
|
||||
fn read_project_rules(path: &Path) -> Result<Option<(String, fs::Permissions)>, String> {
|
||||
let metadata = match fs::symlink_metadata(path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(format!("cannot inspect AGENTS.md: {error}")),
|
||||
};
|
||||
if !metadata.is_file() {
|
||||
return Err("AGENTS.md must be a regular file, not a symlink or directory".into());
|
||||
}
|
||||
if metadata.permissions().readonly() {
|
||||
return Err("AGENTS.md is read-only".into());
|
||||
}
|
||||
let text = fs::read_to_string(path)
|
||||
.map_err(|error| format!("cannot read AGENTS.md as UTF-8: {error}"))?;
|
||||
Ok(Some((text, metadata.permissions())))
|
||||
}
|
||||
|
||||
fn managed_block(text: &str) -> Result<Option<Range<usize>>, String> {
|
||||
let starts: Vec<_> = text.match_indices(START).map(|(offset, _)| offset).collect();
|
||||
let ends: Vec<_> = text.match_indices(END).map(|(offset, _)| offset).collect();
|
||||
match (starts.as_slice(), ends.as_slice()) {
|
||||
([], []) => Ok(None),
|
||||
([start], [end]) if start < end => Ok(Some(*start..end + END.len())),
|
||||
_ => Err("AGENTS.md must have exactly one complete Dever rules block or no block".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn publish(entry: &Path, destination: &Path, replace: bool) -> Result<(), String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use rustix::fs::{CWD, RenameFlags, renameat_with};
|
||||
let flags = if replace {
|
||||
RenameFlags::empty()
|
||||
} else {
|
||||
RenameFlags::NOREPLACE
|
||||
};
|
||||
renameat_with(CWD, entry, CWD, destination, flags)
|
||||
.map_err(|error| format!("cannot publish AGENTS.md: {error}"))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (entry, destination, replace);
|
||||
Err("skill installation is not available for this platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
struct Staging(PathBuf);
|
||||
|
||||
impl Staging {
|
||||
fn create(parent: &Path) -> Result<Self, String> {
|
||||
loop {
|
||||
let path = parent.join(format!(
|
||||
".dever-skill-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT_STAGING.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
let mut builder = fs::DirBuilder::new();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
builder.mode(0o700);
|
||||
}
|
||||
match builder.create(&path) {
|
||||
Ok(()) => return Ok(Self(path)),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
||||
Err(error) => return Err(format!("cannot stage project rules: {error}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Staging {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(self.0.join("AGENTS.md"));
|
||||
let _ = fs::remove_dir(&self.0);
|
||||
}
|
||||
}
|
||||
@@ -17,24 +17,51 @@ pub(crate) fn check(program: &mut Program) {
|
||||
continue;
|
||||
}
|
||||
if !function.public {
|
||||
let key = (function.owner, function_key(function));
|
||||
if let Some(previous) = implementations.get(&key).copied() {
|
||||
let previous: &Function = &program.functions[previous];
|
||||
warnings.push(related("W003", format!("private function '{}' has the same implementation and contract as '{}'; consider sharing one implementation", function.name, previous.name), function.span, previous.span));
|
||||
} else {
|
||||
implementations.insert(key, id);
|
||||
if let Some(target) = forwarding_target(function, program) {
|
||||
warnings.push(related("W005", format!("private function '{}' only forwards unchanged inputs to '{}' without adding a contract; review whether this wrapper is needed", function.name, target.name), function.span, target.span));
|
||||
}
|
||||
if let Some(duplicate) =
|
||||
duplicate_implementation(program, id, &mut implementations, "W003")
|
||||
{
|
||||
warnings.push(duplicate);
|
||||
} else if let Some(target) = forwarding_target(function, program) {
|
||||
warnings.push(related("W005", format!("private function '{}' only forwards unchanged inputs to '{}' without adding a contract; review whether this wrapper is needed", function.name, target.name), function.span, target.span));
|
||||
}
|
||||
}
|
||||
for clause in &function.clauses {
|
||||
repeated_calls(clause, program, &mut warnings);
|
||||
repeated_calls(clause, program, "W004", &mut warnings);
|
||||
}
|
||||
}
|
||||
program.warnings.extend(warnings);
|
||||
}
|
||||
|
||||
fn duplicate_implementation(
|
||||
program: &Program,
|
||||
id: usize,
|
||||
implementations: &mut BTreeMap<(usize, String), usize>,
|
||||
code: &'static str,
|
||||
) -> Option<Diagnostic> {
|
||||
let function = &program.functions[id];
|
||||
let key = (function.owner, function_key(function));
|
||||
if let Some(previous) = implementations.get(&key).copied() {
|
||||
let previous = &program.functions[previous];
|
||||
let action = if code == "C016" {
|
||||
"reuse one implementation"
|
||||
} else {
|
||||
"consider sharing one implementation"
|
||||
};
|
||||
Some(related(
|
||||
code,
|
||||
format!(
|
||||
"private function '{}' has the same implementation and contract as '{}'; {action}",
|
||||
function.name, previous.name
|
||||
),
|
||||
function.span,
|
||||
previous.span,
|
||||
))
|
||||
} else {
|
||||
implementations.insert(key, id);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn related(code: &'static str, message: String, primary: Span, previous: Span) -> Diagnostic {
|
||||
let mut warning = Diagnostic::error(code, message, primary);
|
||||
warning.related.push(Label {
|
||||
@@ -291,7 +318,12 @@ struct Computation {
|
||||
result_version: usize,
|
||||
}
|
||||
|
||||
fn repeated_calls(clause: &Clause, program: &Program, warnings: &mut Vec<Diagnostic>) {
|
||||
fn repeated_calls(
|
||||
clause: &Clause,
|
||||
program: &Program,
|
||||
code: &'static str,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
let mut versions = vec![0; clause.locals.len()];
|
||||
let mut previous = BTreeMap::<String, Computation>::new();
|
||||
for statement in &clause.body {
|
||||
@@ -310,7 +342,17 @@ fn repeated_calls(clause: &Clause, program: &Program, warnings: &mut Vec<Diagnos
|
||||
if let Some(computation) = previous.get(&key)
|
||||
&& versions[computation.result_slot] == computation.result_version
|
||||
{
|
||||
warnings.push(related("W004", "this pure call repeats unchanged inputs while the earlier result is still available; consider reusing that result".into(), value.span, computation.span));
|
||||
let action = if code == "C016" {
|
||||
"reuse that result"
|
||||
} else {
|
||||
"consider reusing that result"
|
||||
};
|
||||
diagnostics.push(related(
|
||||
code,
|
||||
format!("this pure call repeats unchanged inputs while the earlier result is still available; {action}"),
|
||||
value.span,
|
||||
computation.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
// A field write invalidates every projection from the containing value.
|
||||
@@ -360,6 +402,7 @@ pub(crate) fn application_errors(
|
||||
reachable: &BTreeSet<usize>,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut errors = Vec::new();
|
||||
let mut implementations = BTreeMap::new();
|
||||
for (id, function) in program.functions.iter().enumerate() {
|
||||
let package = &program.packages[function.owner];
|
||||
if package.bundled || function.port.is_some() || function.setting.is_some() {
|
||||
@@ -379,12 +422,27 @@ pub(crate) fn application_errors(
|
||||
),
|
||||
function.span,
|
||||
));
|
||||
} else if !function.public {
|
||||
continue;
|
||||
}
|
||||
if !function.public {
|
||||
// Model 查询等有副作用的操作还需比较此结构键未记录的元数据。
|
||||
if program.effects[id].is_empty()
|
||||
&& let Some(duplicate) =
|
||||
duplicate_implementation(program, id, &mut implementations, "C016")
|
||||
{
|
||||
errors.push(duplicate);
|
||||
continue;
|
||||
}
|
||||
if let Some(target) = forwarding_target(function, program) {
|
||||
errors.push(related("C016", format!("private function '{}' only forwards unchanged inputs to '{}' without adding a contract", function.name, target.name), function.span, target.span));
|
||||
continue;
|
||||
}
|
||||
} else if app && trivial_model_wrapper(function) {
|
||||
errors.push(Diagnostic::error("C016", format!("App function '{}' only forwards a Model CRUD operation; declare rest for plain CRUD", function.name), function.span));
|
||||
continue;
|
||||
}
|
||||
for clause in &function.clauses {
|
||||
repeated_calls(clause, program, "C016", &mut errors);
|
||||
}
|
||||
}
|
||||
errors.sort_by_key(|error| (error.primary.source, error.primary.start));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Dever Language for VS Code
|
||||
|
||||
支持 `.dever` 语法高亮、括号配对、自动闭合、缩进和 `#` 快捷键注释。
|
||||
Markdown 正文保留原有编辑体验,`dever` 和 `typescript dever` 围栏共用 Dever 高亮规则;支持反引号、波浪号及更长的关闭围栏。无需先安装 Dever 编译器。
|
||||
Markdown 中的 `dever` 和 `typescript dever` 代码块也会高亮,正文仍按 Markdown 显示。无需先安装 Dever 编译器。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -14,7 +14,7 @@ code --install-extension dever-language-0.1.0.vsix
|
||||
`.dever.md` 继续使用 Markdown 语言模式,不要切换为普通 Dever。已有纯 TypeScript 围栏不会被接管。
|
||||
插件随 VSIX 单独分发,不包含编译器、skills、依赖或构建缓存;`dever update` 不负责安装或更新编辑器插件。
|
||||
|
||||
这是纯语法插件,不提供补全、跳转、格式化或实时编译诊断。最终颜色由 VS Code 主题决定;Markdown 着色依赖 VS Code 的宿主语法,不等同于编译器的程序块抽取。语言合法性、Markdown 说明契约和业务约束仍由 `dever check` 判断。
|
||||
目前只做语法高亮,不提供补全、跳转、格式化或实时诊断。颜色由 VS Code 主题决定;高亮正常不代表代码正确,语法、Markdown 说明和业务约束仍用 `dever check` 检查。
|
||||
插件暂未发布到插件市场,扩展标识为 `shemic.dever-language`。
|
||||
|
||||
## 开发与打包
|
||||
@@ -29,4 +29,4 @@ code --install-extension dever-language-0.1.0.vsix
|
||||
npm test -- --markdown-grammar /absolute/path/to/markdown.tmLanguage.json
|
||||
```
|
||||
|
||||
测试用实际 TextMate/Oniguruma 引擎校验作用域与嵌入语言标识,覆盖两种 Markdown 围栏、关闭边界、注释/字符串、整数除法、声明以及仓库维护中的 CMS 双实现。没有宿主 Markdown 语法时明确报错,不以仿造语法代替。
|
||||
测试使用 VS Code 实际的 TextMate/Oniguruma 语法引擎,检查反引号和波浪号代码块、关闭边界、注释、字符串、整数除法、声明与 CMS 双实现。缺少真实 Markdown 语法文件时会报错,不用仿造文件代替。
|
||||
+38
-12
@@ -1,6 +1,6 @@
|
||||
# Dever 示例
|
||||
|
||||
`examples/` 只保留两个入口:`cms/` 是当前维护的应用示例,`old/` 是旧语言合同和回归材料的归档。旧目录不符合当前 component/domain/role 应用布局,不应直接执行 `dever check/run/build`。
|
||||
看当前用法请用 `cms/`;`old/` 只保存历史回归材料,不是新项目模板,也不能直接按当前应用运行。
|
||||
|
||||
CMS 同时维护 [Dever 源码](cms/dever/module/news/article/api/admin/manage.dever)和 [Markdown 源码](cms/md/module/news/article/api/admin/manage.dever.md)。两套实现使用同一目录与业务合同:
|
||||
|
||||
@@ -35,29 +35,55 @@ test/
|
||||
|
||||
Markdown 项目使用相同的目录和 API/CMD 声明,仅把源码扩展名换成 `.dever.md` 并补齐文档合同。
|
||||
|
||||
编译器从 API/CMD/Job 声明生成入口。平台租户、账号、成员与会话是 `global type`,保存在共享平台库;文章、分类、修订、媒体、排期和审计属于当前租户物理库。认证 verify 只读取 global Model 并建立只读用户与租户身份;接口权限由核心根据当前站点和角色自动校验,后续 API 才进入可信 tenant scope。一个职责文件可包含一组内聚方法,没有按函数拆文件。
|
||||
## 数据和权限
|
||||
|
||||
文章通过 `/news/article/admin/manage/create` 创建,`list/detail` 查询,`remove` 删除草稿,`publish` 发布。编辑调用 `/content/revision/admin/manage/create`,传入 `selected_article_id`、`expected_version`、标题正文和可空分类/媒体 ID;版本冲突返回 409,文章更新和修订快照在同一事务内完成。文章不声明绕过这些规则的自动 REST 写接口。列表 SQL 直接返回同领域 `app.ArticleView`,不暴露私有 Model。
|
||||
平台租户、账号、成员和会话使用 `global type`,保存在平台库。文章、分类、修订、媒体、排期和审计放在各租户自己的数据库。
|
||||
|
||||
`/publishing/schedule/admin/manage/schedule` 接收文章 ID 和 `publish_at`;创建时验证作者与草稿状态,按真实到期时间入队。每个排期有独立任务键,同刻文章不会合并。任务执行仍由核心复验身份和权限;发布、审计和排期状态一并提交或回滚,完成任务重放不会重复写入。手动重复发布返回 409。
|
||||
verify 先核实 global 身份数据。核心按站点和角色检查接口权限,成功后才访问租户数据。入口由 API、CMD、Job 声明生成,无需 main。相关方法放在同一业务主题文件里,不按函数逐个拆文件。
|
||||
|
||||
文章接口位于 `/news/article/admin/manage/`:create 创建,list/detail 查询,remove 删除草稿,publish 发布。
|
||||
|
||||
编辑使用 `/content/revision/admin/manage/create`,传入文章 ID、expected_version、标题正文和可空分类/媒体 ID。版本冲突返回 409,文章更新与修订快照一起提交。文章不用自动 REST 写接口,以免跳过这些规则;列表返回 `app.ArticleView`,不泄漏私有 Model。
|
||||
|
||||
定时发布使用 `/publishing/schedule/admin/manage/schedule`,传入文章 ID 和 publish_at。创建时检查作者和草稿状态;每个排期有独立任务,不会合并同时发布的文章。
|
||||
|
||||
执行任务前,核心再次核验身份和权限。发布、审计和排期状态一起提交或回滚,完成任务重复执行不重复写入。手动重复发布返回 409。
|
||||
|
||||
## 先检查,再运行
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
dever check examples/cms/dever
|
||||
dever check examples/cms/md
|
||||
dever test examples/cms/dever
|
||||
dever test examples/cms/md
|
||||
dever test examples/cms/dever --filter news.article.publish
|
||||
dever test examples/cms/md --filter news.article.publish
|
||||
dever fmt examples/cms/dever --check
|
||||
dever fmt examples/cms/md --check
|
||||
dever run examples/cms/dever -- user.account.bootstrap '{"tenant_key":"tenant-one","tenant_name":"Tenant One","email":"owner@example.com","display_name":"Owner","password":"change-me-now"}'
|
||||
dever tenant migrate examples/cms/dever 1
|
||||
dever tenant owner examples/cms/dever 1 admin 1
|
||||
dever tenant owner examples/cms/dever 1 front 1
|
||||
```
|
||||
|
||||
`run/build` 只读取各项目的 `config/setting.json`,运行数据写入各自 `data/`。先用 bootstrap CMD 建立平台租户、业务 Owner membership 与 Argon2 密码,再用返回的真实 `tenant_id` 执行 `tenant migrate`,并用返回的 `tenant_id/user_id` 分别 provision admin/front 核心 RBAC Owner;示例中的 `1` 只是首次空库的常见结果,不能在生产脚本里硬编码。两个站点各自通过同站点的 `user/authorization/<site>/manage` 接口管理普通角色。不带 CMD 参数时启动 admin/front HTTP API。`test` 不读取部署配置;每个涉及 Model 的测试使用独立临时 SQLite,并在结束后清理。
|
||||
这些测试使用独立临时 SQLite,不连接部署数据库,结束后清理。命令仍会读取配置与锁中的依赖信息。检查全部应用测试时再去掉 `--filter`。
|
||||
|
||||
CMS 的五个应用测试验证分类、发布规则、账号引导/规范化和会话生命周期的正常业务输出,位于各项目根 `test/`;拒绝、过期等负向行为,以及带真实登录的 HTTP、并发编辑、上传、Job 重试/回滚验收见 [业务验收说明](../test/cms/README.md)。普通测试不伪造认证上下文。编译器和 CLI 的 Rust 回归测试仍统一位于仓库根 `test/`,两者都不混入业务 `module/`。部署前必须替换示例 JWT secret,并把两个站点的 `origin` 和 Host 改成真实 HTTPS 入口;`origin` 描述反向代理外部地址,不是 Dever 内部监听地址。
|
||||
实际部署前先替换 JWT secret,并把两个站点的 origin 和 Host 改成真实 HTTPS 入口;origin 是用户访问的外部地址,不是内部监听地址。
|
||||
|
||||
用 bootstrap CMD 建立平台租户、Owner 成员和密码:
|
||||
|
||||
```sh
|
||||
dever run examples/cms/dever -- user.account.bootstrap '{"tenant_key":"tenant-one","tenant_name":"Tenant One","email":"owner@example.com","display_name":"Owner","password":"change-me-now"}'
|
||||
```
|
||||
|
||||
替换示例账号与密码,再用返回的真实 tenant_id 和 user_id 执行以下命令,不要猜测 ID 是 1:
|
||||
|
||||
```text
|
||||
dever tenant migrate examples/cms/dever <tenant_id>
|
||||
dever tenant owner examples/cms/dever <tenant_id> admin <user_id>
|
||||
dever tenant owner examples/cms/dever <tenant_id> front <user_id>
|
||||
```
|
||||
|
||||
这会迁移租户数据库,并建立后台、前台的核心 Owner。之后各站点通过 `user/authorization/<site>/manage` 管理普通角色。运行配置在项目的 `config/setting.json`,数据在 `data/`;`dever run examples/cms/dever` 不带 CMD 参数时启动 HTTP 服务。
|
||||
|
||||
CMS 的五个应用测试覆盖分类、发布规则、账号引导/规范化和会话正常流程,放在各项目的 `test/`,不伪造认证上下文。
|
||||
|
||||
拒绝、过期、真实登录、HTTP、并发编辑、上传和 Job 重试/回滚见 [业务验收说明](../test/cms/README.md)。编译器与 CLI 的 Rust 回归测试在仓库根 `test/`,都不放进业务 module。
|
||||
|
||||
旧示例仍可供编译器维护者查阅历史语义,但不能作为新 Dever 应用的目录、可见性或调用方式范本。
|
||||
+45
-11
@@ -1,25 +1,59 @@
|
||||
# External Worker SDKs
|
||||
# Python、JavaScript 与 Go SDK
|
||||
|
||||
These are stdio Worker libraries for Python, JavaScript (Node), and Go. The compiler remains the sole owner of Port schemas. Each checked external Adapter exposes `ExternalWorkerContract::sdk_manifest()`, including the exact identity and recursive setting, input, output and declared-error types. The managed Python and JavaScript runners package this manifest and bind each Port operation to a same-named function exported by the Adapter entry; JavaScript accepts ESM named exports or CommonJS `module.exports` properties. Application code does not maintain a second registration table. Python uses `Worker.from_manifest(manifest, handlers)`, JavaScript uses `Worker.fromManifest(manifest, handlers)`, and Go uses `NewFromManifest(manifest)` followed by `RegisterHandlers(handlers)`. These paths decode inputs and validate results and declared business-error payloads from the same compiler-generated contract. The SDK checks the identity during `hello` and never adopts a schema supplied by the supervisor.
|
||||
SDK 用于把现成的 Python、JavaScript 或 Go 能力接入 Dever。应用先声明 Port,再用外部 Adapter 对接;Dever 会管理 Worker 进程和通信。
|
||||
|
||||
The lower-level `Contract`/`Worker` registration API remains available for protocol fixtures. The setting decoder runs before `ready`; it receives only the `hello.setting` value. Handlers return JSON-compatible values or a declared `BusinessError`. Python handlers are async; JavaScript handlers receive an `AbortSignal`; Go handlers receive a `context.Context`. A cooperative cancellation receives one `dever.cancelled` protocol error and discards its handler result. If a handler ignores cancellation for one second, the Worker fails and admits no further calls. The supervisor owns the Worker process and its stdin lifetime; an in-process Go `Serve` caller must close or unblock its own `io.Reader` when returning.
|
||||
开发应用先看 [三种语言的完整例子](../skills/references/external-lib.md)。那里包含同一 Port、三种 Adapter、处理函数和 fake 测试,不需要手写协议或维护另一份接口注册表。
|
||||
|
||||
The three `example` entries use the same fixture identity and contract to demonstrate the lower-level API. `fixture-schema-v1` and `test/external-sdk/typed_contract.json` are test data, not production Port hashes. Worker stdout is reserved for 4-byte big-endian length-prefixed UTF-8 JSON frames. Diagnostics go to stderr. The libraries use no external packages and do not read environment variables or `PATH`. A managed Worker entry reads only its explicitly packaged manifest path. Managed Python packs require fixed `-I -S -B` flags; the runner imports its packaged SDK before exposing Adapter source and dependencies on `sys.path`.
|
||||
## 处理函数怎么写
|
||||
|
||||
Run the opt-in host SDK check with explicit absolute runtime paths, for example:
|
||||
函数名与 Port 操作名对应。输入、输出和业务错误的类型由 Dever 编译器生成,SDK 负责验证。
|
||||
|
||||
| 语言 | 处理函数 | 加载生成的接口定义 |
|
||||
| --- | --- | --- |
|
||||
| Python | 异步函数 | `Worker.from_manifest(manifest, handlers)` |
|
||||
| JavaScript | 函数接收 `AbortSignal`;支持 ESM 导出或 CommonJS 属性 | `Worker.fromManifest(manifest, handlers)` |
|
||||
| Go | 函数接收 `context.Context` | `NewFromManifest(manifest)`,再 `RegisterHandlers(handlers)` |
|
||||
|
||||
处理函数返回能用 JSON 表达的值,业务失败返回已声明的 `BusinessError`。不要往 stdout 写日志;stdout 用于 Worker 通信,日志写 stderr。
|
||||
|
||||
取消请求会产生一次 `dever.cancelled` 错误,已取消调用的结果会被丢弃。如果处理函数一秒内不响应取消,Worker 失败并停止接收新调用。Dever 管理进程与 stdin;自行在 Go 进程内调用 `Serve` 时,调用方负责关闭或解除自己的 `io.Reader` 阻塞。
|
||||
|
||||
## SDK 维护与验证
|
||||
|
||||
维护 SDK 时,显式指定本机运行环境的绝对路径:
|
||||
|
||||
```text
|
||||
/usr/bin/python3 -B test/external-sdk/check.py --python /usr/bin/python3 --node /absolute/path/to/node --go /absolute/path/to/go
|
||||
```
|
||||
|
||||
Omitted runtime paths report `unavailable`; the script never discovers language runtimes through `PATH`. It builds the Go fixtures in a temporary offline cache. This check exercises both the lower-level and compiler-manifest-shaped typed paths on host interpreters. Managed runtime packaging and execution on a machine without host language runtimes require separate acceptance.
|
||||
未指定的语言报告 `unavailable`,不会通过 PATH 猜测。Go 夹具在临时离线缓存中构建。这项检查验证本机 SDK 和类型协议;不能代替发行包或“机器未安装 Python/Node/Go”的运行验收。普通应用测试使用 Port fake,不启动真实 Worker。
|
||||
|
||||
Managed dependencies use `dever-lock-v6`. Python extras and selected wheel METADATA, npm nested instances/peers/optional/bundled dependencies, and authenticated Go module evidence are part of the exact lock. Original archives retain their fixed source locator and filename; build receipts retain the complete execution input lock, including npm's graph before optional build pruning. `dever lib install <project>` restores these exact inputs and artifacts without resolving new versions or modifying configuration/lock bytes. Missing built outputs are replayed through the same sandbox executor and must match the recorded output digest; non-reproducible outputs fail explicitly. Native Python wheels install into the packaged interpreter prefix and must satisfy its signed tags, extension ABI and loader closure.
|
||||
编译器通过 `ExternalWorkerContract::sdk_manifest()` 生成唯一接口定义,包括身份、配置、输入、输出和业务错误。SDK 在 `hello` 时核对身份,不接受 supervisor 临时指定的 schema。配置在 `ready` 前解码,只读取 `hello.setting`。
|
||||
|
||||
Cross builds select the application platform explicitly: `dever lib update <project> --target linux-aarch64`, followed by `dever build <project> --target linux-aarch64 --output <new-file>`. Worker source and Port contracts stay unchanged. Locks, interpreters, native dependencies and sandbox assets must match that target. A Go runtime pack additionally declares `build.host` for its compiler/linker/analyzer; `build.goos/goarch` and standard-library object headers describe the output architecture. The tools run with a cleared environment and must have the correct target baked in. Missing target assets are errors, with no host runtime fallback. `run` and ordinary tests remain host operations.
|
||||
本目录的三个 `example` 展示底层 `Contract`/`Worker` 注册 API,用于协议夹具。`fixture-schema-v1` 和 `test/external-sdk/typed_contract.json` 都是测试数据,不能复制成生产 Port hash。通信帧由 4 字节大端长度和 UTF-8 JSON 组成。
|
||||
|
||||
Only explicit Lib preparation may build Python sdists or run npm registry installation hooks. The shared build owner uses signed compiler/header/sysroot/frontend packs in a Linux sandbox with no network, immutable inputs and owned writable scratch space. Python PEP 517 build requirements have their own exact lock; each hook has a fresh process, and the parent retains the metadata snapshot used to validate the final wheel. npm uses its packaged lifecycle runner for `preinstall`, `install` and `postinstall`, including dependency bins and node-gyp. Missing tools do not fall back to host `PATH`.
|
||||
SDK 不依赖第三方包,也不读取环境变量或 PATH。托管 Worker 只读取已打包的 manifest。Python 固定使用 `-I -S -B`,先加载打包的 SDK,再把 Adapter 与依赖加入 `sys.path`。
|
||||
|
||||
Build receipts bind sources, target runtimes, tools, frontend, dependency graph and final output hashes. Worker preparation replays those outputs offline without running installation hooks. Every npm `.node` output is classified by target/loader checks and an isolated Node load probe; only admitted addons enter the immutable Worker tree, including trees used by threads or child processes. Rejected platform/ABI variants remain in the locked archive. Runtime native dependencies must come from that tree or the signed OS library set, never the compiler sysroot. These rules do not change the Port protocol or require an application-side registration layer.
|
||||
## 依赖与跨平台构建
|
||||
|
||||
A generated addon that fails admission fails the build; a pure JavaScript fallback is not evidence of successful native compilation. Build subprocesses have a five-minute deadline and separate 16 MiB stdout/stderr limits. Scratch usage is checked every 250 ms against 512 MiB and 65,536 entries, with owned-process cleanup on failure. This periodic check is not a kernel disk quota and may observe overshoot between checks. The final Worker installation remains limited to 256 MiB and 65,536 entries.
|
||||
`dever lib install <project>` 按现有锁恢复依赖,不重新选版本、不修改配置或锁。`dever-lock-v6` 记录 Python extras/wheel METADATA、npm 嵌套依赖及 peers/optional/bundled 依赖、Go 模块校验证据。原始包保留来源与文件名,构建记录保留完整输入,包括 npm 剪除可选构建依赖前的依赖图。
|
||||
|
||||
丢失的构建结果由同一沙箱重建,必须与记录的摘要一致。Python 原生 wheel 安装到配套解释器中,必须满足签名声明的标签、扩展 ABI 和动态库依赖要求。
|
||||
|
||||
生成 ARM64 应用时,先运行 `dever lib update <project> --target linux-aarch64`,再运行 `dever build <project> --target linux-aarch64 --output <new-file>`。Port 和 Worker 源码无需改写;锁、解释器、原生依赖和沙箱资源必须匹配目标平台。缺少目标资源时失败,不借用宿主运行环境。`run/test` 仍在本机执行。
|
||||
|
||||
Go pack 的 `build.host` 描述编译器、链接器与分析器的运行平台;`build.goos/goarch` 和标准库对象头描述输出平台。工具在清空的环境中执行,目标必须预先正确配置。
|
||||
|
||||
## 第三方源码构建限制
|
||||
|
||||
只有显式准备 Lib 时才构建 Python sdist 或运行 npm 安装钩子。构建使用签名工具包和 Linux 沙箱,输入只读、网络关闭,写入仅限本次临时目录。缺少工具时不回退宿主 PATH。
|
||||
|
||||
Python PEP 517 的构建依赖单独锁定,每个 hook 使用新进程,最终 wheel 按保留的元数据核对。npm 的配套运行器处理 `preinstall/install/postinstall`、依赖 bin 和 node-gyp。
|
||||
|
||||
构建记录绑定源码、目标运行环境、工具、构建前端、依赖图和输出摘要。Worker 准备阶段离线恢复结果,不重跑安装钩子。npm `.node` 产物须通过目标检查、动态加载检查和独立 Node 加载探测,才能进入只读 Worker 目录;线程和子进程也受此限制。不匹配平台或 ABI 的其他变体保留在锁定原始包中,不加入运行目录。运行时原生依赖只能来自 Worker 目录或签名 OS 库,不能借用编译器 sysroot。
|
||||
|
||||
生成的原生 addon 验证失败,整个构建就失败;使用纯 JS 备用路径不能证明原生构建成功。当前限制:
|
||||
|
||||
- 构建进程最多 5 分钟,stdout/stderr 各最多 16 MiB。
|
||||
- 每 250 毫秒检查临时目录,限制 512 MiB、65,536 个条目;这是定期检查,检查间隔内可能短暂超出。
|
||||
- 最终 Worker 最多 256 MiB、65,536 个条目。失败时清理本次拥有的进程。
|
||||
+11
-6
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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 组件开发。
|
||||
description: 开发、检查或维护 Dever 语言应用(.dever、.dever.md),创建项目,安装更新 dever,接入 Package 或 Python/JS/Go Lib 时使用。涵盖目录、语法、权限、租户与代码质量约束。不用于 Go Dever 框架或 dever-go。
|
||||
---
|
||||
|
||||
# Dever 语言开发
|
||||
@@ -10,9 +10,9 @@ Dever 是独立语言。应用源码使用 `.dever` 或 `.dever.md`,不沿用
|
||||
## 先确定版本和范围
|
||||
|
||||
1. 读取用户项目的 `AGENTS.md`、现有目录和 `config/setting.json`。回答、分析、诊断只做只读检查;用户要求修改时再实施,不擅自安装软件、改服务或改全局命令。
|
||||
2. 若已安装机器版 Dever,新项目运行 `dever skill path`,已有项目运行 `dever skill path <project-root>`,读取返回目录中的 `SKILL.md` 与本次需要的参考文件。已经在那个目录读取本 skill 时不再递归加载。它由 `dever update` 与核心一起更新;已有项目按其可选 dever.version 选择对应指引,不能把活动版本新增语法强加给旧项目。
|
||||
2. 已有项目运行 `dever skill path <project-root>`,新项目运行 `dever skill path`。完整读取返回目录中的 `SKILL.md` 与本次需要的参考;已在该目录读取时不重复加载。项目可用 `dever.version` 锁定编译器版本,规则随该版本选择。
|
||||
3. 尚无 `dever` 时按 [安装与更新](references/install.md) 引导安装;不能把 `dever-go` 当作 Dever 语言命令。先核对主机平台和安装前提,安装失败时保留真实错误,不用缺少运行资源的源码开发二进制冒充完整安装。
|
||||
4. 新项目使用 `dever new <new-directory>`;Markdown 项目加 `--markdown`。不要手工猜模板,不覆盖已有目录。项目模板的用途和检查命令见 [项目开发](references/development.md)。
|
||||
4. 新项目使用 `dever new <new-directory>`;Markdown 项目加 `--markdown`。它会自动生成 `AGENTS.md`。已有项目需要接入或刷新规则时,在项目根目录运行 `dever skill install`,无需指定技能目录。不要手工猜模板或覆盖已有项目。
|
||||
|
||||
开发、重构或审查应用代码前,必须读取 [项目开发](references/development.md) 中的文件与函数组织、配置与硬编码、禁止绕过检查、验证范围。安装或纯语法问答只加载与请求相关的参考。
|
||||
|
||||
@@ -24,14 +24,19 @@ Dever 是独立语言。应用源码使用 `.dever` 或 `.dever.md`,不沿用
|
||||
| 类型、分句、函数、集合、失败和并发 | [language.md](references/language.md) |
|
||||
| HTTP/CMD、Model、权限、租户、Job | [application.md](references/application.md) |
|
||||
| 标准能力、外部生态、Package | [libraries.md](references/libraries.md) |
|
||||
| HTTP/REST、Job、集合、Port fake 的完整项目 | [examples.md](references/examples.md),只读所需示例 |
|
||||
| 常用调用的参数、返回值与失败边界 | [signatures.md](references/signatures.md) |
|
||||
| Python/JS/Go 外部 Lib 的同一 Port 合同 | [external-lib.md](references/external-lib.md) |
|
||||
| Markdown 源码 | [markdown.md](references/markdown.md) |
|
||||
| 安装命令、安装 skill、更新 | [install.md](references/install.md) |
|
||||
|
||||
只加载当前任务相关参考。当前版本的编译器检查和已验证模板决定语法;参考中没有的调用先查官方对应版本源码,不能按其它语言习惯杜撰方法。
|
||||
只加载当前任务相关参考。完整项目与常用签名随此 skill 一起提供,不需要源码 checkout。当前版本的编译器检查和已验证资产决定语法;索引未覆盖的能力再查官方对应版本源码,不能按其它语言习惯杜撰方法。
|
||||
|
||||
## 必须遵守的应用边界
|
||||
## 必须遵守
|
||||
|
||||
- 默认只实现当前接口与语法。除非用户明确要求,禁止加旧参数、旧接口、旧配置的兼容分支、别名或备用实现。改接口时同步本项目调用方,删除本次废弃路径。项目锁定编译器版本不等于保留兼容代码。
|
||||
- 配置只在 `config/setting.json`;不通过环境变量读取配置、密码、数据库 URL 或选择工具链。不要把 Dever 的官方仓库地址、更新配置写入业务项目。
|
||||
- 业务数据放 Model;用户、租户与权限来自可信上下文。不按特殊账号、租户或记录的 ID 开业务特例,不把测试数据当生产兜底。正常字面量与稳定业务常量可以保留。
|
||||
- 应用目录是 `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 拼装等碎片函数。
|
||||
@@ -46,6 +51,6 @@ Dever 是独立语言。应用源码使用 `.dever` 或 `.dever.md`,不沿用
|
||||
2. 实施时以完整业务职责组织文件和函数。抽取须有业务含义、明确不变量、实际复用或降低复杂流程理解成本的作用;不按行数或函数个数机械拆分。只清理本次引入或废弃的重复与死代码,保留无关改动。
|
||||
3. 完成后审查新增调用链、公开合同、配置与可信身份来源,检查本次引入的诊断并执行范围内验证。禁止假调用、吞错、假成功、擅自修改接口基线或弱化测试来通过检查;详见开发指引。
|
||||
|
||||
`fmt --check`、`check`、`test` 和 `run/build` 的覆盖范围不同。`check` 与 `run/build` 共用应用入口可达性和冗余硬检查,保持静态、离线,不执行应用;具体见 [验证范围](references/development.md#验证范围)。`dever test` 没有筛选参数,已有大项目不擅自全量测试;新模板的小套件可完整验证。已有授权覆盖的验证直接执行,启动服务、接真实数据库或更改系统安装须遵守用户的范围限制。
|
||||
`fmt --check` 查格式,`check` 查源码、配置及无法从业务入口调用到的代码,`test` 执行测试。具体见 [验证范围](references/development.md#验证范围)。已有项目优先 `dever test <root> --filter component.domain.case`;筛选仍检查全部源码,每例默认 60 秒,可用 `--timeout-ms` 调整。执行已授权的检查;启动服务、接真实数据库或改系统安装遵守用户限制。
|
||||
|
||||
交付说明实际改动、验证结果与未验证边界。规则写入 skill 不等于新增编译器检查,能编译不等于业务验收、性能验收或已发布。
|
||||
@@ -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 @@
|
||||
rest model
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 -->
|
||||
@@ -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 或项目配置。
|
||||
|
||||
|
||||
@@ -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 目标。
|
||||
+4
-2
@@ -68,11 +68,13 @@ python3 -B test/ci/run.py run --stage sandbox
|
||||
python3 -B test/ci/run.py run --stage bounded-perf
|
||||
```
|
||||
|
||||
以上命令由同一受限托管进程串行执行。若 author 改变输入,应在作者准备完成后再跑正式静态/default/all-feature门;较早输入版本的证据会显示过期。每阶段写入新的独占时间目录,开始即记录 running;缺输入记录 blocked,失败命令保留 exit/count/日志与原始异常。`summary` 与 inventory 消费者只接受最新一次尝试的当前源码/输入证据,不能越过较新的失败或中断回用旧 pass;所有必选阶段通过才返回0。源码摘要覆盖未提交文件,不只看 Git HEAD。
|
||||
以上命令由同一受限托管进程串行执行。若 author 改变输入,应在作者准备完成后再跑正式静态/default/all-feature门;较早输入版本的证据会显示过期。每阶段写入新的独占时间目录,开始即记录 running;缺输入记录 blocked,失败命令保留 exit/count/日志与原始异常。`summary` 与 inventory 消费者只接受最新一次尝试的当前源码/输入证据,不能越过较新的失败或中断回用旧 pass;所有必选阶段通过才返回0。源码摘要覆盖未提交文件及 `skills/` 的指引、模板和示例,不只看 Git HEAD;修改或删除这些文件会使旧结果失效。
|
||||
|
||||
default/all-features 由实际 Cargo metadata 注册 target 和已解析 feature 形成串行矩阵,覆盖 lib/bin/integration/doctest;default 显式保留 workspace 解析的 package features,最小 feature 阶段另按单 package 验证。只有显式启用 `remove_test_executables` 时才删除 Cargo 本次返回、`profile.test=true` 且位于 target 下 `deps/` 的测试可执行文件,失败阶段同样清理;不删除作者工具、静态 init、依赖 archive 或整个 target。删除对象在日志 JSON 中可审计,重跑 Cargo 可恢复。inventory 按 package/target/name 三元组核对完整性,跨 target 同名用例不能代替缺失项。
|
||||
|
||||
Python 阶段包含 `test/ci`、`test/performance`、`test/ecosystem-release` 三个目录;后两者必须已配置 `performance.network_peer/live_peer`,真实自有 loopback检查由主代理执行。三 SDK 路径全部必需,三个明确成功标志全部出现才通过。
|
||||
Python 阶段包含 `test/ci`、`test/performance`、`test/ecosystem-release` 三个目录,并执行 `test/skill_examples.py`,检查 skill 安装后的本地链接、文件预算、标准库签名和教程代码与源码的一致性。教程代码块须链接到受检查项目中的唯一源码片段;文档或源码漂移都会失败。performance 测试必须已配置 `performance.network_peer/live_peer`,真实自有 loopback 检查由主代理执行。三 SDK 路径全部必需,三个明确成功标志全部出现才通过。
|
||||
|
||||
只验证本次 CI 或 skill 改动时,可运行 `python3 -B test/ci/test_gate.py -v` 和 `python3 -B test/skill_examples.py -v`,不需要启动完整 CI、数据库或压力测试。
|
||||
|
||||
PostgreSQL 只接受显式 loopback端口和恰好一个 `{case}` 库名模板,拒绝 query 覆盖 host/dbname 等。增加 `performance.linux_ci.owned_postgres: {"data_directory":"/absolute/owned-cluster","port":51234}`,由托管者创建所需基库并在 finally 停止该自有实例;gate复用现有用例的 schema/tenant 清理,不替用户启动或连接既有业务数据库。
|
||||
|
||||
|
||||
+3
-2
@@ -20,7 +20,7 @@ STAGES = ("static", "default", "all-features", "features", "python", "sdk",
|
||||
"inventory", "author", "native", "postgres", "sandbox", "bounded-perf",
|
||||
"cms-64", "cms-128", "http2-64", "http2-128", "recovery-64", "recovery-128",
|
||||
"live-64", "live-128")
|
||||
SOURCE_ROOTS = ("crates", "test", "sdk", "library", "examples", ".cargo")
|
||||
SOURCE_ROOTS = ("crates", "test", "sdk", "library", "examples", "skills", ".cargo")
|
||||
|
||||
|
||||
class MissingInput(Exception):
|
||||
@@ -129,7 +129,8 @@ def command_plan(stage, config):
|
||||
python = str(absolute_file(tools.get("python"), "performance.linux_ci.tools.python"))
|
||||
if stage == "python":
|
||||
return [[python, "-B", "-m", "unittest", "discover", "-s", directory, "-p", "test_*.py", "-v"]
|
||||
for directory in ("test/ci", "test/performance", "test/ecosystem-release")]
|
||||
for directory in ("test/ci", "test/performance", "test/ecosystem-release")] + [
|
||||
[python, "-B", "test/skill_examples.py", "-v"]]
|
||||
if stage == "sdk":
|
||||
return [[python, "-B", "test/external-sdk/check.py", "--python", python,
|
||||
"--node", str(absolute_file(tools.get("node"), "performance.linux_ci.tools.node")),
|
||||
|
||||
@@ -5,6 +5,7 @@ import contextlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -177,6 +178,81 @@ class GateTests(unittest.TestCase):
|
||||
(root / "Cargo.lock").write_text("changed immutable input")
|
||||
self.assertNotEqual(author_before, gate.input_fingerprint({}, config, "author"))
|
||||
|
||||
def test_skill_sources_invalidate_evidence_but_generated_caches_do_not(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
for name in ("Cargo.toml", "Cargo.lock"):
|
||||
(root / name).write_text(name)
|
||||
sources = ("skills/assets/template/AGENTS.md", "skills/references/development.md",
|
||||
"skills/assets/examples/jobs/module/report/archive/app.dever")
|
||||
for name in sources:
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("original")
|
||||
original = gate.source_fingerprint(root)
|
||||
for name in sources:
|
||||
path = root / name
|
||||
with self.subTest(source=name):
|
||||
path.write_text("changed")
|
||||
self.assertNotEqual(original, gate.source_fingerprint(root))
|
||||
path.unlink()
|
||||
self.assertNotEqual(original, gate.source_fingerprint(root))
|
||||
path.write_text("original")
|
||||
self.assertEqual(original, gate.source_fingerprint(root))
|
||||
for cache in ("__pycache__", "target", ".git"):
|
||||
path = root / "skills/assets/examples/jobs" / cache / "generated"
|
||||
path.parent.mkdir()
|
||||
path.write_text("generated bytes")
|
||||
self.assertEqual(original, gate.source_fingerprint(root))
|
||||
path.write_text("changed generated bytes")
|
||||
self.assertEqual(original, gate.source_fingerprint(root))
|
||||
path.unlink()
|
||||
self.assertEqual(original, gate.source_fingerprint(root))
|
||||
|
||||
def test_python_plan_runs_real_skill_checks_and_rejects_each_failure(self):
|
||||
config = {"tools": {"cargo": sys.executable, "python": sys.executable}}
|
||||
commands = gate.command_plan("python", config)
|
||||
skill_commands = [command for command in commands if "test/skill_examples.py" in command]
|
||||
self.assertEqual(len(skill_commands), 1)
|
||||
command = skill_commands[0]
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / "test").mkdir()
|
||||
shutil.copyfile(gate.ROOT / "test/skill_examples.py", root / "test/skill_examples.py")
|
||||
for name in ("skills", "library"):
|
||||
shutil.copytree(gate.ROOT / name, root / name)
|
||||
logs = root / "logs"
|
||||
logs.mkdir()
|
||||
with patch.object(gate, "ROOT", root):
|
||||
record, content = gate.run_command(command, logs, 0, 10)
|
||||
gate.check_output("python", record, content)
|
||||
test_count = record["python_tests"]
|
||||
self.assertGreater(test_count, 0)
|
||||
for number, (relative, broken, case) in enumerate((
|
||||
("skills/SKILL.md", "[broken](missing.md)",
|
||||
"test_relocated_skill_links_and_release_file_limits"),
|
||||
("skills/references/signatures.md", "```signatures\ndever.io.missing() ()\n```\n",
|
||||
"test_signature_index_matches_public_library_declarations"),
|
||||
("skills/references/language.md",
|
||||
(root / "skills/references/language.md").read_text().replace(
|
||||
'"Hello, "', '"Incorrect tutorial, "', 1
|
||||
),
|
||||
"test_tutorial_code_matches_relocated_checked_sources"),
|
||||
), 1):
|
||||
path = root / relative
|
||||
original = path.read_bytes()
|
||||
path.write_text(broken)
|
||||
with self.subTest(check=case), self.assertRaisesRegex(RuntimeError, "command exited 1"):
|
||||
gate.run_command(command, logs, number, 10)
|
||||
result = json.loads((logs / f"{number:03d}.command.json").read_text())
|
||||
self.assertEqual(result["exit_code"], 1)
|
||||
self.assertEqual(result["python_tests"], test_count)
|
||||
log = (logs / f"{number:03d}.log").read_text()
|
||||
self.assertTrue(
|
||||
f"FAIL: {case}" in log or f"ERROR: {case}" in log, log
|
||||
)
|
||||
path.write_bytes(original)
|
||||
|
||||
def test_cleanup_cannot_delete_author_binary(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
|
||||
@@ -151,6 +151,12 @@ fn application_check_rejects_redundancy_before_preparing_build_resources() {
|
||||
] {
|
||||
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
|
||||
}
|
||||
assert_eq!(diagnostic.matches("error[C016]").count(), 3, "{diagnostic}");
|
||||
assert!(!diagnostic.contains("warning[W005]"), "{diagnostic}");
|
||||
assert!(
|
||||
diagnostic.contains("sample/profile/domain.dever:1:1: note:"),
|
||||
"{diagnostic}"
|
||||
);
|
||||
// The same source fails before native packs, output publication or execution.
|
||||
let executable = root.join("must-not-build");
|
||||
for (action, args) in [
|
||||
@@ -166,6 +172,120 @@ fn application_check_rejects_redundancy_before_preparing_build_resources() {
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_rejects_proven_duplicates_before_runtime_or_database_access() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
for (domain, app, diagnostic, location, warning) in [
|
||||
(
|
||||
"first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) pure { answer = renamed + 1 }",
|
||||
"total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
|
||||
"same implementation and contract",
|
||||
"sample/count/domain.dever:2:1: error[C016]",
|
||||
"warning[W003]",
|
||||
),
|
||||
(
|
||||
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
||||
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nsecond = domain.calculate(value)\nanswer = first + second }",
|
||||
"earlier result is still available",
|
||||
"sample/count/app.dever:2:",
|
||||
"warning[W004]",
|
||||
),
|
||||
] {
|
||||
let project_dir = TemporaryDirectory::new();
|
||||
let root = project_dir.path();
|
||||
write(root, "config/setting.json", "{}");
|
||||
write(root, "module/sample/count/model.dever", "type Count { value: Int }");
|
||||
write(root, "module/sample/count/domain.dever", domain);
|
||||
write(root, "module/sample/count/app.dever", app);
|
||||
write(root, "module/sample/count/api.dever", "cmd total = app.total");
|
||||
let checked = cli(&compiler, root, "check", &[]);
|
||||
for expected in ["C016", diagnostic, location, "previous implementation or computation"] {
|
||||
failure(&checked, expected);
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&checked.stderr);
|
||||
assert_eq!(stderr.matches("error[C016]").count(), 1, "{stderr}");
|
||||
assert!(!stderr.contains(warning), "{stderr}");
|
||||
assert_eq!(
|
||||
stderr.matches("note: previous implementation or computation").count(),
|
||||
1,
|
||||
"{stderr}"
|
||||
);
|
||||
let executable = root.join("must-not-build");
|
||||
for (action, arguments) in [
|
||||
("run", vec!["--", "sample.count.total", "{\"value\":1}"]),
|
||||
("build", vec!["--output", executable.to_str().unwrap()]),
|
||||
] {
|
||||
let rejected = cli(&compiler, root, action, &arguments);
|
||||
assert!(!rejected.status.success());
|
||||
assert_eq!(rejected.stderr, checked.stderr);
|
||||
}
|
||||
let snapshot = cli(&compiler, root, "api", &[]);
|
||||
success(&snapshot);
|
||||
let stderr = String::from_utf8_lossy(&snapshot.stderr);
|
||||
assert!(stderr.contains(warning), "{stderr}");
|
||||
assert!(!stderr.contains("C016"), "{stderr}");
|
||||
write(
|
||||
root,
|
||||
"test/sample/count/value.dever",
|
||||
"value() () { assert_eq(app.total(1), 4) }",
|
||||
);
|
||||
let tests = cli(&compiler, root, "test", &[]);
|
||||
failure(&tests, "runtime pack");
|
||||
let stderr = String::from_utf8_lossy(&tests.stderr);
|
||||
assert!(stderr.contains(warning), "{stderr}");
|
||||
assert!(!stderr.contains("C016"), "{stderr}");
|
||||
write(root, "dever.api", "invalid baseline");
|
||||
let baseline = cli(&compiler, root, "check", &[]);
|
||||
failure(&baseline, "invalid API baseline");
|
||||
let stderr = String::from_utf8_lossy(&baseline.stderr);
|
||||
assert!(stderr.contains(warning), "{stderr}");
|
||||
assert!(!stderr.contains("C016"), "{stderr}");
|
||||
assert!(!executable.exists());
|
||||
assert!(!root.join("data").exists());
|
||||
}
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_keeps_warnings_with_distinct_evidence_and_other_codes() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
let project_dir = TemporaryDirectory::new();
|
||||
let root = project_dir.path();
|
||||
write(root, "config/setting.json", "{}");
|
||||
write(
|
||||
root,
|
||||
"module/sample/count/domain.dever",
|
||||
"orphan(value: Int) (answer: Int) pure { answer = value + 1 }\nfirst(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(value: Int) (answer: Int) pure { answer = value + 1 }",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/sample/count/app.dever",
|
||||
"total(value: Int) (answer: Int) { unused = 0\nfirst = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
|
||||
);
|
||||
write(root, "module/sample/count/api.dever", "cmd total = app.total");
|
||||
let checked = cli(&compiler, root, "check", &[]);
|
||||
failure(&checked, "C016");
|
||||
let stderr = String::from_utf8_lossy(&checked.stderr);
|
||||
assert_eq!(stderr.matches("error[C016]").count(), 2, "{stderr}");
|
||||
// Warning proof includes the orphan; production proof starts at the first reachable function.
|
||||
assert_eq!(stderr.matches("warning[W003]").count(), 2, "{stderr}");
|
||||
assert_eq!(stderr.matches("warning[W001]").count(), 1, "{stderr}");
|
||||
assert_eq!(
|
||||
stderr.matches("sample/count/domain.dever:1:1: note:").count(),
|
||||
2,
|
||||
"{stderr}"
|
||||
);
|
||||
assert_eq!(
|
||||
stderr.matches("sample/count/domain.dever:2:1: note:").count(),
|
||||
1,
|
||||
"{stderr}"
|
||||
);
|
||||
assert!(!root.join("data").exists());
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_rejects_plain_model_get_wrapper_without_opening_database() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
@@ -303,6 +423,68 @@ fn application_check_does_not_apply_production_reachability_to_tests() {
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_still_checks_unselected_sources_and_reports_no_match_before_compilation() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
let project_dir = TemporaryDirectory::new();
|
||||
let root = project_dir.path();
|
||||
project(root);
|
||||
write(root, "test/sample/echo/first.dever", "first() () { assert_eq(1, 1) }");
|
||||
write(root, "test/sample/echo/second.dever", "second() () { assert_eq(1, 1) }");
|
||||
for filter in ["sam", "sample.ec", "sample.echo.fir", "sample.echo.missing"] {
|
||||
let result = cli(&compiler, root, "test", &["--filter", filter]);
|
||||
failure(&result, "no tests match filter");
|
||||
assert!(!String::from_utf8_lossy(&result.stdout).contains("runtime pack"));
|
||||
}
|
||||
let selected = cli(&compiler, root, "test", &["--filter", "sample.echo.second"]);
|
||||
failure(&selected, "runtime pack");
|
||||
let report = String::from_utf8_lossy(&selected.stdout);
|
||||
assert!(report.contains("0 passed; 1 failed; 1 not run"), "{report}");
|
||||
assert!(!report.contains("test sample/echo/first ..."), "{report}");
|
||||
write(root, "test/sample/echo/first.dever", "first() () { missing() }");
|
||||
failure(&cli(&compiler, root, "test", &["--filter", "sample.echo.second"]), "C004");
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an explicitly prepared runtime pack adjacent to CARGO_BIN_EXE_dever; owned short-lived cases only"]
|
||||
fn llvm_test_controls_preserve_indices_prefix_boundaries_and_timeout_continuation() {
|
||||
let compiler = Path::new(env!("CARGO_BIN_EXE_dever"));
|
||||
let directory = TemporaryDirectory::new();
|
||||
let root = directory.path();
|
||||
project(root);
|
||||
for (relative, source) in [
|
||||
("test/sample/echo/alpha.dever", "alpha() () { assert_eq(1, 2) }"),
|
||||
("test/sample/echo/beta.dever", "beta() () { assert_eq(7, 7) }"),
|
||||
("test/sample/echo/slow.dever", "slow() () { dever.task.sleep(30000) }"),
|
||||
("test/sample/echo/zeta.dever", "zeta() () { assert_eq(9, 9) }"),
|
||||
("test/sample/echoes/first.dever", "first() () { assert_eq(1, 2) }"),
|
||||
("test/samples/echo/first.dever", "first() () { assert_eq(1, 2) }"),
|
||||
] {
|
||||
write(root, relative, source);
|
||||
}
|
||||
let selected = cli(compiler, root, "test", &["--filter", "sample.echo.beta"]);
|
||||
success(&selected);
|
||||
let report = String::from_utf8_lossy(&selected.stdout);
|
||||
assert!(report.contains("test sample/echo/beta ... ok"), "{report}");
|
||||
assert!(report.contains("1 passed; 0 failed; 5 not run"), "{report}");
|
||||
assert!(!report.contains("sample/echo/alpha"), "{report}");
|
||||
|
||||
for (filter, counts) in [
|
||||
("sample.echo", "2 passed; 2 failed; 2 not run"),
|
||||
("sample", "2 passed; 3 failed; 1 not run"),
|
||||
] {
|
||||
let selected = cli(compiler, root, "test", &["--filter", filter, "--timeout-ms", "250"]);
|
||||
failure(&selected, counts);
|
||||
let report = String::from_utf8_lossy(&selected.stdout);
|
||||
assert!(report.contains("test process timed out after 250 ms"), "{report}");
|
||||
assert!(report.contains("test sample/echo/zeta ... ok"), "{report}");
|
||||
assert!(!report.contains("test samples/echo/first"), "{report}");
|
||||
}
|
||||
failure(&cli(compiler, root, "test", &["--filter", "sample.echo.bet"]), "no tests match filter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_build_requires_the_selected_pack_even_when_host_pack_exists() {
|
||||
let root = TemporaryDirectory::new();
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
use super::*;
|
||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||
use std::process::Output;
|
||||
|
||||
struct Project {
|
||||
temporary: TemporaryDirectory,
|
||||
layout: Layout,
|
||||
root: PathBuf,
|
||||
key: Ed25519KeyPair,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
fn new() -> Self {
|
||||
let temporary = TemporaryDirectory::new();
|
||||
let layout = Layout::new(temporary.path().join("machine"));
|
||||
layout.initialize().unwrap();
|
||||
let key = signing_key();
|
||||
fs::write(
|
||||
layout.state().join("trusted-release-key"),
|
||||
format!("{}\n", hex(key.public_key().as_ref())),
|
||||
)
|
||||
.unwrap();
|
||||
write_release(
|
||||
&layout,
|
||||
&key,
|
||||
"1.0.0",
|
||||
&temporary.path().join("dispatch"),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
MachineManager::new(layout.clone())
|
||||
.install_requested("1.0.0")
|
||||
.unwrap();
|
||||
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
|
||||
let root = temporary.path().join("project");
|
||||
fs::create_dir_all(root.join("module")).unwrap();
|
||||
fs::create_dir(root.join("config")).unwrap();
|
||||
fs::write(root.join("config/setting.json"), "{}").unwrap();
|
||||
Self {
|
||||
temporary,
|
||||
layout,
|
||||
root,
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&self, root: &Path, extra: &[&str]) -> Output {
|
||||
Command::new(self.layout.bin().join("dever"))
|
||||
.args(["skill", "install"])
|
||||
.args(extra)
|
||||
.current_dir(root)
|
||||
.env_clear()
|
||||
.output()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn succeeds(&self) {
|
||||
let output = self.run(&self.root, &[]);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("AGENTS.md"));
|
||||
self.assert_no_staging();
|
||||
}
|
||||
|
||||
fn fails(&self, message: &str) {
|
||||
let output = self.run(&self.root, &[]);
|
||||
assert!(!output.status.success());
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains(message),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
self.assert_no_staging();
|
||||
}
|
||||
|
||||
fn assert_no_staging(&self) {
|
||||
assert!(fs::read_dir(&self.root).unwrap().all(|entry| {
|
||||
!entry
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with(".dever-skill-")
|
||||
}));
|
||||
}
|
||||
|
||||
fn sign_manifest(&self, manifest: &ReleaseManifest) {
|
||||
let release = self.layout.versions().join("1.0.0");
|
||||
let bytes = serde_json::to_vec(manifest).unwrap();
|
||||
fs::write(release.join("manifest.json"), &bytes).unwrap();
|
||||
fs::write(
|
||||
release.join("manifest.sig"),
|
||||
hex(self.key.sign(&bytes).as_ref()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_skill_preserves_user_bytes_permissions_and_updates_only_its_block() {
|
||||
let fixture = Project::new();
|
||||
let agents = fixture.root.join("AGENTS.md");
|
||||
let before = "# 项目规则\r\n保留用户内容,无末尾换行";
|
||||
fs::write(&agents, before).unwrap();
|
||||
fs::set_permissions(&agents, fs::Permissions::from_mode(0o640)).unwrap();
|
||||
fixture.succeeds();
|
||||
assert_eq!(
|
||||
fs::read_to_string(&agents).unwrap(),
|
||||
format!("{before}\n\n{}", project_rules("1.0.0"))
|
||||
);
|
||||
assert_eq!(
|
||||
fs::metadata(&agents).unwrap().permissions().mode() & 0o777,
|
||||
0o640
|
||||
);
|
||||
let installed = fs::read(&agents).unwrap();
|
||||
fixture.succeeds();
|
||||
assert_eq!(fs::read(&agents).unwrap(), installed);
|
||||
|
||||
let prefix = "# 自定义前文\r\n\r\n";
|
||||
let suffix = "\r\n# 自定义后文\r\n原样保留";
|
||||
fs::write(
|
||||
&agents,
|
||||
format!("{prefix}<!-- dever-language:start -->\n旧规则\n<!-- dever-language:end -->{suffix}"),
|
||||
)
|
||||
.unwrap();
|
||||
fixture.succeeds();
|
||||
assert_eq!(
|
||||
fs::read_to_string(&agents).unwrap(),
|
||||
format!("{prefix}{}{suffix}", project_rules("1.0.0").trim_end())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_skill_rejects_ambiguous_or_unsafe_agents_without_changes() {
|
||||
let fixture = Project::new();
|
||||
let agents = fixture.root.join("AGENTS.md");
|
||||
for content in [
|
||||
"<!-- dever-language:start -->",
|
||||
"<!-- dever-language:end -->",
|
||||
"<!-- dever-language:end --><!-- dever-language:start -->",
|
||||
"<!-- dever-language:start --><!-- dever-language:start --><!-- dever-language:end -->",
|
||||
"<!-- dever-language:start --><!-- dever-language:end --><!-- dever-language:start --><!-- dever-language:end -->",
|
||||
] {
|
||||
fs::write(&agents, content).unwrap();
|
||||
fixture.fails("exactly one complete");
|
||||
assert_eq!(fs::read_to_string(&agents).unwrap(), content);
|
||||
}
|
||||
fs::write(&agents, [0xff, 0xfe]).unwrap();
|
||||
fixture.fails("UTF-8");
|
||||
assert_eq!(fs::read(&agents).unwrap(), [0xff, 0xfe]);
|
||||
fs::write(&agents, "do not overwrite").unwrap();
|
||||
fs::set_permissions(&agents, fs::Permissions::from_mode(0o444)).unwrap();
|
||||
fixture.fails("read-only");
|
||||
fs::set_permissions(&agents, fs::Permissions::from_mode(0o644)).unwrap();
|
||||
fs::remove_file(&agents).unwrap();
|
||||
fs::create_dir(&agents).unwrap();
|
||||
fixture.fails("regular file");
|
||||
fs::remove_dir(&agents).unwrap();
|
||||
let other = fixture.temporary.path().join("other-agents");
|
||||
fs::write(&other, "untouched").unwrap();
|
||||
symlink(&other, &agents).unwrap();
|
||||
fixture.fails("regular file");
|
||||
assert!(fs::symlink_metadata(&agents).unwrap().is_symlink());
|
||||
assert_eq!(fs::read_to_string(&other).unwrap(), "untouched");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_skill_requires_project_root_valid_version_and_signed_template() {
|
||||
let fixture = Project::new();
|
||||
let agents = fixture.root.join("AGENTS.md");
|
||||
let output = fixture.run(&fixture.root, &["unwanted-directory"]);
|
||||
assert!(!output.status.success());
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("accepts no arguments"));
|
||||
assert!(!fixture.root.join("unwanted-directory").exists());
|
||||
let output = fixture.run(fixture.temporary.path(), &[]);
|
||||
assert!(!output.status.success());
|
||||
assert!(!fixture.temporary.path().join("AGENTS.md").exists());
|
||||
fs::remove_dir(fixture.root.join("module")).unwrap();
|
||||
symlink(fixture.temporary.path(), fixture.root.join("module")).unwrap();
|
||||
fixture.fails("real module/");
|
||||
fs::remove_file(fixture.root.join("module")).unwrap();
|
||||
fs::create_dir(fixture.root.join("module")).unwrap();
|
||||
let settings = fixture.root.join("config/setting.json");
|
||||
for invalid in [
|
||||
"not json",
|
||||
"[]",
|
||||
r#"{"dever":{"version":"latest"}}"#,
|
||||
r#"{"dever":{"version":"9.0.0"}}"#,
|
||||
] {
|
||||
fs::write(&settings, invalid).unwrap();
|
||||
let output = fixture.run(&fixture.root, &[]);
|
||||
assert!(!output.status.success());
|
||||
assert!(!agents.exists());
|
||||
}
|
||||
fs::remove_file(&settings).unwrap();
|
||||
fixture.fails("cannot read");
|
||||
fs::write(&settings, "{}").unwrap();
|
||||
let template = fixture
|
||||
.layout
|
||||
.versions()
|
||||
.join("1.0.0/skills/dever-language/assets/template/AGENTS.md");
|
||||
fs::write(&template, "tampered instructions").unwrap();
|
||||
let output = fixture.run(&fixture.root, &[]);
|
||||
assert!(!output.status.success());
|
||||
assert!(!agents.exists());
|
||||
fixture.assert_no_staging();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_skill_refuses_unsigned_extra_or_invalid_signed_template() {
|
||||
let fixture = Project::new();
|
||||
let agents = fixture.root.join("AGENTS.md");
|
||||
fs::write(&agents, "user-owned instructions").unwrap();
|
||||
let release = fixture.layout.versions().join("1.0.0");
|
||||
let mut manifest: ReleaseManifest =
|
||||
serde_json::from_slice(&fs::read(release.join("manifest.json")).unwrap()).unwrap();
|
||||
let logical = "skills/dever-language/assets/template/AGENTS.md";
|
||||
manifest.artifacts.retain(|entry| entry.path != logical);
|
||||
fixture.sign_manifest(&manifest);
|
||||
fixture.fails("does not declare resource");
|
||||
assert_eq!(
|
||||
fs::read_to_string(&agents).unwrap(),
|
||||
"user-owned instructions"
|
||||
);
|
||||
|
||||
let template = release.join(logical);
|
||||
for invalid in [
|
||||
"no managed block",
|
||||
"<!-- dever-language:start -->missing end",
|
||||
"outside\n<!-- dever-language:start -->rules<!-- dever-language:end -->",
|
||||
] {
|
||||
fs::write(&template, invalid).unwrap();
|
||||
manifest.artifacts.retain(|entry| entry.path != logical);
|
||||
manifest.artifacts.push(artifact(&template, logical));
|
||||
fixture.sign_manifest(&manifest);
|
||||
let output = fixture.run(&fixture.root, &[]);
|
||||
assert!(!output.status.success());
|
||||
assert_eq!(
|
||||
fs::read_to_string(&agents).unwrap(),
|
||||
"user-owned instructions"
|
||||
);
|
||||
fixture.assert_no_staging();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ static NEXT_TEMPORARY: AtomicU64 = AtomicU64::new(0);
|
||||
#[path = "shared_toolchain_extensions.rs"]
|
||||
mod extensions;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "project_skill.rs"]
|
||||
mod project_skill;
|
||||
|
||||
struct TemporaryDirectory(PathBuf);
|
||||
|
||||
impl TemporaryDirectory {
|
||||
@@ -306,30 +310,16 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
|
||||
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
|
||||
"skill 1.0.0\n"
|
||||
);
|
||||
let skill_entry = temporary.path().join("dever-language");
|
||||
execute_in(
|
||||
&layout,
|
||||
&[
|
||||
"skill".into(),
|
||||
"install".into(),
|
||||
skill_entry.as_os_str().into(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let loader = fs::read(skill_entry.join("SKILL.md")).unwrap();
|
||||
assert!(String::from_utf8_lossy(&loader).contains("skill path"));
|
||||
assert!(
|
||||
execute_in(
|
||||
&layout,
|
||||
&[
|
||||
"skill".into(),
|
||||
"install".into(),
|
||||
skill_entry.as_os_str().into()
|
||||
]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(fs::read(skill_entry.join("SKILL.md")).unwrap(), loader);
|
||||
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
|
||||
let active_project = temporary.path().join("active-project");
|
||||
fs::create_dir_all(active_project.join("module")).unwrap();
|
||||
fs::create_dir(active_project.join("config")).unwrap();
|
||||
fs::write(active_project.join("config/setting.json"), "{}").unwrap();
|
||||
install_project_skill(&layout, &active_project);
|
||||
let rules = fs::read(active_project.join("AGENTS.md")).unwrap();
|
||||
assert_eq!(rules, project_rules("1.0.0").as_bytes());
|
||||
install_project_skill(&layout, &active_project);
|
||||
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
|
||||
|
||||
let new_project = temporary.path().join("not-created-yet");
|
||||
let CommandResult::Exited(code) = execute_in(
|
||||
@@ -351,6 +341,7 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
|
||||
|
||||
let project = temporary.path().join("project");
|
||||
fs::create_dir_all(project.join("config")).unwrap();
|
||||
fs::create_dir(project.join("module")).unwrap();
|
||||
fs::write(
|
||||
project.join("config/setting.json"),
|
||||
r#"{"dever":{"version":"2.0.0"}}"#,
|
||||
@@ -368,6 +359,11 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
|
||||
Path::new(&pinned_skill),
|
||||
layout.versions().join("2.0.0/skills/dever-language")
|
||||
);
|
||||
install_project_skill(&layout, &project);
|
||||
assert_eq!(
|
||||
fs::read_to_string(project.join("AGENTS.md")).unwrap(),
|
||||
project_rules("2.0.0")
|
||||
);
|
||||
let CommandResult::Exited(code) = execute_in(&layout, &arguments).unwrap() else {
|
||||
panic!("project command must dispatch")
|
||||
};
|
||||
@@ -393,7 +389,12 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
|
||||
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
|
||||
"skill 2.0.0\n"
|
||||
);
|
||||
assert_eq!(fs::read(skill_entry.join("SKILL.md")).unwrap(), loader);
|
||||
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
|
||||
install_project_skill(&layout, &active_project);
|
||||
assert_eq!(
|
||||
fs::read_to_string(active_project.join("AGENTS.md")).unwrap(),
|
||||
project_rules("2.0.0")
|
||||
);
|
||||
fs::write(layout.downloads().join("latest"), "1.0.0\n").unwrap();
|
||||
assert!(
|
||||
manager
|
||||
@@ -1342,12 +1343,16 @@ fn write_release(
|
||||
let skill = root.join("skills/dever-language/SKILL.md");
|
||||
fs::create_dir_all(skill.parent().unwrap()).unwrap();
|
||||
fs::write(&skill, format!("skill {version}\n")).unwrap();
|
||||
let rules = root.join("skills/dever-language/assets/template/AGENTS.md");
|
||||
fs::create_dir_all(rules.parent().unwrap()).unwrap();
|
||||
fs::write(&rules, project_rules(version)).unwrap();
|
||||
let manifest = ReleaseManifest::new(
|
||||
Version::parse(version).unwrap(),
|
||||
vec![
|
||||
artifact(&core, "dever-core"),
|
||||
artifact(&runtime, "runtime.pack"),
|
||||
artifact(&skill, "skills/dever-language/SKILL.md"),
|
||||
artifact(&rules, "skills/dever-language/assets/template/AGENTS.md"),
|
||||
],
|
||||
);
|
||||
let bytes = serde_json::to_vec(&manifest).unwrap();
|
||||
@@ -1359,6 +1364,29 @@ fn write_release(
|
||||
fs::write(root.join("manifest.sig"), format!("{}\n", hex(&signature))).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn project_rules(version: &str) -> String {
|
||||
include_str!("../../skills/assets/template/AGENTS.md").replace(
|
||||
"<!-- dever-language:end -->",
|
||||
&format!("<!-- fixture version {version} -->\n<!-- dever-language:end -->"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn install_project_skill(layout: &Layout, project: &Path) {
|
||||
let output = Command::new(layout.bin().join("dever"))
|
||||
.args(["skill", "install"])
|
||||
.current_dir(project)
|
||||
.env_clear()
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn artifact(path: &Path, relative: &str) -> Artifact {
|
||||
let bytes = fs::read(path).unwrap();
|
||||
Artifact {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
use super::{Action, OsString, arguments_for};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_options_default_to_sixty_seconds_and_accept_either_order() {
|
||||
for options in [
|
||||
vec![],
|
||||
vec!["--filter", "sample.account.case_2"],
|
||||
vec!["--filter", "sample.account", "--timeout-ms", "15"],
|
||||
vec!["--timeout-ms", "15", "--filter", "sample"],
|
||||
vec!["--timeout-ms", "18446744073709551615"],
|
||||
] {
|
||||
let arguments: Vec<_> = [vec!["test", "project"], options.clone()]
|
||||
.concat()
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect();
|
||||
let (root, action) = arguments_for(&arguments).unwrap();
|
||||
assert_eq!(root.to_str(), Some("project"));
|
||||
let Action::Test(parsed) = action else {
|
||||
panic!("expected test")
|
||||
};
|
||||
let timeout = options
|
||||
.iter()
|
||||
.position(|option| *option == "--timeout-ms")
|
||||
.map(|index| options[index + 1].parse::<u64>().unwrap())
|
||||
.unwrap_or(60_000);
|
||||
assert_eq!(parsed.timeout, Duration::from_millis(timeout));
|
||||
assert_eq!(
|
||||
parsed.filter.as_deref(),
|
||||
options
|
||||
.iter()
|
||||
.position(|option| *option == "--filter")
|
||||
.map(|index| options[index + 1]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_options_reject_invalid_missing_duplicate_and_unknown_arguments() {
|
||||
let mut invalid = vec![
|
||||
vec!["--filter"],
|
||||
vec!["--timeout-ms"],
|
||||
vec!["--filter", "--timeout-ms", "20"],
|
||||
vec!["--filter", "sample", "--filter", "sample.account"],
|
||||
vec!["--timeout-ms", "20", "--timeout-ms", "30"],
|
||||
vec!["--unknown", "value"],
|
||||
vec!["extra"],
|
||||
];
|
||||
for filter in [
|
||||
"", ".sample", "sample.", "sample..case", "sample.a.b.c", "sam*", "sample/account",
|
||||
"Sample", "sample.__case", "sample.case_", "sample.2case", " sample", "用户",
|
||||
] {
|
||||
invalid.push(vec!["--filter", filter]);
|
||||
}
|
||||
for timeout in [
|
||||
"", "0", "-1", "+1", "1.5", "1ms", " 1", "18446744073709551616",
|
||||
] {
|
||||
invalid.push(vec!["--timeout-ms", timeout]);
|
||||
}
|
||||
for options in invalid {
|
||||
let arguments: Vec<_> = [vec!["test", "project"], options]
|
||||
.concat()
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect();
|
||||
assert!(arguments_for(&arguments).is_err(), "accepted {arguments:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_options_reject_non_utf8_values() {
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
for option in ["--filter", "--timeout-ms"] {
|
||||
let arguments = [
|
||||
OsString::from("test"),
|
||||
OsString::from("project"),
|
||||
OsString::from(option),
|
||||
OsString::from_vec(vec![0xff]),
|
||||
];
|
||||
assert!(arguments_for(&arguments).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use super::{TemporaryProject, process};
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const DESCENDANTS: &str = r#"
|
||||
/bin/sh -c 'echo $$ > descendant; /bin/sleep 30 & echo $! > grandchild; wait' &
|
||||
while [ ! -s grandchild ]; do :; done
|
||||
"#;
|
||||
|
||||
// subreaper 是进程级状态;此单例测试顺序覆盖所有真实进程场景。
|
||||
#[test]
|
||||
fn case_process_captures_output_times_out_and_reaps_only_owned_descendants() {
|
||||
let original_reaper = rustix::process::child_subreaper().unwrap();
|
||||
let mut unrelated = Command::new("/bin/sleep").arg("30").spawn().unwrap();
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
for (body, descendants, expected) in [
|
||||
("printf hidden; printf hidden-error >&2; exit 0", false, None),
|
||||
(
|
||||
"printf output; printf diagnostic >&2; exit 7",
|
||||
false,
|
||||
Some("exit status: 7"),
|
||||
),
|
||||
(
|
||||
"printf before-timeout; /bin/sleep 30",
|
||||
false,
|
||||
Some("timed out after 100 ms"),
|
||||
),
|
||||
("exit 0", true, None),
|
||||
("printf owned-error >&2; exit 7", true, Some("exit status: 7")),
|
||||
("printf started; wait", true, Some("timed out after 100 ms")),
|
||||
] {
|
||||
let root = TemporaryProject::new().unwrap();
|
||||
let started = Instant::now();
|
||||
let script = if descendants {
|
||||
format!("{DESCENDANTS}\n{body}")
|
||||
} else {
|
||||
body.to_owned()
|
||||
};
|
||||
let result = process::run(
|
||||
Command::new("/bin/sh")
|
||||
.arg("-c")
|
||||
.arg(&script)
|
||||
.env_clear()
|
||||
.current_dir(root.path()),
|
||||
root.path(),
|
||||
Duration::from_millis(100),
|
||||
);
|
||||
assert!(started.elapsed() < Duration::from_secs(5));
|
||||
match expected {
|
||||
None => result.unwrap(),
|
||||
Some(message) => {
|
||||
let failure = result.unwrap_err();
|
||||
assert!(failure.message.contains(message), "{failure:?}");
|
||||
if script.contains("printf output") {
|
||||
assert_eq!(failure.stdout, b"output");
|
||||
assert_eq!(failure.stderr, b"diagnostic");
|
||||
}
|
||||
if script.contains("printf before-timeout") {
|
||||
assert_eq!(failure.stdout, b"before-timeout");
|
||||
}
|
||||
if script.contains("printf owned-error") {
|
||||
assert_eq!(failure.stderr, b"owned-error");
|
||||
}
|
||||
}
|
||||
}
|
||||
for name in ["descendant", "grandchild"] {
|
||||
let path = root.path().join(name);
|
||||
if descendants {
|
||||
let pid = fs::read_to_string(path)
|
||||
.unwrap()
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.unwrap();
|
||||
assert!(
|
||||
!std::path::Path::new(&format!("/proc/{pid}")).exists(),
|
||||
"unreaped {name} {pid}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper);
|
||||
}
|
||||
|
||||
let root = TemporaryProject::new().unwrap();
|
||||
let failure = process::run(
|
||||
Command::new("/bin/sh")
|
||||
.arg("-c")
|
||||
.arg("/bin/dd if=/dev/zero bs=131072 count=2 2>/dev/null; printf end >&2; exit 9")
|
||||
.env_clear(),
|
||||
root.path(),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(failure.message.contains("exit status: 9"));
|
||||
assert_eq!(failure.stdout.len(), 262_144);
|
||||
assert_eq!(failure.stderr, b"end");
|
||||
|
||||
let root = TemporaryProject::new().unwrap();
|
||||
let failure = process::run(
|
||||
&mut Command::new(root.path().join("missing-executable")),
|
||||
root.path(),
|
||||
Duration::from_millis(100),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(failure.message.contains("cannot run test process"));
|
||||
assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper);
|
||||
});
|
||||
// 即使断言失败也回收此对照进程,不能让测试自身遗留资源。
|
||||
let unrelated_running = unrelated.try_wait().unwrap().is_none();
|
||||
let _ = unrelated.kill();
|
||||
unrelated.wait().unwrap();
|
||||
result.unwrap();
|
||||
assert!(unrelated_running, "case cleanup affected an unrelated child");
|
||||
}
|
||||
@@ -86,6 +86,10 @@ path = "tests/markdown_source.rs"
|
||||
name = "cms_acceptance"
|
||||
path = "tests/cms_acceptance.rs"
|
||||
|
||||
[[test]]
|
||||
name = "skill_examples"
|
||||
path = "tests/skill_examples.rs"
|
||||
|
||||
[[test]]
|
||||
name = "native_cache"
|
||||
path = "tests/native_cache.rs"
|
||||
|
||||
@@ -2,6 +2,16 @@ mod support;
|
||||
|
||||
use support::{checked, sources};
|
||||
|
||||
fn application(files: &[(&str, &str)]) -> dever_core::hir::Program {
|
||||
let root = support::temp::TemporaryDirectory::new();
|
||||
for (relative, source) in files {
|
||||
let path = root.path().join("module").join(relative);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, source).unwrap();
|
||||
}
|
||||
checked(&dever_core::source::SourceMap::load(&root.path().join("module")).unwrap())
|
||||
}
|
||||
|
||||
fn warnings(source: &str, code: &str) -> Vec<dever_core::diagnostic::Diagnostic> {
|
||||
let sources = sources(source);
|
||||
let user_source = sources.files()[0].id();
|
||||
@@ -99,3 +109,122 @@ fn existing_unused_write_warning_is_not_duplicated_as_dead_computation() {
|
||||
.any(|warning| warning.code == "W006")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_rejects_reachable_private_copies_with_renamed_inputs() {
|
||||
let program = application(&[
|
||||
("sample/count/domain.dever", "first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) pure { answer = renamed + 1 }"),
|
||||
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }"),
|
||||
("sample/count/api.dever", "cmd total = app.total"),
|
||||
]);
|
||||
let warnings: Vec<_> = program.warnings().iter().filter(|item| item.code == "W003").collect();
|
||||
let errors = program.application_errors().unwrap();
|
||||
assert_eq!(errors.len(), 1, "{errors:?}");
|
||||
assert_eq!(errors[0].code, "C016");
|
||||
assert!(errors[0].message.contains("same implementation and contract"));
|
||||
assert!(errors[0].message.contains("domain.first"));
|
||||
assert!(errors[0].message.contains("domain.second"));
|
||||
assert_eq!(errors[0].related.len(), 1);
|
||||
assert!(warnings.iter().any(|warning| warning.primary == errors[0].primary));
|
||||
assert_ne!(errors[0].primary, errors[0].related[0].span);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_rejects_repeated_pure_calls_using_the_warning_proof() {
|
||||
let program = application(&[
|
||||
("sample/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
|
||||
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.calculate(value)\nsecond = domain.calculate(value)\nanswer = first + second }"),
|
||||
("sample/count/api.dever", "cmd total = app.total"),
|
||||
]);
|
||||
let errors = program.application_errors().unwrap();
|
||||
assert_eq!(errors.len(), 1, "{errors:?}");
|
||||
assert_eq!(errors[0].code, "C016");
|
||||
assert!(errors[0].message.contains("earlier result is still available"));
|
||||
assert_eq!(errors[0].related.len(), 1);
|
||||
assert!(program.warnings().iter().any(|warning| {
|
||||
warning.code == "W004" && warning.primary == errors[0].primary
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_does_not_reject_changed_inputs_results_or_effectful_calls() {
|
||||
for (app, domain, api) in [
|
||||
(
|
||||
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nvalue = value + 1\nsecond = domain.calculate(value)\nanswer = first + second }",
|
||||
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
||||
"cmd total = app.total",
|
||||
),
|
||||
(
|
||||
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nfirst = 0\nsecond = domain.calculate(value)\nanswer = first + second }",
|
||||
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
||||
"cmd total = app.total",
|
||||
),
|
||||
(
|
||||
"type Counter { value: Int }\ntotal(counter: Counter) (answer: Int) { first = domain.calculate(counter.value)\ncounter.value = counter.value + 1\nsecond = domain.calculate(counter.value)\nanswer = first + second }",
|
||||
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
||||
"cmd total = app.total",
|
||||
),
|
||||
(
|
||||
"noisy(value: Int) (answer: Int) { dever.io.println(\"called\")\nanswer = value }\ntotal(value: Int) (answer: Int) { first = noisy(value)\nsecond = noisy(value)\nanswer = first + second }",
|
||||
"",
|
||||
"cmd total = app.total",
|
||||
),
|
||||
] {
|
||||
let program = application(&[
|
||||
("sample/count/app.dever", app),
|
||||
("sample/count/domain.dever", domain),
|
||||
("sample/count/api.dever", api),
|
||||
]);
|
||||
let errors = program.application_errors().unwrap();
|
||||
assert!(errors.is_empty(), "{app}: {errors:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_preserves_different_contracts_and_owners() {
|
||||
for domain in [
|
||||
"first(value: Int) (answer: Int) pure { answer = value }\nsecond(value: Int) (different: Int) pure { different = value }",
|
||||
"first(value: Int) (answer: Int) pure recover(\"first policy\") { answer = value }\nsecond(value: Int) (answer: Int) pure recover(\"second policy\") { answer = value }",
|
||||
"first() (answer: Int >= 1) pure { answer = 1 }\nsecond() (answer: Int >= 0) pure { answer = 1 }",
|
||||
] {
|
||||
let inputs = if domain.contains("value:") { "1" } else { "" };
|
||||
let app = format!("total() (answer: Int) {{ first = domain.first({inputs})\nsecond = domain.second({inputs})\nanswer = first + second }}");
|
||||
let program = application(&[
|
||||
("sample/count/domain.dever", domain),
|
||||
("sample/count/app.dever", &app),
|
||||
("sample/count/api.dever", "cmd total = app.total"),
|
||||
]);
|
||||
assert!(program.application_errors().unwrap().is_empty(), "{domain}");
|
||||
}
|
||||
let program = application(&[
|
||||
("first/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
|
||||
("first/count/app.dever", "total(value: Int) (answer: Int) { answer = domain.calculate(value) }"),
|
||||
("first/count/api.dever", "cmd total = app.total"),
|
||||
("second/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
|
||||
("second/count/app.dever", "total(value: Int) (answer: Int) { answer = domain.calculate(value) }"),
|
||||
("second/count/api.dever", "cmd total = app.total"),
|
||||
]);
|
||||
assert!(program.application_errors().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_keeps_effectful_private_copies_and_adapter_redundancy_as_warnings() {
|
||||
let program = application(&[
|
||||
("sample/count/domain.dever", "emit(value: Int) (answer: Int) { dever.io.println(\"called\")\nanswer = value }\nfirst(value: Int) (answer: Int) { answer = emit(value) + 1 }\nsecond(renamed: Int) (answer: Int) { answer = emit(renamed) + 1 }"),
|
||||
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }"),
|
||||
("sample/count/api.dever", "cmd total = app.total"),
|
||||
]);
|
||||
assert!(program.warnings().iter().any(|warning| warning.code == "W003"));
|
||||
assert!(program.application_errors().unwrap().is_empty());
|
||||
|
||||
let program = application(&[
|
||||
("sample/count/port.dever", "read(value: Int) (answer: Int) fails dever.time.SleepResult"),
|
||||
("sample/count/adapter.dever", "first(value: Int) (answer: Int) { answer = value + 1 }\nsecond(value: Int) (answer: Int) { answer = value + 1 }\nport.read(value: Int) (answer: Int) { one = first(value)\ntwo = second(value)\nthree = first(value)\nanswer = one + two + three }"),
|
||||
("sample/count/app.dever", "read(value: Int) (answer: Int) { answer = port.read(value) }"),
|
||||
("sample/count/api.dever", "cmd read = app.read"),
|
||||
]);
|
||||
for code in ["W003", "W004"] {
|
||||
assert!(program.warnings().iter().any(|warning| warning.code == code));
|
||||
}
|
||||
assert!(program.application_errors().unwrap().is_empty());
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use dever_core::{hir::Program, source::SourceMap};
|
||||
|
||||
#[path = "support/temp.rs"]
|
||||
mod temp;
|
||||
|
||||
fn examples() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../skills/assets/examples")
|
||||
}
|
||||
|
||||
fn checked(root: &Path) -> Program {
|
||||
let sources = SourceMap::load_project(&root.join("module"), &root.join("test")).unwrap();
|
||||
let settings = dever_runtime::config::Settings::load_project(root).unwrap();
|
||||
let program = dever_core::check_with_settings(&sources, &settings).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}: {}",
|
||||
root.display(),
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
let errors = program.application_errors().unwrap();
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
);
|
||||
program.validate_database_settings(&settings).unwrap();
|
||||
for source in sources.files() {
|
||||
assert_eq!(
|
||||
dever_core::format::format(source).unwrap(),
|
||||
source.text(),
|
||||
"{}",
|
||||
source.path().display()
|
||||
);
|
||||
}
|
||||
program
|
||||
}
|
||||
|
||||
fn copy_tree(source: &Path, target: &Path) {
|
||||
fs::create_dir(target).unwrap();
|
||||
for entry in fs::read_dir(source).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let destination = target.join(entry.file_name());
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
copy_tree(&entry.path(), &destination);
|
||||
} else {
|
||||
fs::copy(entry.path(), destination).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relocated_examples_check_production_roots_settings_and_case_local_effects() {
|
||||
let temporary = temp::TemporaryDirectory::new();
|
||||
let projects = [
|
||||
("http-rest", 1, false),
|
||||
("jobs", 1, true),
|
||||
("external-lib", 2, false),
|
||||
("collections", 2, false),
|
||||
("command", 1, false),
|
||||
];
|
||||
assert_eq!(fs::read_dir(examples()).unwrap().count(), projects.len());
|
||||
for (name, test_count, uses_database) in projects {
|
||||
let root = temporary.path().join(name);
|
||||
copy_tree(&examples().join(name), &root);
|
||||
let program = checked(&root);
|
||||
assert_eq!(program.tests().len(), test_count);
|
||||
assert!(
|
||||
program
|
||||
.tests()
|
||||
.iter()
|
||||
.all(|case| case.uses_database() == uses_database)
|
||||
);
|
||||
}
|
||||
let template = temporary.path().join("template");
|
||||
copy_tree(&examples().parent().unwrap().join("template"), &template);
|
||||
assert_eq!(checked(&template).tests().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_ecosystems_share_one_checked_port_contract_and_fake_suite() {
|
||||
let temporary = temp::TemporaryDirectory::new();
|
||||
let root = temporary.path().join("external-lib");
|
||||
copy_tree(&examples().join("external-lib"), &root);
|
||||
let original = checked(&root).external_worker_contracts().remove(0);
|
||||
let mut schemas = std::collections::BTreeSet::new();
|
||||
for (ecosystem, entry, dependency) in [
|
||||
("pip", "python/render.py", "markdown@3.8.2"),
|
||||
("npm", "javascript/render.mjs", "marked@15.0.12"),
|
||||
("go", "go/render.go", "github.com/yuin/goldmark@v1.7.8"),
|
||||
] {
|
||||
fs::write(
|
||||
root.join("module/document/markup/adapter.dever"),
|
||||
format!("external {ecosystem} \"{entry}\" {{\n lib \"{dependency}\"\n}}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join("config/setting.json"),
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"lib": [format!("{ecosystem}:{dependency}")]
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let program = checked(&root);
|
||||
let contracts = program.external_worker_contracts();
|
||||
let [contract] = contracts.as_slice() else {
|
||||
panic!("one external Adapter must own the shared Port")
|
||||
};
|
||||
// 协议身份绑定生态和 Lib 声明;跨生态复用的是 Port 的类型合同。
|
||||
assert!(schemas.insert(contract.schema.clone()));
|
||||
assert_eq!(contract.port, original.port);
|
||||
assert_eq!(contract.setting, original.setting);
|
||||
assert_eq!(contract.inputs, original.inputs);
|
||||
assert_eq!(contract.outputs, original.outputs);
|
||||
assert_eq!(contract.errors, original.errors);
|
||||
assert_eq!(contract.operations, ["render"]);
|
||||
assert_eq!(contract.ecosystem, ecosystem);
|
||||
assert_eq!(contract.libs, [format!("{ecosystem}:{dependency}")]);
|
||||
assert!(root.join(&contract.entry).is_file());
|
||||
assert_eq!(program.tests().len(), 2);
|
||||
assert!(!program.tests().iter().any(|case| case.uses_database()));
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class InstalledDaemon:
|
||||
self.__exit__(None, None, None)
|
||||
raise
|
||||
|
||||
def command(self, *arguments):
|
||||
def command(self, *arguments, cwd="/"):
|
||||
if self.child.poll() is not None:
|
||||
raise RuntimeError("owned daemon exited")
|
||||
expected = self.image.stat()
|
||||
@@ -137,7 +137,7 @@ class InstalledDaemon:
|
||||
raise RuntimeError("owned namespace identity changed")
|
||||
return execute([
|
||||
"/usr/bin/nsenter", "--target", str(self.pid), "--all", "--root",
|
||||
"--wdns=/", "--", "/usr/local/bin/dever", *arguments,
|
||||
f"--wdns={cwd}", "--", "/usr/local/bin/dever", *arguments,
|
||||
])
|
||||
|
||||
def __exit__(self, *_):
|
||||
@@ -277,10 +277,22 @@ def accept(release, assets, image, report, init, extension_helper=None):
|
||||
expected = f"/opt/dever/versions/{manifest['version']}/skills/dever-language"
|
||||
if daemon.command("skill", "path") != expected:
|
||||
raise RuntimeError("versioned skill did not match installed core")
|
||||
daemon.command("skill", "install", "/ai-skill")
|
||||
if not (image / "ai-skill/SKILL.md").is_file():
|
||||
raise RuntimeError("missing installed skill entry")
|
||||
passed("versioned_skill_resolution_and_loader_install")
|
||||
rules = image / "first/AGENTS.md"
|
||||
project_rules = b"# Project-specific rules\n\n"
|
||||
rules.write_bytes(project_rules + (
|
||||
"<!-- dever-language:start -->\n"
|
||||
"Previous Dever rules\n"
|
||||
"<!-- dever-language:end -->\n"
|
||||
).encode())
|
||||
daemon.command("skill", "install", cwd="/first")
|
||||
template = installed / "skills/dever-language/assets/template/AGENTS.md"
|
||||
expected_rules = project_rules + template.read_bytes()
|
||||
if rules.read_bytes() != expected_rules:
|
||||
raise RuntimeError("project rules differ from the signed template or user rules were lost")
|
||||
daemon.command("skill", "install", cwd="/first")
|
||||
if rules.read_bytes() != expected_rules:
|
||||
raise RuntimeError("repeated skill installation changed project rules")
|
||||
passed("versioned_skill_resolution_and_project_rules_install")
|
||||
try:
|
||||
daemon.command("build", "/first", "--target", "linux-aarch64", "--output", "/first/program.arm64")
|
||||
except RuntimeError as error:
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Focused static checks for the self-contained, versioned language skill."""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKILL = ROOT / "skills"
|
||||
|
||||
|
||||
def check_tutorial_sources(skill):
|
||||
"""Keep tutorial excerpts tied to the complete projects checked by Rust."""
|
||||
projects = [
|
||||
path.resolve()
|
||||
for path in [skill / "assets/template", *(skill / "assets/examples").iterdir()]
|
||||
]
|
||||
for document in sorted((skill / "references").glob("*.md")):
|
||||
content = document.read_text()
|
||||
blocks = list(re.finditer(
|
||||
r"^```dever[ \t]*\n(.*?)^```[ \t]*$", content, re.MULTILINE | re.DOTALL
|
||||
))
|
||||
if len(blocks) != len(re.findall(r"^```dever\b", content, re.MULTILINE)):
|
||||
raise ValueError(f"{document.name}: unclosed or unsupported dever fence")
|
||||
for block in blocks:
|
||||
binding = re.search(
|
||||
r"源码:\[[^\]\n]+\]\(([^)\n]+)\)\n\n\Z", content[:block.start()]
|
||||
)
|
||||
if binding is None:
|
||||
raise ValueError(f"{document.name}: dever block needs a source link")
|
||||
source = (document.parent / binding.group(1)).resolve()
|
||||
owners = [root for root in projects if source.is_relative_to(root)]
|
||||
if len(owners) != 1 or source.suffix != ".dever":
|
||||
raise ValueError(f"{document.name}: source must belong to a checked skill project")
|
||||
relative = source.relative_to(owners[0])
|
||||
if relative.parts[0] not in ("module", "test") or not source.is_file():
|
||||
raise ValueError(f"{document.name}: source must be a module or test file")
|
||||
excerpt = block.group(1)
|
||||
if not excerpt.strip() or ("\n" + source.read_text()).count("\n" + excerpt) != 1:
|
||||
raise ValueError(f"{document.name}: excerpt differs from its source or is ambiguous")
|
||||
|
||||
|
||||
class SkillExamples(unittest.TestCase):
|
||||
def test_relocated_skill_links_and_release_file_limits(self):
|
||||
with tempfile.TemporaryDirectory(prefix="dever-skill-") as directory:
|
||||
installed = Path(directory) / "dever-language"
|
||||
shutil.copytree(SKILL, installed)
|
||||
files = [path for path in installed.rglob("*") if path.is_file()]
|
||||
self.assertLessEqual(len(files), 256)
|
||||
self.assertLessEqual(sum(path.stat().st_size for path in files), 8 * 1024 * 1024)
|
||||
self.assertTrue((installed / "SKILL.md").is_file())
|
||||
for path in files:
|
||||
self.assertFalse(path.is_symlink())
|
||||
self.assertLessEqual(path.stat().st_size, 1024 * 1024)
|
||||
if path.suffix != ".md":
|
||||
continue
|
||||
for target in re.findall(r"\]\(([^)]+)\)", path.read_text()):
|
||||
if "://" in target or target.startswith("#"):
|
||||
continue
|
||||
resolved = (path.parent / target.split("#", 1)[0]).resolve()
|
||||
self.assertTrue(resolved.is_relative_to(installed), (path, target))
|
||||
self.assertTrue(resolved.exists(), (path, target))
|
||||
|
||||
def test_tutorial_code_matches_relocated_checked_sources(self):
|
||||
with tempfile.TemporaryDirectory(prefix="dever-tutorial-") as directory:
|
||||
installed = Path(directory) / "dever-language"
|
||||
shutil.copytree(SKILL, installed)
|
||||
check_tutorial_sources(installed)
|
||||
|
||||
def test_tutorial_source_drift_and_unbound_code_are_rejected(self):
|
||||
with tempfile.TemporaryDirectory(prefix="dever-tutorial-drift-") as directory:
|
||||
installed = Path(directory) / "dever-language"
|
||||
shutil.copytree(SKILL, installed)
|
||||
document = installed / "references/language.md"
|
||||
original = document.read_text()
|
||||
source = installed / "assets/template/module/hello/greeting/app.dever"
|
||||
original_source = source.read_text()
|
||||
mutations = {
|
||||
"unbound": original.replace("源码:", "完整示例:", 1),
|
||||
"changed_code": original.replace('"Hello, "', '"Welcome, "', 1),
|
||||
"unclosed": original + "\n```dever\nunclosed() () {}\n",
|
||||
"outside_project": original.replace(
|
||||
"../assets/template/module/hello/greeting/app.dever", "../SKILL.md", 1
|
||||
),
|
||||
}
|
||||
for name, changed in mutations.items():
|
||||
with self.subTest(name=name):
|
||||
document.write_text(changed)
|
||||
with self.assertRaises(ValueError):
|
||||
check_tutorial_sources(installed)
|
||||
document.write_text(original)
|
||||
source.write_text(original_source.replace('"Hello, "', '"Welcome, "'))
|
||||
with self.assertRaisesRegex(ValueError, "excerpt differs"):
|
||||
check_tutorial_sources(installed)
|
||||
source.write_text(original_source + "\n" + original_source)
|
||||
with self.assertRaisesRegex(ValueError, "ambiguous"):
|
||||
check_tutorial_sources(installed)
|
||||
|
||||
def test_signature_index_matches_public_library_declarations(self):
|
||||
index = (SKILL / "references/signatures.md").read_text()
|
||||
blocks = re.findall(r"```signatures\n(.*?)```", index, re.DOTALL)
|
||||
self.assertTrue(blocks, "the shipped index must contain concrete signatures")
|
||||
seen = set()
|
||||
for signature in "\n".join(blocks).splitlines():
|
||||
if not signature:
|
||||
continue
|
||||
qualified = signature.split("(", 1)[0]
|
||||
package, function = qualified.rsplit(".", 1)
|
||||
library = ROOT / "library" / (package.replace(".", "/") + ".dever")
|
||||
self.assertNotIn(qualified, seen)
|
||||
seen.add(qualified)
|
||||
declaration = re.search(
|
||||
rf"^public ({re.escape(function)}\s*\([^{{]*?)\s*\{{",
|
||||
library.read_text(),
|
||||
re.MULTILINE,
|
||||
)
|
||||
self.assertIsNotNone(declaration, qualified)
|
||||
expected = package + "." + declaration.group(1)
|
||||
self.assertEqual(re.sub(r"\s", "", signature), re.sub(r"\s", "", expected), qualified)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user