@agentsfleet/orly 0.14.3 → 0.14.4
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/README.md +1 -1
- package/audits/msid-ui.sh +136 -38
- package/docs/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/scanner_scope.test.ts +37 -0
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.4 after its version change merges to `main`.
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
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/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.4 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
- **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.
|
|
9
|
+
|
|
3
10
|
## 0.14.3 — 2026-10-15
|
|
4
11
|
|
|
5
12
|
The REST API guide carries agentsfleet's conversation and reviewed-setup exceptions, and the TypeScript rules name how timestamps are labelled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
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",
|
|
@@ -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");
|