@agentsfleet/orly 0.6.8 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-commit +5 -1
- package/audits/doc-read.sh +14 -12
- package/audits/rule-ledger-lib.sh +45 -1
- package/audits/rule-ledger.sh +15 -3
- package/dispatch/edit_rules.sh +41 -0
- package/dispatch/write_any.md +28 -0
- package/dispatch/write_go.md +2 -0
- package/dispatch/write_mdx.md +2 -0
- package/dispatch/write_python.md +2 -0
- package/dispatch/write_rust.md +2 -0
- package/dispatch/write_shell.md +2 -0
- package/docs/EXECUTE_DOC_READS.md +3 -0
- package/docs/RULE_ENFORCEMENT.md +2 -2
- package/docs/greptile-learnings/RULES.md +7 -0
- package/package.json +1 -1
- package/packs/language/go/rules.md +2 -0
- package/packs/language/mdx/rules.md +2 -0
- package/packs/language/python/rules.md +2 -0
- package/packs/language/rust/rules.md +130 -0
- package/packs/language/shell/rules.md +2 -0
- package/registry.json +4 -0
package/.githooks/pre-commit
CHANGED
|
@@ -32,7 +32,11 @@ fi
|
|
|
32
32
|
# that did or did not read the façade is still the session committing.
|
|
33
33
|
SOURCE_PATTERN='\.(zig|ts|tsx|js|jsx|py|rs|go|sh|sql)$'
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# Governance paths join the source list here: dispatch/edit_rules.sh declares
|
|
36
|
+
# the corpus scope, so a staged rule edit now has a façade to have read. The
|
|
37
|
+
# check itself stays the arbiter — a path outside every leaf's globs still
|
|
38
|
+
# reports "nothing staged triggers a façade" and costs one grep.
|
|
39
|
+
if git diff --cached --name-only | grep -qE "$SOURCE_PATTERN|$GOVERNANCE_PATTERN"; then
|
|
36
40
|
if ! bash audits/doc-read.sh check; then
|
|
37
41
|
echo "pre-commit: a triggered façade went unread — read it, or record why" >&2
|
|
38
42
|
exit 1
|
package/audits/doc-read.sh
CHANGED
|
@@ -94,19 +94,21 @@ content_hash() {
|
|
|
94
94
|
git -C "$LEDGER_ROOT" hash-object "$LEDGER_ROOT/$1" 2>/dev/null || printf ''
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
# Façade pages the staged files trigger, one per line.
|
|
98
|
-
#
|
|
99
|
-
#
|
|
97
|
+
# Façade pages the staged files trigger, one per line. Scope comes from
|
|
98
|
+
# ledger_facade_scope — the page's own oracle-scope tag, else its leaf's
|
|
99
|
+
# dispatch_init — which is the SAME resolver the ledger's reachability report
|
|
100
|
+
# uses, so this check and the ledger can never disagree about what fires.
|
|
101
|
+
# Iterating pages rather than leaves is the point: a façade without a
|
|
102
|
+
# deterministic leaf still has a trigger surface, and used to be unaskable.
|
|
100
103
|
expected_facade_pages() {
|
|
101
|
-
local staged="$1"
|
|
102
|
-
while IFS= read -r
|
|
103
|
-
[ -n "$
|
|
104
|
-
|
|
105
|
-
[ -
|
|
106
|
-
|
|
107
|
-
[
|
|
108
|
-
|
|
109
|
-
done < <(ledger_facade_scripts)
|
|
104
|
+
local staged="$1" page stem scope
|
|
105
|
+
while IFS= read -r page; do
|
|
106
|
+
[ -n "$page" ] || continue
|
|
107
|
+
scope="$(ledger_facade_scope "$page")"
|
|
108
|
+
[ -n "$scope" ] || continue
|
|
109
|
+
stem="$(basename "$page" .md)"
|
|
110
|
+
[ "$(ledger_match_count "$scope" < "$staged")" -gt 0 ] && printf 'dispatch/%s.md\n' "$stem"
|
|
111
|
+
done < <(ledger_facade_pages)
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
json_unescape() {
|
|
@@ -187,6 +187,40 @@ ledger_has_prefix() {
|
|
|
187
187
|
DISPATCH_LIB_BASENAME="lib.sh"
|
|
188
188
|
|
|
189
189
|
# The executable façades: dispatch/*.sh minus the shared library.
|
|
190
|
+
# A façade's trigger surface, declared where it belongs: in the façade. The tag
|
|
191
|
+
# is one HTML comment, invisible in rendered markdown, e.g.
|
|
192
|
+
# <!-- oracle-scope: *.rs -->
|
|
193
|
+
# Scope used to be readable ONLY from a leaf's dispatch_init line, so the five
|
|
194
|
+
# façades carrying deterministic checks had a voice and the other fourteen had
|
|
195
|
+
# none — a Rust author was never asked to read the Rust rules, because
|
|
196
|
+
# write_rust.md has no .sh. The page wins; the leaf remains the fallback so the
|
|
197
|
+
# mechanised façades keep the scope they already declare, in one place.
|
|
198
|
+
FACADE_SCOPE_TAG='^[[:space:]]*<!--[[:space:]]*oracle-scope:[^>]*-->[[:space:]]*$'
|
|
199
|
+
|
|
200
|
+
ledger_facade_pages() {
|
|
201
|
+
local file
|
|
202
|
+
for file in "$LEDGER_ROOT"/dispatch/*.md; do
|
|
203
|
+
[ -f "$file" ] || continue
|
|
204
|
+
printf '%s\n' "$file"
|
|
205
|
+
done
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Space-separated, trailing space — same shape ledger_facade_globs returns, so
|
|
209
|
+
# ledger_match_count reads either without knowing which declared it.
|
|
210
|
+
ledger_facade_scope() {
|
|
211
|
+
local page="$1" tag stem leaf
|
|
212
|
+
tag="$(grep -hE "$FACADE_SCOPE_TAG" "$page" 2>/dev/null | head -1)"
|
|
213
|
+
if [ -n "$tag" ]; then
|
|
214
|
+
printf '%s' "$tag" \
|
|
215
|
+
| sed -e 's/.*oracle-scope:[[:space:]]*//' -e 's/[[:space:]]*-->.*//' \
|
|
216
|
+
-e 's/[[:space:]][[:space:]]*/ /g' -e 's/[[:space:]]*$/ /'
|
|
217
|
+
return 0
|
|
218
|
+
fi
|
|
219
|
+
stem="$(basename "$page" .md)"
|
|
220
|
+
leaf="$(dirname "$page")/$stem.sh"
|
|
221
|
+
[ -f "$leaf" ] && ledger_facade_globs "$leaf"
|
|
222
|
+
}
|
|
223
|
+
|
|
190
224
|
ledger_facade_scripts() {
|
|
191
225
|
local file
|
|
192
226
|
for file in "$LEDGER_ROOT"/dispatch/*.sh; do
|
|
@@ -208,7 +242,16 @@ ledger_facade_globs() {
|
|
|
208
242
|
# is the match itself — the same expansion dispatch_resolve_files applies to
|
|
209
243
|
# explicit file arguments, so a fire count means what the dispatcher means.
|
|
210
244
|
ledger_match_count() {
|
|
211
|
-
local globs="$1" path glob matches=0
|
|
245
|
+
local globs="$1" path glob matches=0 reglob=""
|
|
246
|
+
# `for glob in $globs` needs WORD SPLITTING and must not get PATHNAME
|
|
247
|
+
# EXPANSION: unguarded, bash expands each pattern against the real tree
|
|
248
|
+
# first, so a façade declaring 'src/*' iterates the 33 files that happen to
|
|
249
|
+
# exist there and then compares them as literals — src/foo.rs matches
|
|
250
|
+
# nothing and core/* "matches" only because it expanded to the one file in
|
|
251
|
+
# core/. Latent while every leaf declared extension globs ('*.zig') that
|
|
252
|
+
# match nothing at the root and so survive expansion unchanged; the first
|
|
253
|
+
# path-glob façade turns it into a scope that silently reads wrong.
|
|
254
|
+
case "$-" in *f*) ;; *) reglob="yes"; set -f ;; esac
|
|
212
255
|
while IFS= read -r path; do
|
|
213
256
|
[ -n "$path" ] || continue
|
|
214
257
|
for glob in $globs; do
|
|
@@ -216,6 +259,7 @@ ledger_match_count() {
|
|
|
216
259
|
case "$path" in $glob) matches=$((matches + 1)); break ;; esac
|
|
217
260
|
done
|
|
218
261
|
done
|
|
262
|
+
[ -n "$reglob" ] && set +f
|
|
219
263
|
printf '%d' "$matches"
|
|
220
264
|
}
|
|
221
265
|
|
package/audits/rule-ledger.sh
CHANGED
|
@@ -101,12 +101,24 @@ check_registry_parity() {
|
|
|
101
101
|
# is answerable from the tree — unlike "has this fired lately", which depends on
|
|
102
102
|
# what happened to get committed recently and gates nothing.
|
|
103
103
|
check_facade_scopes() {
|
|
104
|
-
local script stem
|
|
104
|
+
local script stem page
|
|
105
|
+
# Iterates LEAVES, not pages: a leaf is a promise that something mechanical
|
|
106
|
+
# fires, so one declaring no scope is dead weight whether or not a page sits
|
|
107
|
+
# beside it. A façade with no leaf and no tag is situational — a lifecycle
|
|
108
|
+
# stage transition, a "tests pass" claim, writing a PR body — and has no
|
|
109
|
+
# staged-diff trigger to declare, so it is not a finding.
|
|
105
110
|
while IFS= read -r script; do
|
|
106
111
|
[ -n "$script" ] || continue
|
|
107
|
-
[ -n "$(ledger_facade_globs "$script")" ] && continue
|
|
108
112
|
stem="$(basename "$script" .sh)"
|
|
109
|
-
|
|
113
|
+
page="$LEDGER_ROOT/dispatch/$stem.md"
|
|
114
|
+
# Either surface may carry the scope; ledger_facade_scope prefers the page
|
|
115
|
+
# and falls back to this leaf, so both spellings satisfy the same check.
|
|
116
|
+
if [ -f "$page" ]; then
|
|
117
|
+
[ -n "$(ledger_facade_scope "$page")" ] && continue
|
|
118
|
+
else
|
|
119
|
+
[ -n "$(ledger_facade_globs "$script")" ] && continue
|
|
120
|
+
fi
|
|
121
|
+
fail "facade=$stem declares no scope — no oracle-scope tag in dispatch/$stem.md and no dispatch_init in dispatch/$stem.sh (nothing it carries can ever fire)"
|
|
110
122
|
done < <(ledger_facade_scripts)
|
|
111
123
|
}
|
|
112
124
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dispatch/edit_rules.sh — the rule-corpus dispatch, DETERMINISTIC façade.
|
|
3
|
+
#
|
|
4
|
+
# Pairs with dispatch/edit_rules.md (the LATENT façade — prose the agent reads).
|
|
5
|
+
#
|
|
6
|
+
# This leaf exists for the reason RULE UFS exists in the Rust pack: a façade
|
|
7
|
+
# that declares its scope in PROSE declares it to nobody. edit_rules.md has
|
|
8
|
+
# always named its trigger surface — core/**, packs/**, schemas/**, src/**,
|
|
9
|
+
# registry.json, governance hooks (edit_rules.md:15, and the same row in
|
|
10
|
+
# docs/EXECUTE_DOC_READS.md) — but audits/doc-read.sh derives what fires from
|
|
11
|
+
# `dispatch_init` globs in a leaf, and this façade had no leaf. So every
|
|
12
|
+
# governance edit reported "nothing staged triggers a façade" and the DOC READ
|
|
13
|
+
# GATE could not fire on the one surface whose edits change the rules
|
|
14
|
+
# themselves. The globs below ARE that scope, now in a form a gate can read.
|
|
15
|
+
#
|
|
16
|
+
# dispatch/edit_rules.sh <file> [...] # explicit targets (EXECUTE)
|
|
17
|
+
# dispatch/edit_rules.sh --staged # staged corpus files (CONFORM, pre-commit)
|
|
18
|
+
#
|
|
19
|
+
# Layering: AGENTS.md → edit_rules.md (latent) → edit_rules.sh (this) → audits/*.sh
|
|
20
|
+
# Signals: 🟢 pass · 🔴 fail (blocks) · 🤔 DECIDE (judgment; blocks the TURN, not the script)
|
|
21
|
+
# Exit: 0 = mechanical gates pass · 1 = ≥1 failed · 2 = usage error.
|
|
22
|
+
|
|
23
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
24
|
+
|
|
25
|
+
# Scope mirrors docs/EXECUTE_DOC_READS.md's governance row and edit_rules.md:15.
|
|
26
|
+
# Kept in one order with them: a glob here that the row does not name is a leaf
|
|
27
|
+
# reading more than the façade declares, which is the same drift inverted.
|
|
28
|
+
# ONE LINE, deliberately: ledger_facade_globs reads `head -1` of the
|
|
29
|
+
# dispatch_init line, so a backslash continuation declares an EMPTY scope and
|
|
30
|
+
# the façade silently stops firing — the exact failure this leaf exists to end.
|
|
31
|
+
dispatch_init "RULES" 'core/*' 'packs/*' 'schemas/*' 'src/*' 'registry.json' 'dispatch/*' 'audits/*' '.githooks/*' 'AGENTS.md' 'AGENTS.orly.md'
|
|
32
|
+
dispatch_resolve_files "$@"
|
|
33
|
+
dispatch_header
|
|
34
|
+
|
|
35
|
+
# ── deterministic gates ────────────────────────────────────────────
|
|
36
|
+
# Rule prose carries no milestone ids — the packs say so themselves ("never a
|
|
37
|
+
# memory file, never a milestone cite"), and until now nothing read the corpus
|
|
38
|
+
# for them. Same leaf write_any already wires, so no new code enters the gloss.
|
|
39
|
+
dispatch_run_helper "MSID" "msid-ui.sh" "--staged"
|
|
40
|
+
|
|
41
|
+
dispatch_verdict
|
package/dispatch/write_any.md
CHANGED
|
@@ -736,6 +736,34 @@ Every triggered consult is logged in the active spec's **Discovery** section, or
|
|
|
736
736
|
|
|
737
737
|
NLR is the cleanup-on-touch arm; NLG bans new legacy framing pre-v2.0.0; this guard covers the harder judgment calls ("should this whole subsystem exist") that need the user's input.
|
|
738
738
|
|
|
739
|
+
## Porting a codebase between languages (RULE PORT)
|
|
740
|
+
|
|
741
|
+
> [JUDGMENT → NEW:PORT]
|
|
742
|
+
|
|
743
|
+
A port is a REWRITE with a conformance test, never a transliteration.
|
|
744
|
+
|
|
745
|
+
Before porting a construct, ask what it was working AROUND, not what it was.
|
|
746
|
+
A thread with a stop flag is usually the absence of async. A registration map
|
|
747
|
+
with generation counters is usually the absence of structured cancellation. A
|
|
748
|
+
defer chain is usually the absence of ownership. Port the GUARANTEE; delete the
|
|
749
|
+
workaround, and write down which guarantee replaced it.
|
|
750
|
+
|
|
751
|
+
Every construct carried over needs one sentence: "kept because <the target
|
|
752
|
+
language has no better mechanism>". If that sentence cannot be written
|
|
753
|
+
honestly, it is transliteration.
|
|
754
|
+
|
|
755
|
+
Conformance is on OBSERVABLE behaviour — wire bytes, rows, status codes, money,
|
|
756
|
+
exit codes. Never on internal shape. A test that pins internal shape freezes
|
|
757
|
+
the source language's debt into the target.
|
|
758
|
+
|
|
759
|
+
Read the reference implementations the spec names before designing. Take the
|
|
760
|
+
SHAPE and reject the specifics.
|
|
761
|
+
|
|
762
|
+
Name the tech debt you are NOT carrying over, and where it went.
|
|
763
|
+
|
|
764
|
+
A milestone titled "parity" means behavioural parity. It never licenses a
|
|
765
|
+
one-to-one file, type, or thread mapping.
|
|
766
|
+
|
|
739
767
|
## Comment Voice — New to the Codebase, Familiar with the Goal
|
|
740
768
|
|
|
741
769
|
> [JUDGMENT → NEW:CMT]
|
package/dispatch/write_go.md
CHANGED
package/dispatch/write_mdx.md
CHANGED
package/dispatch/write_python.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Python authoring
|
|
2
2
|
|
|
3
|
+
<!-- oracle-scope: *.py -->
|
|
4
|
+
|
|
3
5
|
Use standard-library parsers for structured input. Keep resource ownership in
|
|
4
6
|
context managers. Validate data at parse boundaries and preserve specific
|
|
5
7
|
exception classes until the caller can decide how to respond.
|
package/dispatch/write_rust.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Rust authoring
|
|
2
2
|
|
|
3
|
+
<!-- oracle-scope: *.rs -->
|
|
4
|
+
|
|
3
5
|
Use ownership and borrowing to make resource lifetime visible. Keep `unsafe`
|
|
4
6
|
blocks small and state the invariant they rely on. Preserve error variants until
|
|
5
7
|
the caller has enough information to decide whether to retry or stop.
|
package/dispatch/write_shell.md
CHANGED
|
@@ -10,10 +10,13 @@ Every triggered document requires one `📖 DOC READ: <path>` proof-line **per t
|
|
|
10
10
|
| `core/**`, `packs/**`, `schemas/**`, `src/**`, `registry.json`, agent-home instruction links, repository rules synchronization, or governance hooks | `dispatch/edit_rules.md` + `docs/ORLY_ARCHITECTURE.md` — canonical sources, profiles, snapshots, refusal boundaries, and evidence. | <!-- oracle-packs:workflow.governance -->
|
|
11
11
|
| Lifecycle stage transition (CHORE(open)/PLAN/CHORE(close)/LAND, worktree setup, milestone bootstrap) | `dispatch/lifecycle.md` — the entered stage's runbook section only (section-scan; fires on the transition itself, not on an Edit/Write). |
|
|
12
12
|
| Any source file (cross-cutting authoring) | `dispatch/write_any.md` — length, logging, milestone-id, error-registry, UFS, greptile read, legacy-workaround family. |
|
|
13
|
+
| Porting a codebase, module, or subsystem from another language | `dispatch/write_any.md` §Porting a codebase between languages — a port is a rewrite with a conformance test: port the guarantee, delete the workaround, conform on observable behaviour, name the debt not carried over. |
|
|
13
14
|
| Spec's "Applicable Rules" | Each rule (canonical). Missing → the cross-cutting codes of `dispatch/write_any.md` are the floor; surface omission. |
|
|
14
15
|
| `*.zig` | `dispatch/write_zig.md`. ZIG GATE per edit. |
|
|
15
16
|
| `*.ts`/`*.tsx`/`*.js`/`*.jsx` | `dispatch/write_ts_adhere_bun.md` — TS FILE SHAPE DECISION (§1) at PLAN, const/import/Bun-primitive discipline, anti-patterns. |
|
|
16
17
|
| `*.rs` | `dispatch/write_rust.md` — ownership, error variants, feature combinations, and contention tests. | <!-- oracle-packs:language.rust -->
|
|
18
|
+
| Rust error type, `Result` alias, or any fallible signature | `dispatch/write_rust.md` §Error discipline — one error per crate, one `Result` alias, `#[from]` composition, `source()` never returns self. | <!-- oracle-packs:language.rust -->
|
|
19
|
+
| Designing a Rust function, type, or module; writing `match` on a `Result`/`Option`; any production `unwrap()` | `dispatch/write_rust.md` §Functional design — Result pipelines over control-flow trees, illegal states unrepresentable, bind once, `unwrap` needs a proved invariant. | <!-- oracle-packs:language.rust -->
|
|
17
20
|
| `*.py` | `dispatch/write_python.md` — parsing boundaries, resource ownership, and specific exceptions. |
|
|
18
21
|
| `*.sh` | `dispatch/write_shell.md` — quoting, array arguments, cleanup, input safety, and shell compatibility. |
|
|
19
22
|
| `*.mdx` | `dispatch/write_mdx.md` — structure, front matter, links, code fences, accessibility, and Mintlify isolation. | <!-- oracle-packs:language.mdx -->
|
package/docs/RULE_ENFORCEMENT.md
CHANGED
|
@@ -13,8 +13,8 @@ regenerates this file and fails on any difference. To update it, run
|
|
|
13
13
|
| `docs/LIFECYCLE_PATTERNS.md` | — | 0 | 0 | 0/22 | mechanical |
|
|
14
14
|
| `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/3 | latent |
|
|
15
15
|
| `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/4 | uncited |
|
|
16
|
-
| `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/
|
|
17
|
-
| **corpus** | | | | **26/
|
|
16
|
+
| `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/114 | mechanical |
|
|
17
|
+
| **corpus** | | | | **26/281** | |
|
|
18
18
|
|
|
19
19
|
## How to read this
|
|
20
20
|
|
|
@@ -179,6 +179,13 @@ every code a dispatch `.sh` emits resolves to exactly one row here.
|
|
|
179
179
|
**Tags:** zig, testing, sql
|
|
180
180
|
**Example:** tested 0 for a column with CHECK >= 512.
|
|
181
181
|
|
|
182
|
+
## RULE TCF — A test that cannot fail is not a test
|
|
183
|
+
|
|
184
|
+
**Rule:** Before trusting a test, make it red — delete the clause, guard, or branch it covers and confirm it fails. Assert what the check FOUND (the count, the symbol, the line), never only its exit code. A test that survives removal of its own subject is repaired or deleted, never kept for coverage.
|
|
185
|
+
**Why:** A vacuous pass and a real pass look identical from the outside, so the suite reports green while the guarantee is already gone.
|
|
186
|
+
**Tags:** testing, review, all
|
|
187
|
+
**Example:** a tenant-scope property asserted absence only and passed with the scope clause deleted; a dispatch eval asserted an exit code while its glob pointed at a dead tree.
|
|
188
|
+
|
|
182
189
|
## RULE JCL — CLI JSON contract discipline
|
|
183
190
|
|
|
184
191
|
**Rule:** Use only stable error codes; UNKNOWN_COMMAND must name the unrecognized token; dual jsonMode guards need a comment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Python authoring
|
|
2
2
|
|
|
3
|
+
<!-- oracle-scope: *.py -->
|
|
4
|
+
|
|
3
5
|
Use standard-library parsers for structured input. Keep resource ownership in
|
|
4
6
|
context managers. Validate data at parse boundaries and preserve specific
|
|
5
7
|
exception classes until the caller can decide how to respond.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Rust authoring
|
|
2
2
|
|
|
3
|
+
<!-- oracle-scope: *.rs -->
|
|
4
|
+
|
|
3
5
|
Use ownership and borrowing to make resource lifetime visible. Keep `unsafe`
|
|
4
6
|
blocks small and state the invariant they rely on. Preserve error variants until
|
|
5
7
|
the caller has enough information to decide whether to retry or stop.
|
|
@@ -31,6 +33,134 @@ Local convention wins on conflict, but the divergence is named in the review
|
|
|
31
33
|
output. Refresh the copy when the user asks or a cited rule looks stale
|
|
32
34
|
(re-run the fetch; upstream updates continuously).
|
|
33
35
|
|
|
36
|
+
## Error discipline (RULE ERR-RS)
|
|
37
|
+
|
|
38
|
+
One error type per crate, and a `Result` alias beside it:
|
|
39
|
+
|
|
40
|
+
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
|
41
|
+
|
|
42
|
+
Every fallible function in the crate returns `Result<T>`. The default type
|
|
43
|
+
parameter is what lets the few functions answering with a foreign error keep
|
|
44
|
+
the same spelling — `Result<T, VerifyError>` — instead of reaching for
|
|
45
|
+
`std::result::Result`. A reader must never have to check WHICH error a
|
|
46
|
+
signature returns to know it is this crate's.
|
|
47
|
+
|
|
48
|
+
**Compose with `From`; `?` does the lifting.** This is `M-FROM-ERROR` and is
|
|
49
|
+
read there, carve-out included; what the guideline leaves open is how the
|
|
50
|
+
variant is spelled, which locally is `#[from]`:
|
|
51
|
+
|
|
52
|
+
#[derive(Debug, thiserror::Error)]
|
|
53
|
+
pub enum BootFailure {
|
|
54
|
+
#[error(transparent)]
|
|
55
|
+
Environment(#[from] Refusal),
|
|
56
|
+
#[error("cannot boot: the database would not answer")]
|
|
57
|
+
Database(#[from] db::Error),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
**`map_err` only to ADD context the call site alone knows** — a role tag, a
|
|
61
|
+
budget, an operation name; the same carve-out `M-FROM-ERROR` names. And never
|
|
62
|
+
this, which no guideline covers:
|
|
63
|
+
|
|
64
|
+
// WRONG: a lossy conversion wearing a conversion's clothes.
|
|
65
|
+
.map_err(|error| Mine::Database(error.to_string()))?
|
|
66
|
+
|
|
67
|
+
`to_string()` on the way INTO an error type destroys the `source()` chain. It
|
|
68
|
+
compiles, it reads fine, and it silently defeats every chain walker downstream.
|
|
69
|
+
|
|
70
|
+
**`source()` returns what caused you, never yourself.** An error whose
|
|
71
|
+
`Display` already renders its kind must not also return that kind as its
|
|
72
|
+
source, or a chain walker prints the same sentence twice before reaching
|
|
73
|
+
anything new:
|
|
74
|
+
|
|
75
|
+
// WRONG — Display is "[code] {kind}", so the kind is already printed.
|
|
76
|
+
fn source(&self) -> Option<&(dyn Error + 'static)> { Some(&self.kind) }
|
|
77
|
+
|
|
78
|
+
// RIGHT — skip ourselves, hand back what the kind wraps.
|
|
79
|
+
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
80
|
+
std::error::Error::source(&self.kind)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
**Not every error has a cause, and that is not a defect.** A variant holding
|
|
84
|
+
another error has one; a variant holding only data (`MissingUrl { knob }`) does
|
|
85
|
+
not — nothing *caused* an unset variable. A test asserting `source().is_some()`
|
|
86
|
+
for every variant is WRONG and forces authors to invent causes. Assert the real
|
|
87
|
+
invariant: where there IS a source, it is not a repeat of our own message.
|
|
88
|
+
|
|
89
|
+
**Divergence from the reference guideline, named as that section requires.**
|
|
90
|
+
`M-ERRORS-CANONICAL-STRUCTS` prescribes a situation-specific `struct` carrying
|
|
91
|
+
a `Backtrace` and lets a complex crate expose several error types;
|
|
92
|
+
`M-APP-ERROR` further lets an application crate skip its own type for
|
|
93
|
+
`anyhow`/`eyre`/`ohno::AppError`. This rule diverges on all three for one
|
|
94
|
+
reason: a `#[from]`-composed enum behind a single per-crate alias is what lets
|
|
95
|
+
`?` lift across a crate boundary with no call-site conversion, and an
|
|
96
|
+
application-level catch-all erases the variant the caller would have matched
|
|
97
|
+
on. A `Backtrace` inside a variant is welcome. A crate with two genuinely
|
|
98
|
+
unrelated fallible surfaces may carry a second type — with its own alias, under
|
|
99
|
+
these same rules. Cite this divergence in review rather than re-deriving it.
|
|
100
|
+
|
|
101
|
+
Prior art, not invention: `core_api` (multi-crate workspace, per-crate
|
|
102
|
+
`error.rs` with `pub type Result<T>` + flat `Error`, in production for years),
|
|
103
|
+
bun (`thiserror`, `#[from]`, `pub type Result<T, E = Error>`), habitat (one
|
|
104
|
+
payload-carrying `enum Error`, one alias).
|
|
105
|
+
|
|
106
|
+
## Functional design (RULE FN-RS)
|
|
107
|
+
|
|
108
|
+
Design the fallible path as a Result PIPELINE, never as a control-flow tree.
|
|
109
|
+
|
|
110
|
+
**Fallibility lives in the type, never in a sentinel.** A function that can
|
|
111
|
+
fail returns `Result<T>`; a value that can be absent returns `Option<T>`.
|
|
112
|
+
Never a bool-plus-out-parameter, never a magic value (-1, empty string, null
|
|
113
|
+
UUID), never a logged-and-swallowed error. If the caller cannot see the
|
|
114
|
+
failure in the signature, the design is wrong before the body is written.
|
|
115
|
+
|
|
116
|
+
**Compose with combinators; spend `match` only where arms genuinely differ.**
|
|
117
|
+
`?`, `map`, `and_then`, `ok_or_else`, `unwrap_or_else` say "transform on
|
|
118
|
+
success, pass failure through" in one line, where a nested match spends five
|
|
119
|
+
and gives every arm a chance to swallow the `Err`:
|
|
120
|
+
|
|
121
|
+
// WRONG: the failure path is hand-plumbed, twice.
|
|
122
|
+
let config = match load(path) {
|
|
123
|
+
Ok(raw) => match parse(&raw) {
|
|
124
|
+
Ok(c) => c,
|
|
125
|
+
Err(e) => return Err(e.into()),
|
|
126
|
+
},
|
|
127
|
+
Err(e) => return Err(e.into()),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// RIGHT: one pipeline; `?` and `From` (RULE ERR-RS) do the lifting.
|
|
131
|
+
let config = parse(&load(path)?)?;
|
|
132
|
+
|
|
133
|
+
A `match` earns its place when the arms carry different LOGIC — not when one
|
|
134
|
+
arm is a hand-written `?`.
|
|
135
|
+
|
|
136
|
+
**Make illegal states unrepresentable; parse, don't validate.** Convert raw
|
|
137
|
+
input into a type that can only hold valid states ONCE, at the boundary, and
|
|
138
|
+
pass that type inward. Two `Option` fields where exactly one is ever set is an
|
|
139
|
+
enum. A `state: String` compared against literals is an enum. A checked-then-
|
|
140
|
+
used raw value is a newtype whose constructor returns `Result` — that
|
|
141
|
+
constructor is `M-STRONG-TYPES-GUARD`, and picking the type family is
|
|
142
|
+
`M-STRONG-TYPES`. What those do not say is the consequence: interior code that
|
|
143
|
+
trusts its types needs no defensive re-checks — delete them.
|
|
144
|
+
|
|
145
|
+
**Expression-orientation: bind once.** `let x = if ... { a } else { b };` and
|
|
146
|
+
`let x = match ...` — never a mutable default that later branches overwrite.
|
|
147
|
+
A `mut` that exists only to simulate an expression is a bug surface with no
|
|
148
|
+
compensating power.
|
|
149
|
+
|
|
150
|
+
**`unwrap()` / `expect()` are for tests and for invariants already proved.**
|
|
151
|
+
`M-PANIC-ON-BUG` settles when a panic is right and `M-PANIC-MESSAGE` that it
|
|
152
|
+
must be helpful; the addition here is WHAT the message carries — the proof
|
|
153
|
+
("mutex never poisoned: no thread panics while holding it"). In production
|
|
154
|
+
code, an `unwrap()` without a written invariant is a `Result` that was never
|
|
155
|
+
designed.
|
|
156
|
+
|
|
157
|
+
Prior art, not invention: `core_api` — a multi-crate production workspace
|
|
158
|
+
where every crate with a fallible surface carries its own `error.rs` +
|
|
159
|
+
`Result` alias, and the fallible path is written as pipelines throughout
|
|
160
|
+
(573 `.map`, 463 `.and_then`, 164 `.ok_or_else` across the workspace). It has
|
|
161
|
+
run in production for years; its author attributes the absence of error-path
|
|
162
|
+
defects to exactly this design, and the maintenance record bears that out.
|
|
163
|
+
|
|
34
164
|
## Constant discipline (RULE UFS)
|
|
35
165
|
|
|
36
166
|
`audits/ufs.sh` reads `*.rs`. It did not until this pack said so, and the cost
|
package/registry.json
CHANGED
|
@@ -263,6 +263,10 @@
|
|
|
263
263
|
"source": "dispatch/edit_rules.md",
|
|
264
264
|
"target": "dispatch/edit_rules.md"
|
|
265
265
|
},
|
|
266
|
+
{
|
|
267
|
+
"source": "dispatch/edit_rules.sh",
|
|
268
|
+
"target": "dispatch/edit_rules.sh"
|
|
269
|
+
},
|
|
266
270
|
{
|
|
267
271
|
"source": "audits/agents-md.md",
|
|
268
272
|
"target": "audits/agents-md.md"
|