@sriinnu/omit 0.3.0 → 0.4.0
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/.clinerules/omit.md +1 -1
- package/.cursor/rules/omit.mdc +1 -1
- package/.github/copilot-instructions.md +15 -0
- package/.github/workflows/publish.yml +69 -0
- package/.github/workflows/test.yml +40 -0
- package/.windsurf/rules/omit.md +1 -1
- package/AGENTS.md +11 -2
- package/README.md +72 -15
- package/action.yml +18 -1
- package/bench/run.mjs +25 -4
- package/bin/omit.mjs +518 -58
- package/hooks/command-sentinel.mjs +39 -11
- package/hooks/dep-sentinel.mjs +184 -30
- package/hooks/final-draft-gate.mjs +231 -37
- package/hooks/hazard-sentinel.mjs +157 -39
- package/hooks/hooks.json +18 -1
- package/hooks/leak-sentinel.mjs +55 -0
- package/hooks/lint-sentinel.mjs +34 -11
- package/lib/danger.mjs +9 -1
- package/lib/deps.mjs +341 -45
- package/lib/exec.mjs +18 -0
- package/lib/git.mjs +107 -0
- package/lib/hazards.mjs +29 -4
- package/lib/leaks.mjs +313 -0
- package/lib/lint.mjs +85 -5
- package/lib/receipts.mjs +356 -0
- package/package.json +5 -1
- package/skills/omit/SKILL.md +25 -13
package/lib/receipts.mjs
ADDED
|
@@ -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,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sriinnu/omit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"bin",
|
|
11
14
|
"lib",
|
|
@@ -17,6 +20,7 @@
|
|
|
17
20
|
".cursor",
|
|
18
21
|
".clinerules",
|
|
19
22
|
".windsurf",
|
|
23
|
+
".github",
|
|
20
24
|
"README.md",
|
|
21
25
|
"LICENSE"
|
|
22
26
|
],
|
package/skills/omit/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
Four things make it real:
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|