@agentsfleet/orly 0.14.3 → 0.14.5
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/AGENTS.md +1 -1
- package/README.md +1 -1
- package/audits/agents-md.md +1 -1
- package/audits/msid-ui.sh +136 -38
- package/audits/spec-template.ts +1 -1
- package/core/operating-model.md +1 -1
- package/dispatch/write_sql.md +2 -5
- package/dispatch/write_sql.sh +2 -2
- package/docs/CHANGELOG.md +15 -0
- package/docs/RULE_ENFORCEMENT.md +2 -2
- package/docs/SCHEMA_CONVENTIONS.md +71 -58
- package/docs/TEMPLATE.md +2 -2
- package/docs/greptile-learnings/RULES.md +2 -2
- package/package.json +1 -1
- package/src/install_packs.test.ts +2 -2
- package/src/scanner_scope.test.ts +37 -0
- package/src/spec_template.test.ts +8 -1
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
|
|
|
16
16
|
|
|
17
17
|
**The human is Kishore** — casual handle **Indy**; either name, any case, addresses him. Ambiguous "the user" / "they" here = Kishore. The agent is **Oracle**, casual handle **Aiwa**. `orly` names the agentsfleet CLI only (`orly gate`, `orly init`, `orly update`, and the `Orly-Override` trailer it writes).
|
|
18
18
|
|
|
19
|
-
**Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `
|
|
19
|
+
**Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `fleet.*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
|
|
20
20
|
|
|
21
21
|
MacBook. Languages: Python, Go, Rust, TypeScript, Zig. Tooling: `mise` first, `brew` fallback. Forges: `gh`/`glab`. Commit identity from `git config`, never here.
|
|
22
22
|
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ TypeSafe's Jev model supplies runtime advice through explicit `orly judge` reque
|
|
|
78
78
|
|
|
79
79
|
See [releases](https://github.com/agentsfleet/orly/releases) for published versions.
|
|
80
80
|
The install commands below fetch the published package.
|
|
81
|
-
The release workflow publishes 0.14.
|
|
81
|
+
The release workflow publishes 0.14.5 after its version change merges to `main`.
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
package/audits/agents-md.md
CHANGED
|
@@ -179,7 +179,7 @@ The questionnaire is organised by scenario. Each scenario corresponds to a momen
|
|
|
179
179
|
|
|
180
180
|
| # | Question | Expected |
|
|
181
181
|
|---|---|---|
|
|
182
|
-
| 11.1 | Only under the explicit `product.agentsfleet` pack, is its pre-v0.30.0 removal flow rm-file + rm-
|
|
182
|
+
| 11.1 | Only under the explicit `product.agentsfleet` pack, is its pre-v0.30.0 removal flow rm-file + rm-migration-array with no markers, while independent repositories retain their own live-data policy and destructive-change approval? | YES |
|
|
183
183
|
| 11.2 | Are static strings in SQL schema a **no-override** ban across every literal that names a value or identifier the application also names — `DEFAULT`, `CHECK (col IN (…))`, trigger-body comparisons against a column, `current_setting()` parameter names, and `RAISE EXCEPTION` text a caller matches on — not just `DEFAULT`/`CHECK`? | YES |
|
|
184
184
|
|
|
185
185
|
### Scenario 12 — Auto-mode boundary (added)
|
package/audits/msid-ui.sh
CHANGED
|
@@ -99,7 +99,23 @@ case "$MODE" in
|
|
|
99
99
|
EXPLICIT=("$@"); POST_REF=""; LABEL="explicit files" ;;
|
|
100
100
|
esac
|
|
101
101
|
|
|
102
|
-
#
|
|
102
|
+
# --staged/--diff: one git diff piped into one awk — the scan itself costs
|
|
103
|
+
# two processes however large the staged set, plus at most one `git show`
|
|
104
|
+
# per file holding a candidate hit line (zero for a clean diff). Explicit
|
|
105
|
+
# mode keeps a per-file scan so the reported path is the caller's own argv
|
|
106
|
+
# string (git's --no-index headers strip the leading slash of absolute
|
|
107
|
+
# paths, which would misreport the hit and misread the post-image).
|
|
108
|
+
# This gate runs inside a 30-minute pre-commit budget that the previous
|
|
109
|
+
# per-file fork loop blew through at 1,628 staged files.
|
|
110
|
+
#
|
|
111
|
+
# The awk learns the current file from the diff's own `+++ b/<path>` header
|
|
112
|
+
# instead of a shell loop variable. That parse is made safe on purpose:
|
|
113
|
+
# * `--src-prefix=a/ --dst-prefix=b/` pins the prefixes, because user config
|
|
114
|
+
# (diff.noprefix, diff.mnemonicPrefix) may rename them.
|
|
115
|
+
# * git C-quotes paths containing control characters, `"` or `\` (and with
|
|
116
|
+
# the default core.quotePath, non-ASCII bytes — hence `-c
|
|
117
|
+
# core.quotePath=false` on the git calls), so the header is unquoted in
|
|
118
|
+
# awk. git terminates the header path with one TAB, quoted or not.
|
|
103
119
|
#
|
|
104
120
|
# Carve-out recognition reads the preceding line from the SOURCE, not from the
|
|
105
121
|
# diff. Every carve-out here — the two override markers and the design-system
|
|
@@ -112,29 +128,21 @@ esac
|
|
|
112
128
|
# override comment stopped covering the line it sits above the moment that line
|
|
113
129
|
# was next touched.
|
|
114
130
|
#
|
|
115
|
-
# The hunk header carries the post-image line number, so `line_above`
|
|
116
|
-
#
|
|
117
|
-
#
|
|
131
|
+
# The hunk header carries the post-image line number, so `line_above` answers
|
|
132
|
+
# from the post-image the mode named. The post-image is slurped once per file —
|
|
133
|
+
# on the file's first *candidate* hit line, since only candidates (a line
|
|
134
|
+
# matching the MS-ID or UI regexes in a scoped file) ever read the neighbour —
|
|
135
|
+
# and dropped when the next file's `diff --git` section opens. One `git show`
|
|
136
|
+
# per candidate file, zero for a clean diff, and memory bounded by one file.
|
|
118
137
|
scan_diff() {
|
|
119
|
-
|
|
138
|
+
awk -v post_ref="$POST_REF" '
|
|
120
139
|
# BWK awk (macOS) has no gensub and no --posix quoting help; APOSTROPHE is
|
|
121
140
|
# built rather than written, because this program is inside single quotes.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
function line_above(path, n, key, cmd, out) {
|
|
128
|
-
if (n <= 1) return ""
|
|
129
|
-
key = path SUBSEP n
|
|
130
|
-
if (key in above) return above[key]
|
|
131
|
-
above[key] = ""
|
|
132
|
-
if (post_ref == "") cmd = "sed -n " (n - 1) "p " shell_quote(path)
|
|
133
|
-
else cmd = "git show " shell_quote(post_ref path) " 2>/dev/null | sed -n " (n - 1) "p"
|
|
134
|
-
if ((cmd | getline out) > 0) above[key] = out
|
|
135
|
-
close(cmd)
|
|
136
|
-
return above[key]
|
|
137
|
-
}
|
|
141
|
+
# Explicit mode names its file through the environment (see the driver) and
|
|
142
|
+
# the header parse below then never overrides it; the diff modes pass an
|
|
143
|
+
# empty value and every `+++` header re-attributes f.
|
|
144
|
+
BEGIN { APOSTROPHE = sprintf("%c", 39); in_hunk = 0; loaded = 0; imgn = 0
|
|
145
|
+
f = ENVIRON["ORLY_AUDIT_PATH"]; fixed_f = (f != "") }
|
|
138
146
|
|
|
139
147
|
function shell_quote(s, i,c,out) {
|
|
140
148
|
out = APOSTROPHE
|
|
@@ -145,23 +153,113 @@ ORLY_AUDIT_PATH="$1" awk -v post_ref="$POST_REF" '
|
|
|
145
153
|
}
|
|
146
154
|
return out APOSTROPHE
|
|
147
155
|
}
|
|
156
|
+
|
|
157
|
+
# The post-image path a `+++ ` header names: C-quote resolved (git wraps
|
|
158
|
+
# the whole "b/<path>" in quotes), the pinned b/ prefix removed. /dev/null
|
|
159
|
+
# maps to the empty path, which matches no scope below.
|
|
160
|
+
function dst_path(header, rest,out,i,c,nxt,q,oct,v,j) {
|
|
161
|
+
rest = substr(header, 5)
|
|
162
|
+
if (rest == "/dev/null") return ""
|
|
163
|
+
if (substr(rest, 1, 1) == "\"") {
|
|
164
|
+
q = 0
|
|
165
|
+
for (i = length(rest); i >= 2; i--) if (substr(rest, i, 1) == "\"") { q = i; break }
|
|
166
|
+
rest = substr(rest, 2, q - 2)
|
|
167
|
+
out = ""
|
|
168
|
+
i = 1
|
|
169
|
+
while (i <= length(rest)) {
|
|
170
|
+
c = substr(rest, i, 1)
|
|
171
|
+
if (c == "\\") {
|
|
172
|
+
nxt = substr(rest, i + 1, 1)
|
|
173
|
+
if (nxt ~ /[0-7]/) {
|
|
174
|
+
# \ooo octal escape, one to three digits (only control bytes reach
|
|
175
|
+
# here: core.quotePath=false passes non-ASCII through raw).
|
|
176
|
+
oct = nxt
|
|
177
|
+
while (length(oct) < 3 && substr(rest, i + 1 + length(oct), 1) ~ /[0-7]/)
|
|
178
|
+
oct = oct substr(rest, i + 1 + length(oct), 1)
|
|
179
|
+
v = 0
|
|
180
|
+
for (j = 1; j <= length(oct); j++) v = v * 8 + substr(oct, j, 1)
|
|
181
|
+
out = out sprintf("%c", v)
|
|
182
|
+
i += 1 + length(oct)
|
|
183
|
+
} else {
|
|
184
|
+
if (nxt == "a") out = out sprintf("%c", 7)
|
|
185
|
+
else if (nxt == "b") out = out sprintf("%c", 8)
|
|
186
|
+
else if (nxt == "f") out = out "\f"
|
|
187
|
+
else if (nxt == "n") out = out "\n"
|
|
188
|
+
else if (nxt == "r") out = out "\r"
|
|
189
|
+
else if (nxt == "t") out = out "\t"
|
|
190
|
+
else if (nxt == "v") out = out "\v"
|
|
191
|
+
else out = out nxt
|
|
192
|
+
i += 2
|
|
193
|
+
}
|
|
194
|
+
} else { out = out c; i++ }
|
|
195
|
+
}
|
|
196
|
+
rest = out
|
|
197
|
+
} else {
|
|
198
|
+
# Unquoted: drop the single trailing TAB git appends to the header
|
|
199
|
+
# path. A literal tab in a filename forces C-quoting, so a trailing
|
|
200
|
+
# tab here is never name content; a trailing SPACE is, and is kept.
|
|
201
|
+
sub(/\t$/, "", rest)
|
|
202
|
+
}
|
|
203
|
+
sub(/^b\//, "", rest)
|
|
204
|
+
return rest
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function load_postimage( cmd,l) {
|
|
208
|
+
imgn = 0
|
|
209
|
+
if (post_ref == "") {
|
|
210
|
+
while ((getline l < f) > 0) { imgn++; img[imgn] = l }
|
|
211
|
+
close(f)
|
|
212
|
+
} else {
|
|
213
|
+
cmd = "git show " shell_quote(post_ref f) " 2>/dev/null"
|
|
214
|
+
while ((cmd | getline l) > 0) { imgn++; img[imgn] = l }
|
|
215
|
+
close(cmd)
|
|
216
|
+
}
|
|
217
|
+
loaded = 1
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# The source line above post-image line `n` of the current file, or "" when
|
|
221
|
+
# there is none. Reads the slurped post-image; the slurp happens at most
|
|
222
|
+
# once per file section.
|
|
223
|
+
function line_above(n) {
|
|
224
|
+
if (n <= 1) return ""
|
|
225
|
+
if (!loaded) load_postimage()
|
|
226
|
+
if (n - 1 > imgn) return ""
|
|
227
|
+
return img[n - 1]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# A new file section: reset hunk state and drop the previous post-image —
|
|
231
|
+
# lookups only ever concern the file of the open section.
|
|
232
|
+
/^diff --git / {
|
|
233
|
+
in_hunk = 0
|
|
234
|
+
if (loaded) { for (i = 1; i <= imgn; i++) delete img[i]; imgn = 0; loaded = 0 }
|
|
235
|
+
next
|
|
236
|
+
}
|
|
237
|
+
!in_hunk && /^\+\+\+ / { if (!fixed_f) f = dst_path($0); next }
|
|
148
238
|
/^\+\+\+/ { next }
|
|
149
239
|
# `@@ -a,b +c,d @@` — $3 is the post-image start line for this hunk.
|
|
150
|
-
/^@@/ { start=$3; sub(/^\+/,"",start); split(start,parts,","); nl=parts[1]+0; next }
|
|
240
|
+
/^@@/ { in_hunk = 1; start=$3; sub(/^\+/,"",start); split(start,parts,","); nl=parts[1]+0; next }
|
|
151
241
|
/^[^+]/ { next }
|
|
152
242
|
/^\+/ {
|
|
153
243
|
line=$0
|
|
154
244
|
sub(/^\+/,"",line)
|
|
155
|
-
|
|
245
|
+
# The line above is fetched only for candidate hits: every reader of
|
|
246
|
+
# `prev` sits behind one of these two guards, so a line that matches
|
|
247
|
+
# neither regex never pays for the post-image.
|
|
248
|
+
msid_cand = f ~ /\.(zig|sql|ts|tsx|js|jsx|py|rs|go|sh|toml|yaml|json)$/ &&
|
|
249
|
+
f !~ /^(docs|node_modules|vendor|third_party)\// &&
|
|
250
|
+
match(line, /M[0-9]+_[0-9]+|§[0-9]+(\.[0-9]+)+|(^|[^A-Za-z0-9_])T[0-9]+([^A-Za-z0-9_]|$)|(^|[^A-Za-z0-9_])dim [0-9]+\.[0-9]+([^A-Za-z0-9_]|$)/)
|
|
251
|
+
ui_cand = f ~ /^ui\/packages\/app\/.*\.(tsx|jsx)$/ &&
|
|
252
|
+
line ~ /<(section|button|input|dialog|article|nav|header|form)[ \t>\/]/
|
|
253
|
+
if (msid_cand || ui_cand) {
|
|
254
|
+
prev = line_above(nl)
|
|
255
|
+
ms_id_override = (prev ~ /MILESTONE ID ALLOWED per user override/)
|
|
256
|
+
ui_override = (prev ~ /UI GATE: SKIPPED per user override/)
|
|
257
|
+
} else {
|
|
258
|
+
prev = ""; ms_id_override = 0; ui_override = 0
|
|
259
|
+
}
|
|
156
260
|
nl++
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!ms_id_override &&
|
|
160
|
-
f ~ /\.(zig|sql|ts|tsx|js|jsx|py|rs|go|sh|toml|yaml|json)$/ &&
|
|
161
|
-
f !~ /^(docs|node_modules|vendor|third_party)\//) {
|
|
162
|
-
if (match(line, /M[0-9]+_[0-9]+|§[0-9]+(\.[0-9]+)+|(^|[^A-Za-z0-9_])T[0-9]+([^A-Za-z0-9_]|$)|(^|[^A-Za-z0-9_])dim [0-9]+\.[0-9]+([^A-Za-z0-9_]|$)/)) {
|
|
163
|
-
print "MS-ID " f ": " line
|
|
164
|
-
}
|
|
261
|
+
if (msid_cand && !ms_id_override) {
|
|
262
|
+
print "MS-ID " f ": " line
|
|
165
263
|
}
|
|
166
264
|
# react-hook-form caveat: its prescribed shape is a raw
|
|
167
265
|
# <form onSubmit={form.handleSubmit(...)}> inside the design-system <Form>
|
|
@@ -179,9 +277,7 @@ ORLY_AUDIT_PATH="$1" awk -v post_ref="$POST_REF" '
|
|
|
179
277
|
# is above it in the file whether or not this diff touched it. A
|
|
180
278
|
# bare/unwrapped <section> still trips.
|
|
181
279
|
ds_section = (line ~ /<section[ \t]/ && prev ~ /<Section asChild>/)
|
|
182
|
-
if (!ui_override && !rhf_form && !ds_section
|
|
183
|
-
f ~ /^ui\/packages\/app\/.*\.(tsx|jsx)$/ &&
|
|
184
|
-
line ~ /<(section|button|input|dialog|article|nav|header|form)[ \t>\/]/) {
|
|
280
|
+
if (ui_cand && !ui_override && !rhf_form && !ds_section) {
|
|
185
281
|
print "UI " f ": " line
|
|
186
282
|
}
|
|
187
283
|
}
|
|
@@ -193,12 +289,14 @@ hits=$(
|
|
|
193
289
|
for path in "${EXPLICIT[@]}"; do
|
|
194
290
|
[ -f "$path" ] || continue
|
|
195
291
|
# diff returns one for differences; that is expected input here.
|
|
196
|
-
{ git diff --no-index --no-ext-diff -U0 -- /dev/null "$path" || [ "$?" -eq 1 ]; } |
|
|
292
|
+
{ git -c core.quotePath=false diff --no-index --no-ext-diff -U0 --src-prefix=a/ --dst-prefix=b/ -- /dev/null "$path" || [ "$?" -eq 1 ]; } | ORLY_AUDIT_PATH="$path" scan_diff
|
|
197
293
|
done
|
|
198
294
|
else
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
295
|
+
# --no-renames pins a rename to its delete+add shape: this gate scores
|
|
296
|
+
# added lines, and its verdicts are defined over renamed files being
|
|
297
|
+
# re-scanned as whole-file additions. Rename detection would silently
|
|
298
|
+
# stop scanning their contents.
|
|
299
|
+
git -c core.quotePath=false diff --no-ext-diff -U0 --no-renames --diff-filter=ACMRT --src-prefix=a/ --dst-prefix=b/ "${DIFF_ARGS[@]}" | ORLY_AUDIT_PATH="" scan_diff
|
|
202
300
|
fi
|
|
203
301
|
)
|
|
204
302
|
|
package/audits/spec-template.ts
CHANGED
|
@@ -7,7 +7,7 @@ type Section = { title: string; nodes: MarkdownNode[] };
|
|
|
7
7
|
type Commands = Record<string, string[][]>;
|
|
8
8
|
|
|
9
9
|
const CONFIG_PATH = ".orly/orly.json";
|
|
10
|
-
const SPEC_LINE_LIMIT =
|
|
10
|
+
const SPEC_LINE_LIMIT = 500;
|
|
11
11
|
const BACKTICK = "`";
|
|
12
12
|
const EMPTY = "";
|
|
13
13
|
const PIPE_OUTPUT = "pipe";
|
package/core/operating-model.md
CHANGED
|
@@ -11,7 +11,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
|
|
|
11
11
|
|
|
12
12
|
**The human is Kishore** — casual handle **Indy**; either name, any case, addresses him. Ambiguous "the user" / "they" here = Kishore. The agent is **Oracle**, casual handle **Aiwa**. `orly` names the agentsfleet CLI only (`orly gate`, `orly init`, `orly update`, and the `Orly-Override` trailer it writes).
|
|
13
13
|
|
|
14
|
-
**Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `
|
|
14
|
+
**Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `fleet.*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
|
|
15
15
|
|
|
16
16
|
MacBook. Languages: Python, Go, Rust, TypeScript, Zig. Tooling: `mise` first, `brew` fallback. Forges: `gh`/`glab`. Commit identity from `git config`, never here.
|
|
17
17
|
|
package/dispatch/write_sql.md
CHANGED
|
@@ -57,7 +57,6 @@ The **Schema Table Removal Guard** card dissolves into this façade; its prose i
|
|
|
57
57
|
**Triggers** — before any of these, run `cat VERSION` and print the guard output. `0.30.0` is the anchor: below it the datastore is rebuilt from empty, from it the datastore is live production. Compare the version field by field, never as a string — `0.29.0` is under the anchor and `0.30.0` is not, while a lexical compare puts `0.30.0` under `0.9.0` and reads the era backwards.
|
|
58
58
|
|
|
59
59
|
- Creating, editing, or deleting any file under `schema/*.sql`.
|
|
60
|
-
- Editing `schema/embed.zig` (any `@embedFile` constant).
|
|
61
60
|
- Editing the canonical migration array in `rustd/crates/afd_db/src/migration.rs`.
|
|
62
61
|
- Writing `DROP TABLE`, `ALTER TABLE`, or `SELECT 1;` into any SQL file.
|
|
63
62
|
- Accepting a spec dimension prescribing a "DROP migration", "ALTER migration", or "version marker".
|
|
@@ -73,8 +72,7 @@ The era runs while `VERSION` is under `0.30.0`, and it ends there — the develo
|
|
|
73
72
|
To remove a table:
|
|
74
73
|
|
|
75
74
|
1. `rm schema/NNN_foo.sql`
|
|
76
|
-
2. Remove
|
|
77
|
-
3. Remove the entry from the migration array in `rustd/crates/afd_db/src/migration.rs` and update its registration tests.
|
|
75
|
+
2. Remove the entry from the migration array in `rustd/crates/afd_db/src/migration.rs` and update its registration tests.
|
|
78
76
|
|
|
79
77
|
**Forbidden:** `ALTER TABLE`, `DROP TABLE`, `SELECT 1;` markers, comment-only files, "keep file for slot numbering". Slot gaps are fine — the DB is wiped on rebuild.
|
|
80
78
|
|
|
@@ -91,11 +89,10 @@ The teardown path is closed. Every schema change assumes a live datastore holdin
|
|
|
91
89
|
```
|
|
92
90
|
SCHEMA GUARD: VERSION=<v> (<0.30.0 ? teardown : migrate)
|
|
93
91
|
rm:schema/<file>.sql
|
|
94
|
-
rm-embed:<const>
|
|
95
92
|
rm-migration:v<N>
|
|
96
93
|
```
|
|
97
94
|
|
|
98
|
-
Below `0.30.0`,
|
|
95
|
+
Below `0.30.0`, both rm- lines appear. From `0.30.0`, replace them with `migration:schema/<NNN>_<change>.sql`.
|
|
99
96
|
<!-- oracle-packs:end -->
|
|
100
97
|
|
|
101
98
|
## Companion SQL rules (retained in `docs/greptile-learnings/RULES.md`)
|
package/dispatch/write_sql.sh
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# (retained) and the GREPTILE GATE enforces them by review until a leaf exists.
|
|
19
19
|
# SCH's teardown-era forbidden-token floor (no ALTER/DROP/SELECT 1; in schema/*.sql) is
|
|
20
20
|
# itself a build-the-check (TODO-CHECK) candidate; the teardown-vs-ALTER disposition
|
|
21
|
-
# call above it stays JUDGMENT (which file to rm, which
|
|
21
|
+
# call above it stays JUDGMENT (which file to rm, which migration index).
|
|
22
22
|
|
|
23
23
|
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
24
24
|
|
|
@@ -30,7 +30,7 @@ dispatch_header
|
|
|
30
30
|
dispatch_length_gate 350 # FLL — file ≤ 350
|
|
31
31
|
|
|
32
32
|
# ── judgment gates (no script can decide; agent states verdict in chat) ──
|
|
33
|
-
dispatch_judgment "SCH" "removing/altering a table? cat VERSION and compare field by field, not as a string — below 0.30.0 = full teardown (rm schema/NNN.sql +
|
|
33
|
+
dispatch_judgment "SCH" "removing/altering a table? cat VERSION and compare field by field, not as a string — below 0.30.0 = full teardown (rm schema/NNN.sql + migration-array entry + index tests), never ALTER/DROP/SELECT 1; 0.30.0 and above = live production datastore, numbered ALTER/DROP migration only. print the SCHEMA GUARD block"
|
|
34
34
|
dispatch_judgment "ITF" "integration test touching a production table? seed via src/db/test_fixtures_<scope>.zig against the real schema — never CREATE TEMP TABLE mocking the production shape"
|
|
35
35
|
|
|
36
36
|
dispatch_verdict
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.5 — 2026-10-17
|
|
4
|
+
|
|
5
|
+
agentsfleet's schema conventions carry the owner-signed Postgres rules, and specs may run to 500 lines.
|
|
6
|
+
|
|
7
|
+
- **Schema conventions** — placement in owned schemas, key and counter naming, RULE STS in full, timestamp categories, runtime SQL in `PREPARE`-checked `.sql` files, and SQLx's `Migrator` inside a bounded lock. Entities are named `fleet.*`.
|
|
8
|
+
- **Spec cap** — the spec template gate allows 500 lines, up from 320.
|
|
9
|
+
- **Schema guard** — the teardown flow no longer names the retired `schema/embed.zig`.
|
|
10
|
+
|
|
11
|
+
## 0.14.4 — 2026-10-16
|
|
12
|
+
|
|
13
|
+
The milestone-ID and UI-substitution audit no longer forks per staged file. A pre-commit stage of ~1,600 files that ran over 27 minutes now finishes in about two seconds, with verdicts unchanged.
|
|
14
|
+
|
|
15
|
+
- **Scan** — one `git diff` and one `awk` cover the whole staged set. Reading the line above an added line now costs at most one `git show` per file with a candidate hit, down from one fork pair per added line.
|
|
16
|
+
- **Compatibility** — verdicts, hit order and exit codes match 0.14.3 byte-for-byte across staged, `--diff` and explicit-file modes, including spaced, quoted and non-ASCII filenames, renames re-scanned as whole-file additions, and absolute explicit paths.
|
|
17
|
+
|
|
3
18
|
## 0.14.3 — 2026-10-15
|
|
4
19
|
|
|
5
20
|
The REST API guide carries agentsfleet's conversation and reviewed-setup exceptions, and the TypeScript rules name how timestamps are labelled.
|
package/docs/RULE_ENFORCEMENT.md
CHANGED
|
@@ -8,13 +8,13 @@ regenerates this file and fails on any difference. To update it, run
|
|
|
8
8
|
|---|---|--:|--:|--:|---|
|
|
9
9
|
| `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 10 | 13 | 30/30 | mechanical |
|
|
10
10
|
| `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/69 | mechanical |
|
|
11
|
-
| `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/
|
|
11
|
+
| `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/12 | mechanical |
|
|
12
12
|
| `docs/DOCUMENTATION_RULES.md` | — | 0 | 0 | 0/22 | latent |
|
|
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
16
|
| `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/115 | mechanical |
|
|
17
|
-
| **corpus** | | | | **30/
|
|
17
|
+
| **corpus** | | | | **30/277** | |
|
|
18
18
|
|
|
19
19
|
## How to read this
|
|
20
20
|
|
|
@@ -13,102 +13,115 @@ with the real schema through shared fixture helpers.
|
|
|
13
13
|
Keep migration registration and position assertions synchronized with each edit.
|
|
14
14
|
|
|
15
15
|
<!-- oracle-packs:start product.agentsfleet -->
|
|
16
|
-
Canonical reference for `agentsfleet` database schema patterns. All new tables **must** follow these conventions.
|
|
16
|
+
Canonical reference for `agentsfleet` database schema patterns. All new tables **must** follow these conventions. The table-by-table layout lives in the product repository's `docs/architecture/postgres_schemas.md`; this section carries the rules that layout follows.
|
|
17
17
|
|
|
18
18
|
## Migration Model
|
|
19
19
|
|
|
20
|
-
**
|
|
20
|
+
**Additive at `VERSION >= 0.30.0`.** Every schema change is a new numbered slot; shipped slots are frozen history. Destructive changes (`DROP TABLE`, `DROP COLUMN`, type rewrites) need an explicit owner decision per change. A from-empty rebuild past `0.30.0` happens only under an owner-recorded `SCHEMA GUARD: SKIPPED per user override (reason: …)` with the database dropped first — the M229 rebuild (Oct 17, 2026) is the precedent.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
Completing the rebuild alone does not change the version boundary.
|
|
24
|
-
Every subsequent schema change is a new numbered migration file; shipped slots
|
|
25
|
-
are frozen history. Migrations are tracked incrementally through
|
|
26
|
-
`agentsfleetd doctor --schema-gate`; use `IF NOT EXISTS` where appropriate for
|
|
27
|
-
fresh bootstrap and an already provisioned database.
|
|
22
|
+
## Migrator
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
> Historical note: slots `001`–`031` predate the additive model (teardown-rebuild with inline DDL edits, enforced by a since-removed `check-schema-gate` lint target). M154 retires them along with everything up to `046`.
|
|
24
|
+
SQLx's `Migrator` applies the slots from the explicit list in `rustd/crates/afd_db/src/migration.rs` (`Migrator::with_migrations`; the `migrate` feature only, `macros` stays off). It keeps checksums, so an edited applied slot is refused, and records the ledger in the `audit` schema. Locking is ours: SQLx's lock is off (`set_locking(false)`) and a bounded `pg_try_advisory_lock` poll wraps the run, so a held lock fails loudly instead of hanging a deploy. Migrate refuses to start while a ledger from an older migrator exists.
|
|
32
25
|
|
|
33
26
|
## Schema File Organization
|
|
34
27
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- Every
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
- One slot per table: the table, its local constraints, indexes and grants together; each file **≤100 lines**.
|
|
29
|
+
- Slots are ordered by foreign key — a table follows every table it references, across schemas. A foreign key that closes a cycle (a head pointer, for example) gets its own registered slot after both tables exist, `DEFERRABLE INITIALLY DEFERRED`.
|
|
30
|
+
- Every slot is registered in `rustd/crates/afd_db/src/migration.rs`; the `rust-migration-list` lint compares the list to the tracked files.
|
|
31
|
+
- Shipped numbers are never reused or slid.
|
|
32
|
+
|
|
33
|
+
## Placement and naming
|
|
34
|
+
|
|
35
|
+
1. **Placement.** A table lives in the schema of the product area that owns it (`identity`, `authoring`, `fleet`, `runner`, `connector`, `model`, `billing`, `vault`, `memory`). Its name is a plural noun that never repeats the schema name: `authoring.drafts`, not `authoring.authoring_drafts`. A singleton is named for its one row and says so in its slot.
|
|
36
|
+
2. **Foreign keys** end `_id` and name the table they point at (`user_id`, `draft_id`). A prefix names the role when the column alone would not say it (`invited_by_user_id`, `active_grant_id`, `last_runner_id`). A deliberately missing foreign key is explained in the slot comment.
|
|
37
|
+
3. **Status** columns are named `status`; categories are `<thing>_type`. RULE STS applies in full and without override: no literal that names a value or identifier the application also names — a `DEFAULT`, a `CHECK (col IN (…))` list, a trigger comparison against a column, a `current_setting()` parameter name, or `RAISE EXCEPTION` text a caller matches on. A predicate that needs a state reads a NULL marker column; a privileged path (a purge, for example) is a grant, not a session setting. The only literals left are structural ones nothing in the application names: the UUID version nibble `'7'`, `TG_OP` values, and exception text no caller matches on.
|
|
38
|
+
4. **Timestamps.** Every table has `created_at`; every table whose rows change has `updated_at`. Any other time column is one of: (a) a deadline named `*expires_at`; (b) a NULL marker an index or trigger reads; (c) a domain instant the API returns that `updated_at` cannot carry; (d) an accounting cursor billing reads.
|
|
39
|
+
5. **One fact, one home.** No JSON blob copies a column; no counter restates another table's number. Named exceptions, each with its consistency rule in the slot: an aggregate maintained under the same lock or trigger as its source; a delivery snapshot that must outlive its source; a non-secret projection of encrypted content; a scope copy a restricted role needs to filter alone.
|
|
40
|
+
6. **Retry keys** (`operation_id` plus a request digest) live only in receipt tables.
|
|
41
|
+
7. **Every column names its reader** before it is created. Accounting and audit records are exempt when the slot names the export or verification that reads them.
|
|
42
|
+
8. **A new table** carries a written answer to "why not a column, or an existing table".
|
|
43
|
+
9. **Immutable tables** carry no `updated_at`, no UPDATE grant, and a trigger that refuses UPDATE. DELETE stays governed by grants and the deletion matrix, so cascades keep working.
|
|
44
|
+
|
|
45
|
+
### Counter and pointer names
|
|
46
|
+
|
|
47
|
+
| Kind | Name |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Position within a parent (1, 2, 3…) | `sequence` |
|
|
50
|
+
| Pointer to another table's position | `<thing>_sequence` |
|
|
51
|
+
| Highest position so far | `last_<thing>_sequence` |
|
|
52
|
+
| "Everything up to N" watermark | `<verb>_through_sequence` |
|
|
53
|
+
| Global insertion order | `sequence` (identity column) |
|
|
54
|
+
| Frozen content version | `revision`; pointer `<x>_revision_id`; the live one `head_revision_id` |
|
|
55
|
+
| Compare-and-swap edit counter | `resource_version`, or `<scope>_version` when it covers one action |
|
|
56
|
+
| Lease epoch | `fencing_token` |
|
|
57
|
+
| Tally | `<thing>_count` |
|
|
58
|
+
| Deadline | `*expires_at` |
|
|
59
|
+
|
|
60
|
+
Wire field names (API and runner protocol) are not renamed by these rules.
|
|
61
|
+
|
|
62
|
+
## Deletion matrix
|
|
63
|
+
|
|
64
|
+
Every root a user or operator can delete — draft, user, Library card, fleet, workspace, tenant — has a stated row in the product's deletion matrix: what goes with it, what stays, and what is scrubbed at once. Money records (charges, settlements, the attempts they hang off) never cascade from an erasable owner; they keep opaque identifiers. Private text is scrubbed on every erasure path, not only the one that purges drafts.
|
|
41
65
|
|
|
42
66
|
## SQL Qualification
|
|
43
67
|
|
|
44
|
-
- Use schema-qualified table names in SQL (`
|
|
68
|
+
- Use schema-qualified table names in SQL (`identity.workspaces`, `fleet.fleets`).
|
|
45
69
|
- Do not rely on session `search_path` defaults for correctness.
|
|
46
|
-
|
|
70
|
+
|
|
71
|
+
## Runtime SQL
|
|
72
|
+
|
|
73
|
+
- Every static statement lives in a `.sql` file beside the use case that runs it (for example `library/sql/list.sql`), loaded with `include_str!` and bound by a typed Rust function that states its parameters, authorization and locks.
|
|
74
|
+
- Rows decode into named `FromRow` types selected by explicit column lists; `Json<T>` is for real JSON columns only; storage rows stay separate from wire types. No `to_jsonb(<row>)` whole-row reads.
|
|
75
|
+
- One integration test `PREPARE`s every `.sql` file against the rebuilt schema, so a stale table or column fails before merge. A statement that must be assembled at run time is named in its module and covered by its own test.
|
|
76
|
+
- Domain stores take the caller's connection or transaction; there is no generic repository trait.
|
|
77
|
+
- Every pooled runtime connection carries `lock_timeout` and `statement_timeout` named constants; the migrate connection carries neither.
|
|
47
78
|
|
|
48
79
|
## Schema-Backed Runtime Defaults
|
|
49
80
|
|
|
50
|
-
-
|
|
51
|
-
- Keep the
|
|
52
|
-
- Add an adjacent `Canonical constant:` SQL comment next to each shared numeric default so reviewers can verify the linkage quickly.
|
|
81
|
+
- A numeric schema default also used in runtime fallback or provisioning logic is defined once as a Rust named constant in the owning crate and referenced by the slot comment (`Canonical constant: crate::path::NAME`).
|
|
82
|
+
- Keep the DDL value unchanged unless the product default is intentionally changing.
|
|
53
83
|
|
|
54
84
|
## Identity Column
|
|
55
85
|
|
|
56
|
-
**One identity column per table, named `id` (owner decision, Indy, Jul 31, 2026 — M154).**
|
|
86
|
+
**One identity column per table, named `id` (owner decision, Indy, Jul 31, 2026 — M154).**
|
|
57
87
|
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
- **Generation:** Application-side UUID version 7 (UUIDv7) via `rustd/crates/afd_core/src/id.rs`, never `gen_random_uuid()`.
|
|
61
|
-
- **No second unique key on the same value.** This is a correctness rule, not only a storage one: `ON CONFLICT` can arbitrate exactly one constraint, so a table carrying two unique keys over the same value makes two sessions inserting a brand-new row race to a duplicate-key error on the *other* index instead of taking the update arm. `schema/043_runner_lifetime_counters.sql` recorded this before the rule was generalised.
|
|
88
|
+
- **Type:** Universally Unique Identifier (UUID) `PRIMARY KEY`, application-minted UUID version 7 (UUIDv7) through `afd_core::id`, never `gen_random_uuid()`.
|
|
89
|
+
- **No second unique key on the same value.** `ON CONFLICT` arbitrates exactly one constraint, so two unique keys over one value make concurrent inserts race to a duplicate-key error on the other index.
|
|
62
90
|
- **Foreign keys reference the primary key**, never a secondary unique constraint.
|
|
63
|
-
- **Constraint:** every table carries
|
|
64
|
-
|
|
65
|
-
CONSTRAINT ck_{table}_id_uuidv7 CHECK (substring(id::text from 15 for 1) = '7')
|
|
66
|
-
```
|
|
67
|
-
- **Adding a new table:** add a `generate{TableName}Id()` function to `rustd/crates/afd_core/src/id.rs`.
|
|
68
|
-
- **API shape:** unchanged — public fields keep exposing `id`, `tenant_id`, `workspace_id` and the other documented names. The column now *is* the public name in most tables, so the aliasing the old rule required mostly disappears; where a public field differs from `id`, alias at the boundary rather than renaming client-facing payloads.
|
|
69
|
-
- **Exceptions,** each stated in the slot that creates the table: a curated catalogue keyed by a stable slug (`core.fleet_library`) and a singleton keyed by a pinned integer (`core.model_catalogue_revision`) carry no UUID.
|
|
91
|
+
- **Constraint:** every UUID-keyed table carries `CONSTRAINT ck_{table}_id_uuidv7 CHECK (substring(id::text from 15 for 1) = '7')`.
|
|
92
|
+
- **Exceptions,** each stated in its slot: a row that exists at most once per parent is keyed by the parent (`fleet.sessions`, `fleet.activity_counters`, `runner.fleet_slots`, `runner.lifetime_counters`, `billing.wallets`, `model.tenant_selections`); a singleton keyed by a pinned integer (`model.catalogue_version`); a natural key addressed by every statement (`model.platform_defaults` by `provider`).
|
|
70
93
|
|
|
71
94
|
## Timestamps
|
|
72
95
|
|
|
73
|
-
- **Type:** `BIGINT NOT NULL` — milliseconds since Unix epoch.
|
|
74
|
-
- **
|
|
75
|
-
- **Never** use `TIMESTAMPTZ`, `TIMESTAMP`, or `DEFAULT now()`.
|
|
96
|
+
- **Type:** `BIGINT NOT NULL` — milliseconds since the Unix epoch, from the application clock (`afd_core::clock::UnixMillis`).
|
|
97
|
+
- **Never** `TIMESTAMPTZ`, `TIMESTAMP` or `DEFAULT now()`.
|
|
76
98
|
|
|
77
99
|
## Standard Columns
|
|
78
100
|
|
|
79
|
-
Every table must have:
|
|
80
|
-
|
|
81
101
|
| Column | Type | Required | Notes |
|
|
82
102
|
|--------|------|----------|-------|
|
|
83
|
-
| `id` | `UUID PRIMARY KEY` | Yes | UUIDv7,
|
|
103
|
+
| `id` | `UUID PRIMARY KEY` | Yes | UUIDv7, application-minted |
|
|
84
104
|
| `created_at` | `BIGINT NOT NULL` | Yes | Set once at INSERT |
|
|
85
|
-
| `updated_at` | `BIGINT NOT NULL` | If
|
|
86
|
-
|
|
87
|
-
**Naming is uniform.** `created_at` and `updated_at` are the only names for row lifecycle time — not `recorded_at`, `created_at_ms`, or `updated_at_ms`. A column carrying *domain* time distinct from row birth (for example the originating event's creation instant on a money row) is named for that domain meaning and documented in its slot.
|
|
88
|
-
|
|
89
|
-
**Mutable tables** (any table where UPDATE is a valid operation) must have `updated_at`.
|
|
90
|
-
|
|
91
|
-
**Append-only/event tables** (where UPDATE is blocked by trigger or by design) are exempt from `updated_at`.
|
|
105
|
+
| `updated_at` | `BIGINT NOT NULL` | If the row ever changes | Set at INSERT and every UPDATE |
|
|
92
106
|
|
|
93
107
|
## Audit Pattern
|
|
94
108
|
|
|
95
|
-
- **Actor tracking:**
|
|
96
|
-
- **No `updated_by` column.** Changes to mutable tables are tracked
|
|
97
|
-
- **Append-only enforcement:**
|
|
109
|
+
- **Actor tracking:** `actor TEXT` in event and transition tables.
|
|
110
|
+
- **No `updated_by` column.** Changes to mutable tables are tracked in event tables, not inline.
|
|
111
|
+
- **Append-only enforcement:** an immutable table refuses UPDATE by trigger; DELETE follows grants and the deletion matrix.
|
|
98
112
|
```sql
|
|
99
|
-
CREATE OR REPLACE FUNCTION
|
|
113
|
+
CREATE OR REPLACE FUNCTION {schema}.{table}_refuse_update() RETURNS trigger AS $$
|
|
100
114
|
BEGIN
|
|
101
|
-
RAISE EXCEPTION '{table} is append-only
|
|
115
|
+
RAISE EXCEPTION '{table} is append-only';
|
|
102
116
|
END;
|
|
103
117
|
$$ LANGUAGE plpgsql;
|
|
104
118
|
|
|
105
|
-
CREATE TRIGGER trg_{table}
|
|
106
|
-
BEFORE UPDATE
|
|
107
|
-
FOR EACH ROW EXECUTE FUNCTION
|
|
119
|
+
CREATE TRIGGER trg_{table}_refuse_update
|
|
120
|
+
BEFORE UPDATE ON {schema}.{table}
|
|
121
|
+
FOR EACH ROW EXECUTE FUNCTION {schema}.{table}_refuse_update();
|
|
108
122
|
```
|
|
109
123
|
|
|
110
|
-
##
|
|
124
|
+
## Randomness
|
|
111
125
|
|
|
112
|
-
-
|
|
113
|
-
- **Never** use custom RNG implementations or `std.rand`.
|
|
126
|
+
- Identifiers and secrets come from `afd_core::id` and the operating system's cryptographic source; never a custom random number generator.
|
|
114
127
|
<!-- oracle-packs:end -->
|
package/docs/TEMPLATE.md
CHANGED
|
@@ -17,7 +17,7 @@ Three markup classes appear below the divider. The gate tells them apart mechani
|
|
|
17
17
|
| `tpl:` guidance comment | How to fill the section — written for the authoring agent only | **Deleted after filling.** The gate BLOCKs any survivor. |
|
|
18
18
|
| SPEC AUTHORING RULES banner comment | Standing constraints on the instance | The one comment that survives, verbatim. |
|
|
19
19
|
|
|
20
|
-
Everything else — headings, table skeletons, the standard rubric rows — is kept and filled. The result: the executing agent reads **100% instance content, zero template noise**, and the
|
|
20
|
+
Everything else — headings, table skeletons, the standard rubric rows — is kept and filled. The result: the executing agent reads **100% instance content, zero template noise**, and the 500-line budget buys signal, not boilerplate.
|
|
21
21
|
|
|
22
22
|
## What a spec pins
|
|
23
23
|
|
|
@@ -89,7 +89,7 @@ A milestone is not complete until evidence is captured: commands, logs, screensh
|
|
|
89
89
|
|
|
90
90
|
## Guardrails
|
|
91
91
|
|
|
92
|
-
- **Length — hard upper bound
|
|
92
|
+
- **Length — hard upper bound 500 lines for the filled spec** (after `tpl:` comment deletion). Typical specs 150–350. Past the bound before the Acceptance Rubric → scope is two specs sharing one file; split.
|
|
93
93
|
- **Sections** — 3–9. **Workstreams** — ≤4 (5th cross-cutting only).
|
|
94
94
|
- **File naming** — `docs/v{N}/{pending|active|done}/M{N}_{NNN}_P{Priority}_{CATEGORIES}_{NAME}.md` (e.g. `M40_001_P0_API_WORKER_SUBSTRATE.md`). Categories alphabetised.
|
|
95
95
|
|
|
@@ -409,7 +409,7 @@ another product's version number never authorizes a rebuild here.
|
|
|
409
409
|
|
|
410
410
|
<!-- oracle-packs:start product.agentsfleet -->
|
|
411
411
|
|
|
412
|
-
**Rule:** While `cat VERSION` < 0.30.0 (teardown-rebuild era), removing tables MUST be a full teardown: (1) delete the SQL file (`rm schema/NNN_foo.sql`), (2) remove the
|
|
412
|
+
**Rule:** While `cat VERSION` < 0.30.0 (teardown-rebuild era), removing tables MUST be a full teardown: (1) delete the SQL file (`rm schema/NNN_foo.sql`), (2) remove the migration array entry from `rustd/crates/afd_db/src/migration.rs` and update its registration tests. Never write ALTER TABLE, DROP TABLE, or `SELECT 1;` placeholders. Never keep version-marker files. Migration slot numbers are not sacred pre-v0.30 — the DB is wiped on every rebuild, and gaps in numbering are fine. From VERSION >= 0.30.0 the datastore is live production data, so every removal or reshape is a numbered ALTER/DROP migration and the teardown path is closed. **Compare the version field by field, never as a string:** `0.29.0` is under the anchor, `0.30.0` is not, and a lexical compare puts `0.30.0` below `0.9.0` — the anchor moved off a major-number boundary precisely so the minor field decides.
|
|
413
413
|
**Why:** Markers accumulate dead code and still force CI to splitter-parse them. Below 0.30.0 the development database is dropped and re-created from empty, so there is no production data to protect and full removal leaves no false grep hits or stale migration slots. At 0.30.0 that stops being true: the deployment holds data nobody can re-create, which is what makes an in-place migration the only safe shape from then on.
|
|
414
414
|
**Tags:** sql, process
|
|
415
415
|
**Example:** harness teardown — supersedes prior "replace with `SELECT 1;`" guidance. Under the old rule, comment-only markers broke CI (apostrophe in "slots" opened unterminated string in splitter); full deletion avoids the marker problem entirely.
|
|
@@ -1004,7 +1004,7 @@ Never infer the absence of deployed clients from another product's version.
|
|
|
1004
1004
|
- RPC / handler signature changes → edit the existing struct + every caller in the same commit. No `Foo` and `FooV2`.
|
|
1005
1005
|
- "Versioned" / "additive optional fields" framing in a spec is fine — what's banned is a *parallel* type, not nullable extensions on the existing one.
|
|
1006
1006
|
- Removing a behavior → delete the function, the call sites, and any dispatch arm that selected it. Tests of the removed path go too.
|
|
1007
|
-
- Schema removals already covered by the Schema Table Removal Guard (rm file, rm
|
|
1007
|
+
- Schema removals already covered by the Schema Table Removal Guard (rm file, rm migration array entry) — that's this rule applied to SQL.
|
|
1008
1008
|
- Legacy-Design Consult Guard still fires when you find a *pre-existing* legacy shim left over from the v1→v2 teardown. This rule says: don't create new ones.
|
|
1009
1009
|
- Command-line renames follow the same rule: if the product verb moves to `create`, do not add `add` as an alias. If a flag or command spelling changes, use the new spelling only unless Indy explicitly asks for a compatibility alias in the same session.
|
|
1010
1010
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
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",
|
|
@@ -44,7 +44,7 @@ describe("opt-in pack selection", () => {
|
|
|
44
44
|
const installed = await Promise.all(pages.map((path) => Bun.file(join(repo, ".orly", path)).text()));
|
|
45
45
|
for (const content of installed) {
|
|
46
46
|
expect(content).not.toContain("playbooks/founding/");
|
|
47
|
-
expect(content).not.toContain("
|
|
47
|
+
expect(content).not.toContain("rustd/crates/afd_db/src/migration.rs");
|
|
48
48
|
expect(content).not.toContain("src/cmd/common.zig");
|
|
49
49
|
expect(content).not.toContain("build_runner.zig");
|
|
50
50
|
expect(content).not.toContain("~/Projects/docs/");
|
|
@@ -73,7 +73,7 @@ describe("opt-in pack selection", () => {
|
|
|
73
73
|
const sql = await Bun.file(join(repo, ".orly/dispatch/write_sql.md")).text();
|
|
74
74
|
const zig = await Bun.file(join(repo, ".orly/dispatch/write_zig.md")).text();
|
|
75
75
|
expect(sql).toContain("0.30.0");
|
|
76
|
-
expect(sql).toContain("
|
|
76
|
+
expect(sql).toContain("rustd/crates/afd_db/src/migration.rs");
|
|
77
77
|
expect(sql).toContain("**Forbidden:**");
|
|
78
78
|
expect(zig).toContain("build_runner.zig");
|
|
79
79
|
});
|
|
@@ -30,6 +30,10 @@ const ERROR_TYPE = "pub struct Error;\n";
|
|
|
30
30
|
const VALUE_INIT = "pub fn init() Self { return .{}; }\n";
|
|
31
31
|
const ALLOCATING_INIT = "pub fn init(a: std.mem.Allocator) !Self { const owned = try a.alloc(u8, COUNT); return .{ .owned = owned }; }\n";
|
|
32
32
|
const DIFF = "--diff";
|
|
33
|
+
const SPACED = "lib/has space.ts";
|
|
34
|
+
const TRAILING_SPACE = "lib/trail .ts";
|
|
35
|
+
const OVERRIDE = "MILESTONE ID ALLOWED per user override (reason: pin)";
|
|
36
|
+
const MARKED_T = 'const t = "T' + '7";\n';
|
|
33
37
|
const SCANNERS = ["ufs.sh", "logging.sh", "deinit-pairs.sh", "sql-mod.sh", "error-codes.sh", "rust-error.sh", "design-tokens.sh"];
|
|
34
38
|
|
|
35
39
|
setDefaultTimeout(TEST_TIMEOUT_MS);
|
|
@@ -189,6 +193,39 @@ test("diff marker explicit files use working bytes and missing comparisons refus
|
|
|
189
193
|
expect(missing.text).toContain("comparison revision is missing: missing-revision");
|
|
190
194
|
});
|
|
191
195
|
|
|
196
|
+
test("diff markers survive spaced and trailing-space filenames", async () => {
|
|
197
|
+
const project = newRepository();
|
|
198
|
+
await staged(project, SPACED, MARKED);
|
|
199
|
+
await staged(project, TRAILING_SPACE, MARKED);
|
|
200
|
+
const result = scan(project, "audits/msid-ui.sh", [STAGED]);
|
|
201
|
+
expect(result.code).toBe(1);
|
|
202
|
+
expect(result.text).toContain(`MS-ID ${SPACED}: // ${MARKER}`);
|
|
203
|
+
expect(result.text).toContain(`MS-ID ${TRAILING_SPACE}: // ${MARKER}`);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("pure rename re-scans the file as a whole-file addition", async () => {
|
|
207
|
+
const project = newRepository();
|
|
208
|
+
await staged(project, "lib/old.ts", MARKED);
|
|
209
|
+
git(project, COMMIT, "-qm", "test: seed rename");
|
|
210
|
+
git(project, "mv", "lib/old.ts", "lib/new.ts");
|
|
211
|
+
const result = scan(project, "audits/msid-ui.sh", [STAGED]);
|
|
212
|
+
expect(result.code).toBe(1);
|
|
213
|
+
expect(result.text).toContain(`MS-ID lib/new.ts: // ${MARKER}`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("explicit absolute paths keep the caller path and read its post-image", async () => {
|
|
217
|
+
const project = newRepository();
|
|
218
|
+
const absolute = join(temporaryDirectory(), "outside.ts");
|
|
219
|
+
await Bun.write(absolute, `// ${OVERRIDE}\n${MARKED_T}`);
|
|
220
|
+
const suppressed = scan(project, "audits/msid-ui.sh", [absolute]);
|
|
221
|
+
expect(suppressed.code).toBe(0);
|
|
222
|
+
expect(suppressed.text).toContain("0 hits");
|
|
223
|
+
await Bun.write(absolute, MARKED_T);
|
|
224
|
+
const flagged = scan(project, "audits/msid-ui.sh", [absolute]);
|
|
225
|
+
expect(flagged.code).toBe(1);
|
|
226
|
+
expect(flagged.text).toContain(`MS-ID ${absolute}: const t = "T` + `7";`);
|
|
227
|
+
});
|
|
228
|
+
|
|
192
229
|
test("indexed source links cannot read outside their owned snapshot", async () => {
|
|
193
230
|
const project = newRepository();
|
|
194
231
|
const external = join(temporaryDirectory(), "owner.ts");
|
|
@@ -104,6 +104,13 @@ describe("spec content readiness", () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
test("oversize specs report their measured size against the cap", () => {
|
|
107
|
-
expect(validateSpec(`${VALID}\n${"line\n".repeat(
|
|
107
|
+
expect(validateSpec(`${VALID}\n${"line\n".repeat(500)}`, COMMANDS, EXISTS).join("\n")).toMatch(/spec has \d+ lines; cap 500/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a spec at the cap passes and one line over fails", () => {
|
|
111
|
+
const base = VALID.trimEnd();
|
|
112
|
+
const padded = (lines: number) => `${base}\n${Array(lines - base.split(/\r?\n/).length).fill("line").join("\n")}`;
|
|
113
|
+
expect(validateSpec(padded(500), COMMANDS, EXISTS).join("\n")).not.toMatch(/cap 500/);
|
|
114
|
+
expect(validateSpec(padded(501), COMMANDS, EXISTS)).toContain("spec has 501 lines; cap 500");
|
|
108
115
|
});
|
|
109
116
|
});
|