@agentsfleet/orly 0.14.2 → 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 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.2 after its version change merges to `main`.
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
- # Single awk pass; reads stdin (the unified diff).
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` can fetch
116
- # the neighbouring line out of the post-image the mode named. Same shape as
117
- # audits/design-tokens.sh, which has always read its override out of the file.
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
- ORLY_AUDIT_PATH="$1" awk -v post_ref="$POST_REF" '
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
- BEGIN { APOSTROPHE = sprintf("%c", 39); f = ENVIRON["ORLY_AUDIT_PATH"] }
123
-
124
- # The source line above line `n` of `path`, from the post-image, or "" when
125
- # there is none. Cached: a hunk may ask about the same neighbour repeatedly,
126
- # and each miss costs a fork.
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
- prev = line_above(f, nl)
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
- ms_id_override = (prev ~ /MILESTONE ID ALLOWED per user override/)
158
- ui_override = (prev ~ /UI GATE: SKIPPED per user override/)
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 ]; } | scan_diff "$path"
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
- while IFS= read -r -d '' path; do
200
- git diff --no-ext-diff -U0 "${DIFF_ARGS[@]}" -- "$path" | scan_diff "$path"
201
- done < <(git diff --name-only -z --diff-filter=ACMRT "${DIFF_ARGS[@]}")
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
 
@@ -313,6 +313,8 @@ The TypeScript-relevant gate cards (`ui-substitution`, `design-token`) dissolve
313
313
 
314
314
  The design-system package (`ui/packages/design-system/src/index.ts` exports — `Section`, `Card`, `Badge`, `Button`, `Input`, `Dialog`, `Pagination`, `EmptyState`, `Tooltip`, `List`/`ListItem`, `WakePulse`, `Terminal`, `InstallBlock`, etc.) is the source of truth for visual primitives. Raw HTML in either dashboard or marketing files drifts from those tokens silently.
315
315
 
316
+ **Record dates.** Label creation timestamps **Created** across tables and detail views. Other timestamps name their event: **Updated**, **Last used**, **Occurred**, **Expires**, or **Joined / invited**. Reuse shared app labels; avoid generic **Time** or **Date** headings. Render dates with the shared `Time` component.
317
+
316
318
  **Marketing-display typography — primitives now exist.** `<DisplayXL>` (marketing hero `<h1>`, `text-fluid-hero`) and `<DisplayLG>` (section heads `<h2>`, `text-fluid-display-lg`) ship from `@agentsfleet/design-system` and compose `font-mono` + the fluid text/leading/tracking tokens internally. Use them on marketing-display surfaces. Raw `<h1>+utilities` is no longer the carve-out — UI GATE blocks it. The dashboard's `<PageTitle>` (`text-heading ≈ 18px`) remains the correct primitive for app surfaces.
317
319
 
318
320
  This gate enforces "use the primitive when one exists" without enumerating the primitives in this rule (so the rule scales as the design-system grows).
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
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
+
10
+ ## 0.14.3 — 2026-10-15
11
+
12
+ The REST API guide carries agentsfleet's conversation and reviewed-setup exceptions, and the TypeScript rules name how timestamps are labelled.
13
+
14
+ - **Conversation sends** — fleet and draft `/messages` sends take one UUIDv7 `operation_id` in the body and refuse an `Idempotency-Key` header; replay, conflict and `202` answers are defined.
15
+ - **Stale reviews** — a reviewed setup mutation refused with `409` also returns the current `review_digest` and `current_state`.
16
+ - **Record dates** — creation timestamps are labelled **Created**; other timestamps name their event.
17
+
3
18
  ## 0.14.2 — 2026-10-15
4
19
 
5
20
  The release inventory reads npm 12's pack output, and the API and logging guides cite agentsfleet's current module paths.
@@ -142,7 +142,19 @@ If the parent ID is in the path, never repeat it in the body.
142
142
 
143
143
  **PATCH idempotency guarantee** — PATCH MUST be idempotent unless the spec's "Failure Modes" section explicitly declares otherwise with a reason. Idempotent PATCH means: issuing the same body twice in succession produces identical row state and identical 200 responses. A negative test that issues the same PATCH twice and asserts equality is required (§10).
144
144
 
145
- **Idempotency keys for non-idempotent POSTs.** Any POST that creates a billable, externally-visible, or side-effecting resource MUST accept an `Idempotency-Key` request header (UUIDv7 from the client). The server stores `(workspace_id, key) → response` for at least 24 hours and replays the prior response on duplicate. Endpoints exempt: pure RPC reads, internal-only POSTs. List which endpoints honor the key in the OpenAPI description.
145
+ **Idempotency keys for non-idempotent POSTs.** Any POST that creates a billable, externally-visible, or side-effecting resource MUST accept an `Idempotency-Key` request header (UUIDv7 from the client), except the conversation rules below. The server stores `(workspace_id, key) → response` for at least 24 hours and replays the prior response on duplicate. Endpoints exempt: pure RPC reads, internal-only POSTs. List which endpoints honor the key in the OpenAPI description.
146
+
147
+ **Owner-approved conversation exception.** Fleet and creator-private draft
148
+ `/messages` sends require one UUIDv7 `operation_id` in the JSON body. They MUST
149
+ reject an `Idempotency-Key` header. Draft creation uses the same body key and
150
+ atomically records its optional first message. Authorization and lifecycle
151
+ checks apply before replay. The same key, normalized message and actor recover
152
+ the accepted message; changed content or actor conflicts. Sends return
153
+ `202 {status, sequence, replayed}` for drafts, or the fleet's accepted event
154
+ receipt, with `Location` pointing to that conversation's messages collection.
155
+ Pending state, answers and usage are read through messages and its authorized
156
+ live stream. These resources create neither a public turn-status resource nor
157
+ a generic operation. Creator-private text never appears in workspace streams.
146
158
 
147
159
  **Owner-approved workspace exception.** `POST /v1/workspaces` accepts no replay
148
160
  key. It requires a tenant-unique name and performs one tenant-scoped insert with
@@ -156,7 +168,7 @@ generalize this exception to other POST endpoints.
156
168
 
157
169
  ### Long-running operations
158
170
 
159
- Endpoints that can't complete inside the request window MUST follow this convention — no per-endpoint reinvention:
171
+ Endpoints that can't complete inside the request window MUST follow this convention, except conversation acceptance described above:
160
172
 
161
173
  1. The kicking POST returns `202 Accepted` with `Location: /v1/operations/{operation_id}` and a body containing `operation_id` and `status: "pending"`.
162
174
  2. `GET /v1/operations/{operation_id}` returns:
@@ -434,6 +446,14 @@ the caller gets the registry's 500 and no internal detail.
434
446
  - `409` MUST include `current_state: <string>` naming the state that forbade the transition (e.g. `"current_state": "approved"` when the caller tried to approve an already-approved gate).
435
447
  - `412` MUST include `etag: <string>` carrying the resource's current ETag so the client can refetch and retry.
436
448
 
449
+ <!-- oracle-packs:start product.agentsfleet -->
450
+ Reviewed setup mutations in `agentsfleet` additionally return `review_digest`
451
+ on a stale-review `409`. The daemon computes this opaque digest from the current
452
+ effective setup and access/model resolution versions. The response also carries
453
+ `current_state`; clients fetch the current review before submitting again.
454
+ This extension grants no setup or runtime authority.
455
+ <!-- oracle-packs:end -->
456
+
437
457
  Don't invent other extensions without amending this doc.
438
458
 
439
459
  ---
@@ -7,14 +7,14 @@ regenerates this file and fails on any difference. To update it, run
7
7
  | Rule document | enforced by | judged | acknowledged | classified | trigger |
8
8
  |---|---|--:|--:|--:|---|
9
9
  | `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 10 | 13 | 30/30 | mechanical |
10
- | `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/67 | mechanical |
10
+ | `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/69 | mechanical |
11
11
  | `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/17 | 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/280** | |
17
+ | **corpus** | | | | **30/282** | |
18
18
 
19
19
  ## How to read this
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.14.2",
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");