@sriinnu/omit 0.3.0 → 0.4.1

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.
@@ -0,0 +1,356 @@
1
+ // Receipts: the agent's claims, made checkable by something other than the agent.
2
+ //
3
+ // A citation only its author can read is a self-report. Every receipt here names
4
+ // the thing it claims and the evidence for it in a form this module checks
5
+ // itself — a path/line/symbol it opens, a symbol it searches the repo for, or a
6
+ // snippet it runs. A receipt that fails its check is not a receipt; it is the
7
+ // fabrication the ledger exists to catch.
8
+ //
9
+ // Accepted shapes, one per JSON line in .omit/receipts.jsonl:
10
+ //
11
+ // {"claim":"reuse","rung":2,"file":"src/util.ts","line":42,"symbol":"parseRange"}
12
+ // {"claim":"stdlib","rung":3,"api":"crypto.randomUUID","run":["node","-e","crypto.randomUUID()"]}
13
+ // {"claim":"installed-dep","rung":5,"dep":"zod","run":["node","-e","require('zod').object"]}
14
+ // {"claim":"new-dep","rung":7,"dep":"left-pad","tried":[{"rung":2,"absent":"padTo"}]}
15
+ //
16
+ // EVIDENCE IS BOUND TO THE CLAIM. That is the whole design, and it is why the
17
+ // shapes look fussy. A snippet's exit status alone proves nothing — `["true"]`
18
+ // exits 0 and `["false"]` exits 1 while testing neither the standard library nor
19
+ // any dependency — so `run` must also name what it exercises (`api` or `dep`)
20
+ // AND the argv text must mention it. Without that binding, the cheapest way to
21
+ // satisfy a check that demands failure is to write a citation that cannot be
22
+ // true, and the mechanism ends up rewarding fabricated failure.
23
+ //
24
+ // `new-dep` is the strongest claim here: it asserts the earlier omissions were
25
+ // TRIED and did not hold. So a `tried` entry cites an absence to be reconstructed
26
+ // (a symbol to search the repo for) rather than a path to be taken on trust —
27
+ // the verifier does the searching, not the author.
28
+ //
29
+ // Trust model: `.omit/receipts.jsonl` is executable in the same sense a Makefile
30
+ // is — `run` is spawned. argv arrays, never a shell, so nothing expands; a
31
+ // SIGKILL timeout and a total budget cap it. Reading a checkout you don't trust?
32
+ // OMIT_NO_EXEC=1 downgrades every `run` check to "unverifiable" instead of
33
+ // executing it.
34
+ //
35
+ // omitted: requiring a `reuse` citation to exist at HEAD: a working-tree read is
36
+ // what a staged-file workflow needs, and citing a file written moments ago makes
37
+ // a `tried` entry HOLD (refuting the receipt), which is the safe direction. Add
38
+ // the revision check if a receipt ever cites new code as if it were pre-existing.
39
+ // omitted: a filesystem sandbox: out of scope for a zero-dependency CLI, and the
40
+ // honest boundary is the opt-in, not a pretence of confinement.
41
+ // omitted: proving a snippet SEMANTICALLY exercises its subject. The binding is
42
+ // textual — the argv must name what it claims to test — which closes snippets
43
+ // that exit on demand (`["true"]`, `["false"]`) but not one that names a symbol
44
+ // without calling it. Real proof would mean the verifier writes the snippet, and
45
+ // then it is testing the verifier, not the claim.
46
+ import { lstatSync, readFileSync, realpathSync, statSync } from 'node:fs'
47
+ import { spawnSync } from 'node:child_process'
48
+ import { basename, join, relative, resolve } from 'node:path'
49
+ import { isManifest, parseDeps, MANIFESTS } from './deps.mjs'
50
+ import { execDisabled } from './exec.mjs'
51
+ import { probe, repoRoot } from './git.mjs'
52
+
53
+ export const LEDGER = join('.omit', 'receipts.jsonl')
54
+
55
+ const SYMBOL_WINDOW = 5
56
+ const RUN_TIMEOUT_MS = 10_000
57
+ // Bounds, because cost is otherwise linear in ledger length and the ledger is a
58
+ // committed file: every line can spawn a process, and nothing else caps the count.
59
+ const MAX_LEDGER_LINES = 500
60
+ const MAX_RUNS = 100
61
+ const MAX_FILE_BYTES = 4 << 20
62
+ // `.omit` is skipped because the ledger is a claim ABOUT the code, not code:
63
+ // it necessarily contains every symbol it claims is absent, so searching it
64
+ // found the receipt that named the symbol and every `new-dep` receipt refuted
65
+ // itself in any repo that tracks its ledger.
66
+ const SKIP_DIRS = new Set(['.git', 'node_modules', '.omit'])
67
+
68
+ // Re-exported so existing importers keep working; the rule itself lives in one
69
+ // place (lib/exec.mjs), because copies of it drifted once already.
70
+ export { execDisabled }
71
+
72
+ // ---------- ledger ----------
73
+ // A ledger is untrusted input like any other: `lstat` (not `stat`, which follows
74
+ // links), regular files only, a size cap, and the real path contained in the
75
+ // repo. A committed symlink to /dev/zero previously hung every caller — gate,
76
+ // hooks and CI — because `existsSync` follows links and the read never EOFs.
77
+ function safeRead(path, { root = null, cap = MAX_FILE_BYTES } = {}) {
78
+ try {
79
+ const st = lstatSync(path)
80
+ if (!st.isFile() && !st.isSymbolicLink()) return { failed: `not a regular file` }
81
+ const real = realpathSync(path)
82
+ if (root && relative(root, real).startsWith('..')) return { failed: `resolves outside the repo` }
83
+ const target = statSync(real)
84
+ if (!target.isFile()) return { failed: `resolves to a ${target.isFIFO() ? 'pipe' : 'non-file'}` }
85
+ if (target.size > cap) return { failed: `${target.size} bytes exceeds the ${cap}-byte cap` }
86
+ return { text: readFileSync(real, 'utf8') }
87
+ } catch (e) {
88
+ return { failed: e.code ?? e.message ?? 'unreadable' }
89
+ }
90
+ }
91
+
92
+ // Malformed lines are reported, never thrown: one bad line must not hide the
93
+ // receipts around it.
94
+ export function readLedger(cwd) {
95
+ const path = join(cwd, LEDGER)
96
+ const read = safeRead(path)
97
+ if (read.failed) return { entries: [], error: `${LEDGER} ${read.failed}` }
98
+
99
+ const out = []
100
+ const lines = read.text.split('\n').filter((l) => l.trim())
101
+ if (lines.length > MAX_LEDGER_LINES) out.push({ at: 0, receipt: null, error: `ledger has ${lines.length} lines, over the ${MAX_LEDGER_LINES} cap — the rest were not checked` })
102
+ lines.slice(0, MAX_LEDGER_LINES).forEach((line, i) => {
103
+ try {
104
+ out.push({ at: i + 1, receipt: JSON.parse(line) })
105
+ } catch (e) {
106
+ out.push({ at: i + 1, receipt: null, error: e.message })
107
+ }
108
+ })
109
+ return { entries: out }
110
+ }
111
+
112
+ // ---------- checks ----------
113
+ // Each check reports ok:true (holds), ok:false (ran, and refutes the claim), or
114
+ // ok:null (could not be determined — never silently counted as agreement).
115
+ const ok = (kind, detail) => ({ kind, ok: true, detail })
116
+ const no = (kind, detail) => ({ kind, ok: false, detail })
117
+ const unknown = (kind, detail) => ({ kind, ok: null, detail })
118
+
119
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
120
+ const unique = (xs) => [...new Set(xs)]
121
+
122
+ // "This code already exists" — the file, the line, and the symbol must all be
123
+ // there. A cited line that drifted is a stale citation, and it fails rather
124
+ // than quietly passing.
125
+ function checkReuse(r, cwd) {
126
+ const { file, line, symbol } = r
127
+ if (typeof file !== 'string' || !file) return [unknown('reuse', 'no `file`')]
128
+ if (typeof symbol !== 'string' || !symbol) return [unknown('reuse', 'no `symbol`')]
129
+ if (!Number.isInteger(line) || line < 1) return [unknown('reuse', 'no 1-based integer `line`')]
130
+
131
+ const root = repoRoot(cwd) ?? cwd
132
+ const abs = resolve(root, file)
133
+ const read = safeRead(abs, { root })
134
+ if (read.failed) return [no('reuse', `${file}: ${read.failed}`)]
135
+
136
+ const lines = read.text.split('\n')
137
+ if (line > lines.length) return [no('reuse', `${file} has ${lines.length} lines, not ${line}`)]
138
+ const from = Math.max(0, line - 1 - SYMBOL_WINDOW)
139
+ const to = Math.min(lines.length, line + SYMBOL_WINDOW)
140
+ // A symbol is matched as a literal, not as a pattern: `\b` assertions would
141
+ // make a call form like `parseRange()` unfalsifiable, which tells an honest
142
+ // agent its accurate citation is a fabrication.
143
+ const found = lines.slice(from, to).findIndex((l) => l.includes(symbol))
144
+ return found === -1
145
+ ? [no('reuse', `${symbol} is not within ${SYMBOL_WINDOW} lines of ${file}:${line}`)]
146
+ : [ok('reuse', `${file}:${from + found + 1} contains ${symbol}`)]
147
+ }
148
+
149
+ // "Nothing in the codebase does this" — an ABSENCE, reconstructed here by
150
+ // searching rather than accepted from the author. This is what makes a `tried`
151
+ // entry meaningful: citing a path that simply does not exist proves nothing, but
152
+ // a repo-wide search that comes back empty is evidence.
153
+ //
154
+ // Note the polarity: this check reports whether the SYMBOL IS ABSENT, which is
155
+ // what the author wrote. Every other check reports whether the OMISSION HOLDS.
156
+ // `checkNewDep` inverts this one accordingly — a true absence is the omission
157
+ // failing, not holding.
158
+ function checkAbsent(r, cwd) {
159
+ if (typeof r.absent !== 'string' || !r.absent) return [unknown('absent', 'no `absent` symbol to search for')]
160
+ const hits = searchRepo(cwd, r.absent)
161
+ if (hits === null) return [unknown('absent', 'could not search the tree')]
162
+ return hits.length === 0
163
+ ? [ok('absent', `no occurrence of ${r.absent} anywhere in the tree`)]
164
+ : [no('absent', `${r.absent} is present at ${hits.slice(0, 3).join(', ')}`)]
165
+ }
166
+
167
+ // "An already-installed dependency handles it" — declared by one of the repo's
168
+ // own manifests, and the snippet must name it and execute.
169
+ function checkInstalledDep(r, cwd) {
170
+ const checks = []
171
+ if (typeof r.dep !== 'string' || !r.dep) checks.push(unknown('installed-dep', 'no `dep`'))
172
+ else {
173
+ const declared = declaredDeps(cwd)
174
+ checks.push(declared.has(r.dep) ? ok('installed-dep', `${r.dep} is declared`) : no('installed-dep', `${r.dep} is not declared by any manifest`))
175
+ }
176
+ return [...checks, ...checkRun(r, cwd, r.dep)]
177
+ }
178
+
179
+ // A `run` snippet must name the API or dependency it exercises, and its argv text
180
+ // must mention it. Exit status alone is not evidence: `["true"]` proves nothing.
181
+ function checkRun(r, cwd, dep) {
182
+ const subject = typeof r.api === 'string' && r.api ? r.api : dep
183
+ if (!subject) return [unknown('run', 'the receipt names no `api` or `dep`, so nothing binds this snippet to a claim')]
184
+ if (r.run === undefined) return [unknown('run', 'no `run` snippet — a snippet that executes is the evidence here')]
185
+ if (!Array.isArray(r.run) || r.run.length === 0 || r.run.some((a) => typeof a !== 'string' || !a)) {
186
+ return [unknown('run', '`run` must be a non-empty array of strings')]
187
+ }
188
+ if (!r.run.join(' ').includes(subject)) {
189
+ return [unknown('run', `the snippet does not mention ${subject}, so it does not exercise the thing claimed`)]
190
+ }
191
+ if (execDisabled()) return [unknown('run', 'execution disabled by OMIT_NO_EXEC')]
192
+
193
+ const [cmd, ...args] = r.run
194
+ // SIGKILL, because SIGTERM is catchable and a child that ignores it holds the
195
+ // caller — and therefore `git commit` — open forever.
196
+ const res = spawnSync(cmd, args, { cwd, encoding: 'utf8', timeout: RUN_TIMEOUT_MS, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] })
197
+ if (res.error) return [no('run', `${cmd} could not run: ${res.error.code ?? res.error.message}`)]
198
+ if (res.status !== 0) return [no('run', `${r.run.join(' ')} exited ${res.status}`)]
199
+ return [ok('run', `${r.run.join(' ')} runs`)]
200
+ }
201
+
202
+ // "Every earlier omission was tried and did not hold" — so every `tried` entry
203
+ // must itself fail its check. That is the whole claim, and it is checkable.
204
+ function checkNewDep(r, cwd) {
205
+ const checks = []
206
+ if (typeof r.dep !== 'string' || !r.dep) checks.push(unknown('new-dep', 'no `dep`'))
207
+ if (!Array.isArray(r.tried) || r.tried.length === 0) {
208
+ checks.push(unknown('new-dep', 'no `tried` entries — the earlier omissions are the claim, so they are the evidence'))
209
+ return checks
210
+ }
211
+ r.tried.forEach((t, i) => {
212
+ const inferred = inferClaim(t)
213
+ const label = `tried[${i}] (rung ${t?.rung ?? '?'})`
214
+ // A location is not evidence of absence. Citing a file that happens not to
215
+ // exist used to produce ok:false, which read as "the omission failed" —
216
+ // making a bad citation the cheapest possible proof that reuse was tried.
217
+ // Absence has to be searched for, by this module, via `absent`.
218
+ if (inferred === 'reuse') {
219
+ checks.push(unknown('new-dep', `${label} cites a location, which cannot show that an omission was tried — write {"rung":2,"absent":"<symbol searched for>"} instead`))
220
+ return
221
+ }
222
+ const inner = checksFor({ ...t, claim: inferred }, cwd)
223
+ const detail = inner.map((c) => c.detail).join('; ')
224
+ // An entry only counts as refuted when every part of it was actually
225
+ // determined — a half-checked attempt could not be ruled out, and calling
226
+ // that "it holds" would refute the receipt on no evidence.
227
+ if (inner.length === 0 || inner.some((c) => c.ok === null)) {
228
+ checks.push(unknown('new-dep', `${label} could not be checked: ${detail}`))
229
+ return
230
+ }
231
+ // `absent` answers "is it absent?", the others answer "does it work?" — and
232
+ // an absence is the omission failing, so its polarity is inverted here.
233
+ const omissionHolds = inferred === 'absent' ? inner.every((c) => c.ok === false) : inner.every((c) => c.ok === true)
234
+ checks.push(
235
+ omissionHolds
236
+ ? no('new-dep', `${label} actually HOLDS: ${detail} — so the omission applies and this dep is not needed`)
237
+ : ok('new-dep', `${label} genuinely does not hold`)
238
+ )
239
+ })
240
+ return checks
241
+ }
242
+
243
+ // A `tried` entry rarely repeats its own `claim` — the rung is implied by what
244
+ // evidence the entry carries. `dep` is tested before `run` on purpose: an entry
245
+ // carrying both is an installed-dep attempt, and reading it as a stdlib one
246
+ // would skip the manifest check entirely. Anything left is a location citation,
247
+ // which `checkNewDep` refuses as evidence of absence.
248
+ const inferClaim = (t) =>
249
+ typeof t?.claim === 'string' ? t.claim
250
+ : typeof t?.absent === 'string' ? 'absent'
251
+ : typeof t?.dep === 'string' ? 'installed-dep'
252
+ : Array.isArray(t?.run) ? 'stdlib'
253
+ : 'reuse'
254
+
255
+ // Dispatch by Map, not by indexing an object literal: an untrusted `claim` of
256
+ // "constructor" or "__proto__" used to resolve to an Object.prototype member and
257
+ // crash the whole run, hiding every other receipt's verdict.
258
+ const CHECKS = new Map([
259
+ ['reuse', checkReuse],
260
+ ['absent', checkAbsent],
261
+ ['stdlib', (r, cwd) => checkRun(r, cwd, null)],
262
+ ['installed-dep', checkInstalledDep],
263
+ ['new-dep', checkNewDep],
264
+ ])
265
+
266
+ // A check that throws must not take the verdict with it, and must never read as
267
+ // agreement. `tried` entries citing a directory used to throw EISDIR out of the
268
+ // hook, which exits 1 — a non-blocking error, i.e. fail-open.
269
+ function checksFor(r, cwd) {
270
+ const claim = typeof r?.claim === 'string' ? r.claim : null
271
+ const check = claim !== null ? CHECKS.get(claim) : undefined
272
+ if (!check) return [unknown('claim', `unknown claim "${claim}" — expected one of ${[...CHECKS.keys()].join(', ')}`)]
273
+ try {
274
+ return check(r, cwd)
275
+ } catch (e) {
276
+ return [unknown(claim, `the check threw: ${e.message ?? e}`)]
277
+ }
278
+ }
279
+
280
+ // A receipt verifies only when every check it declares holds. A refuted check
281
+ // outranks an undetermined one: "I could not tell" must not mask "this is
282
+ // wrong", or disabling execution quietly downgrades every refutation to a
283
+ // warning — which is exactly what the Action's default would otherwise do.
284
+ export function verifyReceipt(receipt, cwd) {
285
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) {
286
+ return { claim: null, status: 'unverifiable', checks: [unknown('shape', 'not a JSON object')] }
287
+ }
288
+ const checks = checksFor(receipt, cwd)
289
+ const status = checks.some((c) => c.ok === false) ? 'failed' : checks.some((c) => c.ok === null) || checks.length === 0 ? 'unverifiable' : 'verified'
290
+ return { claim: receipt.claim ?? null, status, checks }
291
+ }
292
+
293
+ export function verifyLedger(cwd) {
294
+ let runs = 0
295
+ return readLedger(cwd).entries.map(({ at, receipt, error }) => {
296
+ const base = error
297
+ ? { at, claim: null, status: 'unverifiable', checks: [unknown('json', `line ${at} is not valid JSON: ${error}`)] }
298
+ : { at, ...verifyReceipt(receipt, cwd) }
299
+ // Bound the total work: the per-run timeout caps one snippet, not the ledger.
300
+ if (runs++ >= MAX_RUNS) return { at, claim: base.claim, status: 'unverifiable', checks: [unknown('budget', `over the ${MAX_RUNS}-snippet cap; not checked`)] }
301
+ return base
302
+ })
303
+ }
304
+
305
+ // dep → the new-dep receipt that cites it, for the gate to consult.
306
+ export function newDepCitations(cwd) {
307
+ const out = new Map()
308
+ for (const { receipt, error } of readLedger(cwd).entries) {
309
+ if (error || receipt?.claim !== 'new-dep' || typeof receipt.dep !== 'string') continue
310
+ out.set(receipt.dep, verifyReceipt(receipt, cwd))
311
+ }
312
+ return out
313
+ }
314
+
315
+ // ---------- repo search ----------
316
+ // The absence evidence a `tried` entry is built on. Text search, bounded, and it
317
+ // refuses to answer rather than returning a partial result it cannot vouch for.
318
+ function searchRepo(cwd, symbol, { maxFiles = 2000, maxHits = 20 } = {}) {
319
+ // Untracked files count. A working tree mid-session is mostly untracked, and
320
+ // the file that answers "does this already exist" is usually one nobody has
321
+ // added yet — searching only the index would miss it and report the absence
322
+ // as confirmed. An untracked file can only ever make a receipt FAIL, which is
323
+ // the safe direction.
324
+ const listed = probe(cwd, ['ls-files', '-z'])
325
+ const untracked = probe(cwd, ['ls-files', '-z', '--others', '--exclude-standard'])
326
+ if (!listed.ok) return null
327
+ const hits = []
328
+ let seen = 0
329
+ for (const rel of `${listed.out}\0${untracked.ok ? untracked.out : ''}`.split('\0')) {
330
+ if (!rel || rel.split('/').some((seg) => SKIP_DIRS.has(seg))) continue
331
+ if (++seen > maxFiles) break
332
+ const read = safeRead(join(cwd, rel), { cap: MAX_FILE_BYTES })
333
+ if (read.failed || !read.text.includes(symbol)) continue
334
+ hits.push(rel)
335
+ if (hits.length >= maxHits) break
336
+ }
337
+ return hits
338
+ }
339
+
340
+ // Every dependency declared anywhere in the repo's own manifests. Memoized per
341
+ // process: without it, N installed-dep receipts spawn N × git ls-files.
342
+ const declaredCache = new Map()
343
+ export function declaredDeps(cwd) {
344
+ if (declaredCache.has(cwd)) return declaredCache.get(cwd)
345
+ const names = new Set()
346
+ const files = new Set([...MANIFESTS].filter((n) => safeRead(join(cwd, n)).text !== undefined))
347
+ const listed = probe(cwd, ['ls-files', '-z'])
348
+ if (listed.ok) for (const f of listed.out.split('\0')) if (f && isManifest(f)) files.add(f)
349
+ for (const f of files) {
350
+ const read = safeRead(join(cwd, f))
351
+ if (read.text === undefined) continue
352
+ for (const d of parseDeps(basename(f), read.text)) names.add(d)
353
+ }
354
+ declaredCache.set(cwd, names)
355
+ return names
356
+ }
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@sriinnu/omit",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Omit needless code. Editorial discipline for AI coding agents: draft less, cite everything, cut last: enforced by hooks, a pre-commit gate, and a PR bot, whatever agent writes the code.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "omit": "bin/omit.mjs"
8
8
  },
9
+ "scripts": {
10
+ "test": "node --test",
11
+ "preversion": "npm test",
12
+ "release": "node scripts/release.mjs",
13
+ "token:sync": "node scripts/sync-token.mjs"
14
+ },
9
15
  "files": [
10
16
  "bin",
11
17
  "lib",
@@ -17,6 +23,7 @@
17
23
  ".cursor",
18
24
  ".clinerules",
19
25
  ".windsurf",
26
+ ".github",
20
27
  "README.md",
21
28
  "LICENSE"
22
29
  ],
@@ -34,21 +34,26 @@ Before writing anything, try to omit. In order: stop at the first omission that
34
34
 
35
35
  ## The Fact-Check
36
36
 
37
- An editor prints no uncited claim. Neither do you. Each omission must be verified **in this session**:
37
+ An editor prints no uncited claim. Neither do you. Each omission must be verified **in this session**, and verified means *checkable by something other than you*.
38
38
 
39
- - "The codebase already does this" → open the file; cite `path:line`.
40
- - "Stdlib/platform covers it" → check the real docs or run a snippet proving the API exists and behaves as needed.
41
- - "The installed dep handles it" → confirm it's in the manifest AND the call you're making exists in the installed version.
39
+ A citation only its author can read is a self-report. So every omission goes on the record in `.omit/receipts.jsonl`, one JSON object per line, naming its own evidence in a form a machine re-checks:
42
40
 
43
- No citation, no omission: move to the next question and keep editing. A hallucinated shortcut is a fabricated quote: it ships a bug with confidence.
41
+ | Omission | Receipt | What gets checked |
42
+ |---|---|---|
43
+ | 2 · reuse the codebase | `{"claim":"reuse","rung":2,"file":"src/x.ts","line":42,"symbol":"parseRange"}` | the file, the line, and `symbol` within a few lines of it |
44
+ | 3-4 · stdlib / platform | `{"claim":"stdlib","rung":3,"api":"crypto.randomUUID","run":["node","-e","crypto.randomUUID()"]}` | the snippet runs **and its argv names the API** |
45
+ | 5 · installed dep | `{"claim":"installed-dep","rung":5,"dep":"zod","run":["node","-e","require('zod').object"]}` | a manifest declares the dep, the snippet names it, and it runs |
46
+ | 7 · new dependency | `{"claim":"new-dep","rung":7,"dep":"left-pad","tried":[{"rung":2,"absent":"padTo"}]}` | each `tried` entry is re-checked — and must fail |
44
47
 
45
- **The receipts ledger.** Every citation goes on the record: append one JSON line to `.omit/receipts.jsonl` as you verify:
48
+ Four things make it real:
46
49
 
47
- ```json
48
- {"claim":"stdlib covers uuid","receipt":"node -e crypto.randomUUID() → ok","rung":3,"file":"src/id.ts"}
49
- ```
50
+ - **Evidence is bound to the claim.** A snippet's exit status proves nothing on its own: `["true"]` exits 0 and `["false"]` exits 1 while testing neither the standard library nor any dependency. So `run` must also name what it exercises (`api`, or the `dep`) and the argv must mention it. A snippet that exits on demand is not evidence.
51
+ - **A `tried` entry cites a search, not a location.** Write the symbol you looked for and omit searches the whole tree for it. Citing a file that merely doesn't exist proves nothing — and it used to be the cheapest way to fake "I tried reuse".
52
+ - **`run` is argv, not a shell string** — an array, so nothing expands and nothing is a second command hiding in an argument.
53
+ - **A `new-dep` receipt is the strongest claim here**, because it asserts omissions 2-5 were tried and did not hold. If an entry holds, the receipt is refuted — the omission applies, so you do not add the dependency.
54
+ - **No receipt, no omission.** Move to the next omission and keep editing.
50
55
 
51
- New dependencies REQUIRE a ledger entry before touching the manifest (the dep sentinel blocks otherwise): cite why omissions 2-5 failed.
56
+ `omit verify` re-checks the whole ledger and passes only when every claim survived. `omit gate` refuses a new dependency cited by anything less than a verified receipt. A claim that does not survive re-checking is a fabricated citation: fix the receipt, or fix the code.
52
57
 
53
58
  ## The Final Draft
54
59
 
@@ -56,9 +61,16 @@ Working code is a first draft. After the change is verified (tests green or beha
56
61
 
57
62
  - Cut dead branches, unused params and imports, speculative options, comments that restate the code.
58
63
  - Collapse indirection with one caller and no second use in sight.
59
- - Report the net: files touched, lines added/removed, new dependencies (target: 0).
60
64
 
61
- Write that report to `.omit/final-draft.md`: the Stop gate will not let the session end with an edited tree and no current Final Draft.
65
+ Then write the net report to `.omit/final-draft.md`. The stop gate **reads these three numbers out of it** and checks them against the tree, so state them in this shape:
66
+
67
+ ```
68
+ files touched: <n>
69
+ lines +<added> −<removed>
70
+ new dependencies: <n>
71
+ ```
72
+
73
+ Untracked files count toward all three, which means `git diff --stat` understates them. `omit audit` prints exactly these numbers — paste its verdict and they are right by construction. A report the gate cannot parse, or whose numbers the tree contradicts, ends the session blocked rather than accepted. The gate is on the report, not on the file existing.
62
74
 
63
75
  Done means final draft: not green tests.
64
76
 
@@ -79,7 +91,7 @@ When a load-bearing line adds code, say `load-bearing: <reason>` and write it. N
79
91
 
80
92
  **The repo's linter is load-bearing.** Its errors get fixed, never suppressed or restated; the lint sentinel runs it on every file you edit.
81
93
 
82
- **Hazards never ship.** Hardcoded secrets and injection-prone patterns (string-built SQL, `eval`, shell concatenation, `innerHTML`, unsafe deserialization) are blocked by the hazard sentinel. Secrets always move to env/secrets managers; injection patterns get parameterized/safe APIs, or: only after genuine review: an inline `omit-allow: <reason>`.
94
+ **Hazards never ship.** Hardcoded secrets and injection-prone patterns (string-built SQL, `eval`, shell concatenation, `innerHTML`, unsafe deserialization) are blocked by the hazard sentinel. **Secrets have no override** — a key always moves to an environment variable or a secrets manager, and no marker waives that. Injection patterns get a parameterized query or a safe API; failing that, a trailing comment on that line carrying a real reason: `// omit-allow: <reason>`. That same trailing-comment-with-a-reason form is the only thing that waives the command and leak sentinels, and the reason is required: the bare token, a token inside a string literal, and a reason-less marker are all ignored.
83
95
 
84
96
  ## Footnote the omissions
85
97