@bompus/house-rules 0.11.0 → 0.13.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.
Files changed (111) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/README.md +91 -60
  3. package/compose.mjs +8 -2
  4. package/composition.mjs +11 -3
  5. package/config-view.mjs +18 -3
  6. package/config.mjs +74 -13
  7. package/docs/assets/overview.png +0 -0
  8. package/docs/configuration.md +85 -9
  9. package/package.json +4 -2
  10. package/rules/core.md +118 -209
  11. package/rules/modifiers/backlog-outline.md +25 -0
  12. package/rules/modifiers/backlog-visual.md +16 -0
  13. package/rules/modifiers/coded-offers.md +47 -17
  14. package/rules/modifiers/cross-session-relay.md +23 -0
  15. package/rules/modifiers/low-quota-handoff.md +4 -3
  16. package/rules/modifiers/model-default-questions.md +33 -0
  17. package/rules/modifiers/multi-agent.md +6 -2
  18. package/rules/modifiers/no-attribution.md +7 -1
  19. package/rules/modifiers/periodic-updates.md +25 -0
  20. package/rules/modifiers/plan-files.md +35 -3
  21. package/rules/modifiers/release-batching.md +4 -2
  22. package/rules/modifiers/shared-host-load.md +13 -42
  23. package/rules/modifiers/squash-landing.md +1 -1
  24. package/rules/modifiers/upstream-contributions.md +45 -0
  25. package/rules/modifiers/web-image-formats.md +35 -0
  26. package/rules/references/backlog-view.md +154 -0
  27. package/rules/references/landing.md +14 -14
  28. package/rules/references/multi-agent.md +6 -2
  29. package/rules/references/release-batching.md +27 -4
  30. package/rules/references/squash-landing.md +10 -4
  31. package/rules/references/thread-visuals.md +45 -0
  32. package/setup.mjs +87 -17
  33. package/skills/hr-agent-guidance-audit/SKILL.md +10 -5
  34. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  35. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +1 -1
  36. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  37. package/skills/hr-agent-guidance-refresh/SKILL.md +25 -57
  38. package/skills/hr-agent-guidance-refresh/references/verification.md +1 -1
  39. package/skills/hr-audit-choices/SKILL.md +1 -1
  40. package/skills/hr-benchmarking/SKILL.md +122 -159
  41. package/skills/hr-benchmarking/references/javascript.md +113 -0
  42. package/skills/hr-benchmarking/references/tools.md +62 -57
  43. package/skills/hr-better-accessibility/SKILL.md +1 -1
  44. package/skills/hr-better-accessibility/screen-readers.md +1 -0
  45. package/skills/hr-code-review/SKILL.md +19 -3
  46. package/skills/hr-code-review/agents/openai.yaml +1 -1
  47. package/skills/hr-code-review/references/acp-client.md +43 -0
  48. package/skills/{hr-change-impact/SKILL.md → hr-code-review/references/impact.md} +2 -12
  49. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  50. package/skills/hr-code-review/references/panel-plan.md +61 -0
  51. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  52. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  53. package/skills/hr-code-review/references/panel-setup.md +54 -0
  54. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  55. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  56. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  57. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  58. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  59. package/skills/hr-design-exploration/SKILL.md +24 -1
  60. package/skills/hr-design-exploration/agents/openai.yaml +2 -2
  61. package/skills/hr-diagnosing-bugs/SKILL.md +2 -2
  62. package/skills/hr-diagnosing-bugs/references/reporting.md +29 -4
  63. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  64. package/skills/hr-explain-code/SKILL.md +1 -1
  65. package/skills/hr-extract-shared-steps/SKILL.md +1 -1
  66. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  67. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  68. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  69. package/skills/hr-handoff/LICENSE +21 -0
  70. package/skills/hr-handoff/NOTICE.md +29 -0
  71. package/skills/hr-handoff/SKILL.md +24 -1
  72. package/skills/hr-handoff/references/reconciliation.md +25 -6
  73. package/skills/hr-handoff/references/successor-thread.md +76 -0
  74. package/skills/hr-handoff/scripts/check-reconciliation.mjs +48 -6
  75. package/skills/hr-house-rules-setup/SKILL.md +22 -8
  76. package/skills/hr-lean-plan/SKILL.md +4 -0
  77. package/skills/hr-maintainability-review/SKILL.md +14 -4
  78. package/skills/hr-maintainability-review/agents/openai.yaml +1 -1
  79. package/skills/hr-manual-qa/SKILL.md +1 -1
  80. package/skills/hr-ordering-tests/SKILL.md +1 -1
  81. package/skills/hr-plain-prose/SKILL.md +7 -4
  82. package/skills/hr-pr-followup/SKILL.md +6 -3
  83. package/skills/hr-pr-followup/references/review-helpers.md +21 -0
  84. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  85. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  86. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  87. package/skills/hr-read-reddit/SKILL.md +1 -1
  88. package/skills/hr-research-shared-links/SKILL.md +103 -0
  89. package/skills/hr-sqlite/SKILL.md +174 -0
  90. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  91. package/skills/hr-sqlite/references/evidence.md +94 -0
  92. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  93. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  94. package/skills/hr-stock-ui-audit/SKILL.md +1 -1
  95. package/skills/hr-test-audit/LICENSE +21 -0
  96. package/skills/hr-test-audit/NOTICE.md +67 -0
  97. package/skills/hr-usage-report/SKILL.md +90 -0
  98. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  99. package/skills/hr-usage-report/assets/example-report.json +125 -0
  100. package/skills/hr-usage-report/assets/report-template.html +377 -0
  101. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  102. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  103. package/skills/hr-what-next/SKILL.md +100 -10
  104. package/skills/hr-what-next/references/reporting.md +13 -22
  105. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  106. package/skills/hr-writing-pr/SKILL.md +29 -14
  107. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
  108. package/docs/npm-release.md +0 -45
  109. package/rules/modifiers/swarmail.md +0 -12
  110. package/skills/hr-benchmarking/references/priority-findings-20261006.md +0 -115
  111. package/skills/hr-change-impact/agents/openai.yaml +0 -5
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: hr-sqlite
3
+ description: Checklist of settings, failure patterns and measured effects for SQLite code. Use when adding or changing code that opens, queries or copies a SQLite database, or when diagnosing "database is locked", SQLITE_BUSY, slow queries or WAL growth.
4
+ ---
5
+
6
+ # SQLite
7
+
8
+ Run `python3 <this skill's directory>/scripts/inspect-sqlite.py <database>` first
9
+ when you have a database file. It opens the file read-only and reports what the
10
+ file records, the WAL size and the mount options. It cannot show connection
11
+ settings, because SQLite does not store them in the file; read the code that
12
+ opens the connection for those. With `--source <directory>` it also scans source
13
+ text for each `journal_mode` mention that has no earlier `busy_timeout` in the
14
+ same file. That scan does not establish run order or which connection a mention
15
+ belongs to.
16
+
17
+ Each item carries an evidence tier:
18
+
19
+ - **M**: measured on a real workload. [The evidence](references/evidence.md)
20
+ gives the counts, the workload and its limits.
21
+ - **D**: stated by the SQLite documentation, not measured here.
22
+ - **U**: suspected or reported by one source. Do not rely on it.
23
+
24
+ The numbers come from specific workloads. Before changing a setting for speed,
25
+ measure your own with `hr-benchmarking`. Before adopting or rejecting a
26
+ different engine, driver or pool, read
27
+ [the tool decisions](references/tool-decisions.md).
28
+
29
+ ## Open a connection
30
+
31
+ 1. **Set `PRAGMA busy_timeout` first**, before `journal_mode` and every other
32
+ statement, on every connection, including short-lived probe, lock and status
33
+ databases. The default is 0, so any competing lock fails at once. Set it
34
+ long enough to cover the longest holder you expect. A deliberate fail-fast
35
+ probe sets 0 and says why in a comment. A connection has one busy handler,
36
+ and `PRAGMA busy_timeout` replaces any handler the driver or your code
37
+ installed (D), so where you register a custom handler, configure that and do
38
+ not also set the pragma. (M: a mail server's first start and a wake-lock
39
+ probe failed this way. A test with a holder of 300 ms failed the old order in
40
+ 5 ms and passed once the timeout came first.)
41
+ 2. **Convert to WAL once.** Switching a brand-new file to `journal_mode=WAL`
42
+ takes an exclusive lock, and several processes doing it together failed.
43
+ Create and convert the file in one place, or retry the conversion on
44
+ `database is locked` and `disk I/O error`. Do not ignore a failed
45
+ conversion: that connection failed later statements. A busy timeout set
46
+ first does cover `journal_mode`. (M: 22 of 300 failures without the retry,
47
+ 0 of 480 with it.)
48
+ 3. **Keep a WAL database on a local filesystem.** WAL needs shared memory, so
49
+ network filesystems do not work (D). A database on a WSL `/mnt/<drive>` mount
50
+ failed with `disk I/O error`; the cause was not isolated (one host).
51
+ 4. **Choose `synchronous` per store.** In WAL mode NORMAL skips the sync at
52
+ commit. An application crash loses nothing, but a power loss or OS crash can
53
+ lose the most recent commits (D). Keep FULL where lost commits cannot be
54
+ replayed or their side effects are not idempotent. Never choose it from a
55
+ microbenchmark. (M: 3.09 to 0.52 ms per message send; 4.18 to 2.93 ms
56
+ end-to-end median.)
57
+ 5. **Do not expect `temp_store=MEMORY` or `mmap_size` to help a small
58
+ database.** No difference beyond noise on a 560 KB database (M). Both are
59
+ untested on large databases here; measure before adopting.
60
+ 6. **Use one SQLite build per process for a WAL file.** A runtime's built-in
61
+ SQLite and a native module's bundled SQLite shared one process, and the WAL
62
+ state was corrupted in about a third of runs. The lock-registry mechanism is
63
+ asserted (M). Open the file with one library. Give the other a checkpointed
64
+ copy, and close its connections deterministically: one run in five still
65
+ hit SIGBUS from a connection closed only at garbage collection (M).
66
+ 7. **Set per-connection pragmas on every connection.** `busy_timeout`,
67
+ `synchronous`, `foreign_keys` and `cache_size` do not persist in the file;
68
+ `journal_mode=WAL` does (D).
69
+
70
+ ## Write
71
+
72
+ 8. **Start write transactions with `BEGIN IMMEDIATE`** and retry the whole
73
+ transaction on `SQLITE_BUSY`. A deferred transaction that reads and then
74
+ writes cannot wait: SQLite returns busy without calling the handler when
75
+ waiting could deadlock (D). `BEGIN IMMEDIATE` takes the write lock at once:
76
+ it waits behind an existing writer and blocks every other writer until it
77
+ ends, even if its body only reads. Using it for reads therefore serializes
78
+ them with writers (read from one server's source, not measured). Keep write
79
+ transactions short and free of I/O.
80
+ 9. **Expect a checkpoint to wait.** A long-running reader stops a checkpoint
81
+ from finishing, so the WAL grows (D). A `TRUNCATE` checkpoint also waits for
82
+ a backfill already running and for an idle reader's WAL mark. With a 2 s
83
+ timeout it lost to a backfill of a 420 MB WAL in about one run in three.
84
+ Draining the backfill first and accepting a complete `PASSIVE` backfill fixed
85
+ it, because a file copy only needs every frame in the database file (M).
86
+
87
+ ## Read
88
+
89
+ 10. **Keep planner statistics.** Run `PRAGMA optimize` just before closing a
90
+ short-lived connection. For a long-lived one, run `PRAGMA optimize=0x10002`
91
+ when it opens and `PRAGMA optimize` periodically. Run it after
92
+ `CREATE INDEX` (D). The `0x10000` bit and the automatic analysis limit
93
+ arrived in SQLite 3.46.0. On older releases set `PRAGMA analysis_limit`
94
+ yourself and run `ANALYZE` (D). Missing statistics made one query scan: 10.3
95
+ to 12.5 ms became 0.0075 ms. The first run on a 15 GB file took 72 to 80 s
96
+ and blocked writers; repeats took 0.5 to 1.2 s. Run it first at a time you
97
+ choose (M).
98
+ 11. **Check the plan before rewriting a query.** `EXPLAIN QUERY PLAN`, then
99
+ statistics. A slow query that looked like bad SQL was missing statistics
100
+ (M).
101
+ 12. **Index what an FTS5 index is deleted and read by.** Deleting by an
102
+ `UNINDEXED` column scans the whole index per row. Keep an ordinary indexed
103
+ table from source to rowid, delete through it, and look up hits and their
104
+ context by indexed rowid. Read rank first: select `rowid` ordered by `rank`
105
+ with a limit, then join content. (M: refresh reads 79.9 to 18.2 MiB; hit and
106
+ context lookups 5,742 to 134 ms after the later read-side change; rank-first
107
+ 84 to 37 ms.)
108
+ 13. **Cover `LIKE` lookups with an index** on the columns the query reads. A
109
+ covering index took 7 to 10 ms to 1 to 2.4 ms (M). The built-in `lower()`
110
+ handles ASCII only (D).
111
+ 14. **Poll for change with `PRAGMA data_version`.** It differs between two reads
112
+ on one connection when another connection committed (D). Rescan only then.
113
+ 15. **Look inside SQLite before swapping a driver.** 80 to 92% of an explore
114
+ call was inside SQLite. Three drivers took 1,330 to 1,810 ms on an
115
+ indexing workload, with no clear difference (M). A driver can still differ in one call; see
116
+ [the tool decisions](references/tool-decisions.md).
117
+
118
+ ## Copy, back up and host settings
119
+
120
+ 16. **Copy a live database with `VACUUM INTO` or the driver's backup API**,
121
+ then run `PRAGMA integrity_check` on the copy. Copying the files while a
122
+ transaction is active can capture old and new content together (D).
123
+ `sqlite3 .backup` restarts whenever another connection writes, so against a
124
+ busy writer it can run without end (D); one run read 2.8 TB. (M: 54 s for 14
125
+ GB with `VACUUM INTO`; 36 s for 15.2 GB with the driver's `backup()`.)
126
+ 17. **Treat a file-level snapshot of the database and its `-wal` as a drill, not
127
+ a guarantee.** One restore of three live databases with their WAL files
128
+ passed `integrity_check` (M, one drill). Restore and check before relying on
129
+ the method.
130
+ 18. **Check mount options.** Online `discard` on ext4 stalled `fsync`: the main
131
+ thread's disk wait fell from 26.8% and 16.8% to under 1% after remounting
132
+ with `nodiscard`, and a weekly `fstrim` replaced it (M). The inspection
133
+ script prints them.
134
+
135
+ ## Measure
136
+
137
+ 19. **Report cold and warm separately.** The same `PRAGMA optimize` took 72 s
138
+ cold and 0.5 s warm (M).
139
+ 20. **Time the change in the real loop.** Turning foreign keys off ran 7.6
140
+ times faster alone and made no difference in the loop (32.9 to 32.8 s)
141
+ (M).
142
+ 21. **Match durability settings before comparing stores.** A ratio between two
143
+ stores whose `synchronous` settings differ, or are unrecorded, compares the
144
+ settings. One comparison reported 84 to 563 times against stores whose
145
+ setting was not recorded (M).
146
+ 22. **Record the SQLite version and compile options**
147
+ (`PRAGMA compile_options`). Two builds of the same version can differ: the
148
+ documentation recommends `SQLITE_DEFAULT_MEMSTATUS=0`, and one runtime's
149
+ build lacks it (D; see [the tool decisions](references/tool-decisions.md)).
150
+
151
+ ## Keep reads off the event loop
152
+
153
+ 23. **With a synchronous driver, run reads on a worker connection.** Node's
154
+ built-in `node:sqlite` runs each statement on the calling thread, so a slow
155
+ read holds the event loop for its whole duration. Where the application
156
+ shares one queued connection for reads and writes, a read also waits behind
157
+ any open write transaction. Route read-only queries to a separate read-only
158
+ connection in a worker thread. That connection sees only committed data, as a
159
+ snapshot, so move only reads that tolerate that; a read that must see the
160
+ caller's uncommitted changes stays on the writer. A text search over 86,163 messages held the
161
+ loop for a median 165 ms (range 97 to 308) on the writer connection and 2.5 ms
162
+ (2 to 4) on the worker, with no clear change in wall time (165 and 157 ms; 12
163
+ runs per arm, 2 ms probe) (M). Prove the routing with a test that keeps the
164
+ main thread busy on a latch and checks the worker read still completes; an
165
+ open writer transaction alone proves nothing, because in WAL mode a second
166
+ main-thread connection can read past it. Record
167
+ the longest event-loop stall, not only the query time; `hr-benchmarking`'s
168
+ JavaScript reference covers the probe.
169
+
170
+ ## What to leave out of a record
171
+
172
+ Do not repeat a claim from [the withdrawn list](references/evidence.md#claims-that-did-not-hold)
173
+ without re-measuring it. Mark anything unmeasured as such, and give the count
174
+ and the workload with any figure.
@@ -0,0 +1,2 @@
1
+ interface:
2
+ display_name: "HR: SQLite"
@@ -0,0 +1,94 @@
1
+ # Evidence behind the checklist
2
+
3
+ Measured by one owner on Linux, WSL and Windows hosts between 2026-09-01 and 2026-10-14.
4
+ Every figure names its workload and its count where the source recorded them.
5
+ [Tested versions](#tested-versions) lists the SQLite, driver, runtime and operating
6
+ system versions recovered from the records of each measurement. Where a version
7
+ could not be recovered, the table says "Not recorded".
8
+ **M** means measured here, **D** means read from the SQLite documentation,
9
+ **U** means unconfirmed. Treat a figure as one workload's result, not a
10
+ benchmark.
11
+
12
+ ## Measured findings
13
+
14
+ | Setting or pattern | Result | Workload and limits |
15
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
16
+ | Missing planner statistics | One query 10.3 to 12.5 ms became 0.0075 ms after `PRAGMA optimize` (about 1,400 to 1,700 times faster, from these rounded times) | Two 15 GB copies of an application database, one cold run each. First copy: median of 5 processes by 20 passes, 15 of 183 query plans changed, first optimize 72.3 s, repeats 0.5 to 1.2 s. Second copy: 80.5 s with the cache dropped. During a cold run 1,598 of 1,602 writer probes with `busy_timeout` 0 failed and all reads succeeded |
17
+ | `synchronous` FULL to NORMAL (microbenchmark) | 1,170 to 13.4 µs per commit | 1,000 commits per run of one 3,000-character row plus one index, 5 runs per arm alternating, each in a fresh process and database on the live database's disk. Taken while online `discard` was mounted, so the FULL figure includes those stalls and was not re-measured after remounting |
18
+ | `synchronous` FULL to NORMAL (application write) | 1,809 to 433 µs per event write, p99 29% worse; 318 to 18 added `fsync` calls per 300 writes; end-to-end median 4.18 to 2.93 ms (30% lower), p95 unchanged | 4 to 8 runs, synthetic load; the `fsync` counts are one `strace` count each. A review panel found that effects outside the file do not roll back, so after an operating-system crash a retry could run an effect again, and nothing shown made them idempotent. The author had not checked |
19
+ | `synchronous` NORMAL on a small local mail store | Message send 3.09 to 0.52 ms; p99 about 5 ms unchanged | 3 interleaved rounds. The owner chose NORMAL over a keep-FULL recommendation |
20
+ | `temp_store=MEMORY`, `mmap_size=256MB` | No difference beyond noise | 3 rounds by 4 arms on a 560 KB database. Not adopted. Untested on large databases |
21
+ | Online `discard` on ext4 | Main-thread disk wait 26.8% and 16.8% became 0.6% and 0.8%; waits of 1 s or more went from 17 and 4 to 0 and 0 | Two 240 s samples per arm and a 158 minute follow-up with 0 stalls. 2.4 TiB was discarded in about 2,290 s. Lowering the kernel's dirty-page limits did not help and was reverted. The remedy chosen was `nodiscard` plus a weekly `fstrim` |
22
+ | Two SQLite builds in one process | Committed rows reappeared and the WAL index's `mxFrame` regressed mid-run, then SIGBUS | A runtime's built-in SQLite and a native module's bundled SQLite (a separate compilation) on one live WAL, in one process. About a third of runs on one corpus failed; sequential runs were clean 3 of 3 and pool-only runs were clean. Failures reproduced with checkpoints suppressed and with worker recycling off. Giving workers a checkpointed copy still produced one SIGBUS in five runs until connections were closed deterministically. The lock-registry mechanism is asserted from this evidence |
23
+ | `TRUNCATE` checkpoint racing a WAL backfill | About one run in three was much slower than the rest; after the fix 8 of 8 runs were normal. Timings omitted: tested versions not recorded | A `TRUNCATE` checkpoint with a 2 s busy timeout lost to the WAL valve's backfill of a 420 MB WAL and the resolver pool was dropped. The fix drains the valve first and accepts a complete `PASSIVE` backfill when `TRUNCATE` is still busy (an idle reader's WAL mark also blocks it), since a copy only needs every frame in the database file |
24
+ | Zero or late `busy_timeout` | Two documented failures: a mail server's first start (`journal_mode` ran with no wait) and a wake-lock probe (`busy_timeout` 0) | Mail server start: a test whose child held an exclusive lock for 300 ms failed in 5 ms with `journal_mode` first and passed with `busy_timeout` first. Wake-lock probe: both starters failed with `SQLITE_BUSY` at 0; changing it to 250 ms passed five consecutive two-order runs. Other incidents were reported in transcripts and are not in the records. Two call sites in that server still use 0 with no comment; one fails with an ownership message |
25
+ | Fresh-file WAL conversion | 105 of 120 concurrent runs failed before a pull request's concurrency fixes and 11 of 120 at its head; every remaining failure was `PRAGMA journal_mode=WAL`. Retrying on `database is locked` and `disk I/O error`: 0 of 480 against 22 of 300 for the head | Windows host, several processes opening a new file together; three arms round-robined over 300 runs, then two samples. Ignoring the failure did not work: the failed connection broke later statements |
26
+ | FTS5 delete by an unindexed column | Refresh logical reads 79.9 to 18.2 MiB | Session-search index; the change deleted through an indexed source-to-rowid map instead of an unindexed column. The wall-clock gap on refresh was inconclusive. A service reached its 1,500 MiB cgroup cap at 1,492 MiB of file cache and 15 MiB resident; the cause of the kill is unresolved |
27
+ | FTS5 hit and context lookups | 18 native hit and context calls 5,741.6 to 133.6 ms (43 times); logical reads 21,902.88 to 118.52 MiB | The same index after a later change: indexed source rowids and bounded rowid lookups for reads, with a context join order that no longer scans FTS metadata. Confirmation wall-time variation 0.77% and 1.92%; 144 outputs hash-matched |
28
+ | Covering index and rank-first FTS | `LIKE` 7 to 10 ms became 1 to 2.4 ms; rank-first FTS 84 to 37 ms | Code-search explore calls, output byte-identical. 80 to 92% of call time was inside SQLite. A separate change that passed an edge kind to the query and cached a name list cut rows read in 12 explores from 1.38 million to 313 thousand |
29
+ | Driver swap | No clear difference between three drivers: 1,330 to 1,810 ms across them | A 500,000-edge indexing workload: better-sqlite3 about 1,400 ms, bun:sqlite about 1,500 ms, node:sqlite 1,330 to 1,810 ms. An isolated in-memory insert arm: node:sqlite and bun:sqlite about 0.5 µs per row, better-sqlite3 about 1.8 times slower. On the explore read path better-sqlite3 was 0.8% faster and both Bun drivers 8% to 11% slower than Node |
30
+ | Micro-win that did not transfer | Foreign keys off ran 7.6 times faster alone and 32.9 to 32.8 s in the real loop | One indexing workload |
31
+ | Live-database copy | `sqlite3 .backup` restarted until stopped after reading 2.8 TB; `VACUUM INTO` took 54 s for 14 GB; driver `backup()` took 36.4 s for 15.2 GB | One sample each. Copying the files while a transaction is active can capture old and new content together (SQLite documentation). A restic restore of three live databases (14.6 GiB) with their WAL files passed `quick_check` and `integrity_check` in one drill |
32
+ | Polling a live database | Per-query page reads fell when the filter was made once per row. Figures omitted: tested versions not recorded | The cut in rescans by checking `PRAGMA data_version` first is modeled, not measured on the deployed service |
33
+ | Big database boot | Booting with the database took several times longer than with an empty one. Figures omitted: tested versions not recorded | 14 GB database, 6 runs per arm. The default `cache_size`, `mmap_size` 0 and no `synchronous` setting were read from the code; the boot cost's cause is unproven |
34
+ | Reads on a worker connection | Longest event-loop stall median 165 ms (97 to 308) became 2.5 ms (2 to 4); wall time 165 and 157 ms, no clear change | Text search over 86,163 messages in a WAL file, 12 runs per arm at `nice -n 10`, 2 ms timer probe. One workload; no test in this repository covers it; item 23 of `SKILL.md` gives the proof to run |
35
+ | WAL on `/mnt/<drive>` under WSL | A database on `/mnt/<drive>` failed with `disk I/O error` | WSL. The cause was not isolated; a `-shm` locking cause was suspected and not confirmed. A separate failure reading Windows-side databases from WSL Python was not diagnosed, while a native read-only open succeeded |
36
+
37
+ ## Tested versions
38
+
39
+ Recovered from the owner's records of each measurement session. A version counts
40
+ only when the record of that session states it. "Not recorded" means the records
41
+ do not state it, so treat that row as a historical result whose versions are
42
+ unknown.
43
+
44
+ | Setting or pattern | Versions and environment | Notes |
45
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | Missing planner statistics | Node 26.11.1 with its bundled SQLite 3.53.4 (`node:sqlite`); Linux x86-64 under WSL2; 2026-10-11 | Filesystem of the copies not recorded |
47
+ | `synchronous` FULL to NORMAL (microbenchmark) | Node's bundled SQLite 3.53.4, WAL mode; local ext4 disk under WSL2; 2026-10-10 | Node patch version not recorded |
48
+ | `synchronous` FULL to NORMAL (application write) | ext4 disk under WSL2; about 2026-10-10 | SQLite and runtime versions not recorded |
49
+ | `synchronous` NORMAL on a small local mail store | Bun 1.4.2 with `bun:sqlite`; 2026-09-28 | Bun version is from the plan that records the run, not printed with the measurement. SQLite version not recorded |
50
+ | `temp_store=MEMORY`, `mmap_size=256MB` | Same as the row above | Same limits |
51
+ | Online `discard` on ext4 | ext4 root filesystem under WSL2; 2026-10-11 and 2026-10-12 | The measured wait is disk time, not SQLite time, so no SQLite version applies |
52
+ | Two SQLite builds in one process | `node:sqlite` with SQLite 3.53.4; a Rust binding (rusqlite 0.40.2) bundling SQLite 3.53.x; Bun 1.4.2 and Node 26.8.2 were both used in the investigation; 2026-09-15 to 2026-09-16 | Which runtime ran the failing corpus runs, and the bundled SQLite patch digits, are not recorded |
53
+ | `TRUNCATE` checkpoint racing a WAL backfill | Not recorded | |
54
+ | Zero or late `busy_timeout` | Not recorded | |
55
+ | Fresh-file WAL conversion | Windows 11; Bun 1.4.2 running a `node:sqlite` shim; 2026-09-06 | SQLite version not recorded |
56
+ | FTS5 delete by an unindexed column | Not recorded | A later assessment of the same work reports SQLite 3.46.1; the measurement receipt does not state it |
57
+ | FTS5 hit and context lookups | Not recorded | The measurement receipt does not state the SQLite version; the index is the one in the row above |
58
+ | Covering index and rank-first FTS | Node 24.21.0 with SQLite 3.53.4 and Bun 1.4.2 with SQLite 3.53.2; 2026-09-27 to 2026-10-03 | Versions are for the benchmark campaign; the figures were not matched to one run |
59
+ | Driver swap | Indexing arms: Bun 1.4.2, Node 26.8.2, SQLite 3.53.4 for `node:sqlite`; 2026-09-16. Explore arms: Node 24.21.0, SQLite 3.53.2 for `bun:sqlite` and 3.53.4 for the others | `better-sqlite3` package version not recorded |
60
+ | Micro-win that did not transfer | Same session as the indexing arms of the row above; 2026-09-16 | Runtime of the loop run not printed separately |
61
+ | Live-database copy | `backup()`: Node 26.11.1, SQLite 3.53.4; 2026-10-11 | Versions of `VACUUM INTO` and the `sqlite3` shell not recorded |
62
+ | Polling a live database | Not recorded | |
63
+ | Big database boot | Not recorded | |
64
+ | Reads on a worker connection | Not recorded | Other runs of the same campaign used Node 26.11.1 on Linux under WSL2; the record does not tie that to this run. SQLite version not recorded |
65
+ | WAL on `/mnt/<drive>` under WSL | Not recorded | |
66
+
67
+ ## Claims that did not hold
68
+
69
+ These appeared in transcripts or records and were withdrawn, disproved or never
70
+ confirmed. Do not repeat them without re-measuring.
71
+
72
+ - **"`busy_timeout` does not cover `journal_mode`."** A 400 ms hold was waited
73
+ out in 429 ms. The failures came from a zero timeout, or from several
74
+ processes converting a new file at once.
75
+ - **"A database-is-locked failure was fixed with a busy timeout and
76
+ `BEGIN IMMEDIATE`."** One indexer's case was a checkpoint racing a
77
+ background backfill.
78
+ - **"The server sets `wal_autocheckpoint=0` and checkpoints by hand."** No
79
+ server code did; the claim was withdrawn.
80
+ - **"A runtime's missing `SQLITE_DEFAULT_MEMSTATUS=0` explains its slower
81
+ concurrent reads."** The compile option is absent from that build (checked
82
+ with `PRAGMA compile_options` against another runtime's build of the same
83
+ SQLite version). A plain C program shows the flag matters for threaded reads.
84
+ No build of that runtime with the flag was run. Suspected only (U).
85
+ - **"The slow query needs rewriting."** The cause was missing statistics.
86
+ - **Ratios between stores of 84 to 563 times.** The comparison set one store at
87
+ `synchronous=NORMAL` against stores whose setting was not recorded.
88
+
89
+ ## Never measured here
90
+
91
+ - `mmap_size`, `cache_size` or `temp_store` on a database of several gigabytes.
92
+ - How long the WAL window of lost commits is under `synchronous=NORMAL`.
93
+ - How long `ANALYZE` holds locks inside a live server.
94
+ - A benchmarked connection pool, Turso or libSQL, or `sqlite-multiwriter`.
@@ -0,0 +1,39 @@
1
+ # Tool decisions
2
+
3
+ What was evaluated, what was decided and what would reopen it. Most of these are
4
+ document assessments, not benchmarks; the basis is stated on each row. Re-check
5
+ a decision when its trigger is met or when the project's own documentation has
6
+ changed since the date.
7
+
8
+ | Candidate | Decision (date) | Basis | Reopen when |
9
+ | ----------------------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | Turso or libSQL engine for local stores | Keep SQLite (2026-09-30) | Documentation and source read; no benchmark run. The engine's FTS was incompatible with the stores' FTS5 use, some pragmas were partial, and one database file could not be shared between the two engines across processes | Shared state must live across devices or machines |
11
+ | [sqlite-multiwriter](https://github.com/sqliteai/sqlite-multiwriter) (per-writer WAL VFS) | Watch (2026-10-09) | README read at release 0.5.2, one day after its creation; its figures (49,277 against 8,630 transactions per second for 16 threads) came from one Apple machine with `synchronous=FULL` and were not reproduced. WAL only; extra `-mw*` files are part of the database; callers must retry on `SQLITE_BUSY_SNAPSHOT`; local filesystems only | A measured workload shows writer throughput, `SQLITE_BUSY` retries or long commit tails on one database that `busy_timeout` and shorter transactions do not fix |
12
+ | A Node connection-pool package | No adoption (2026-10-05) | Code read: the driver in use is synchronous and there are no worker threads; no missing capability was shown. No benchmark | A new Node service needs leased connections, or a measured problem a pool would address |
13
+ | Swapping the SQLite driver for speed | Not worth it (2026-09-16) | No clear difference between three drivers on an indexing workload, 1,330 to 1,810 ms (see the evidence); most call time was inside SQLite itself | No trigger was recorded. Suggested: a profile that shows time in driver binding or row materialization rather than in SQLite |
14
+ | Tantivy or another search engine in place of FTS5 | Keep FTS5 (2026-09-28 and 2026-10-04) | Source and documentation read; no engine performance measured. FTS5 runs in the same process and transaction as the writes, with no second searchable copy to keep in sync | Profiling shows an FTS5 bottleneck on your corpus. Tantivy is then the first embedded alternative worth a controlled comparison |
15
+
16
+ ## Runtime notes
17
+
18
+ - **Bun's `node:sqlite` `get()`** was about 1.5 times slower than Node's when
19
+ returning table columns, because it builds a new row structure per call. A fix
20
+ pull request was open when this was written
21
+ ([oven-sh/bun#44084](https://github.com/oven-sh/bun/issues/44084)).
22
+ - **`SQLITE_DEFAULT_MEMSTATUS`.** The SQLite documentation recommends
23
+ `SQLITE_DEFAULT_MEMSTATUS=0`. `PRAGMA compile_options` shows it on Node's
24
+ SQLite but not on Bun's (checked 2026-10-12, both SQLite 3.53.4). A plain C
25
+ program with 8 threads, each on its own connection, ran each thread about 25%
26
+ slower with memory statistics on. The 8-thread to 1-thread time ratio was 0.89
27
+ to 1.03 with them on and 0.71 to 0.82 with them off, over 3 alternating
28
+ rounds. One thread showed no difference. No Bun build with the flag was run,
29
+ so its share of Bun's slower multi-worker reads is suspected, not confirmed
30
+ ([oven-sh/bun#44187](https://github.com/oven-sh/bun/issues/44187)).
31
+ - **Different runtimes in one process.** Two SQLite builds in one process on one
32
+ WAL file corrupted state; give the second library a checkpointed copy.
33
+
34
+ ## Held, unselected
35
+
36
+ - A pre-backup `sqlite3 .backup` step ahead of file-level snapshots. One restore
37
+ of live database and WAL files opened cleanly, so it stayed a possible
38
+ hardening item. Against an active writer `.backup` restarts (see the
39
+ evidence), so it needs a quiet writer or a bounded retry.
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only inspection of a SQLite database file and, optionally, the source
3
+ that opens it.
4
+
5
+ inspect-sqlite.py DATABASE [--source PATH ...] [--quick-check] [--json]
6
+
7
+ Reports what the file itself records (journal mode, page and freelist counts,
8
+ planner statistics, FTS tables, WAL size) plus the filesystem it sits on. It
9
+ never writes to the database. SQLite may create an empty `-shm` file beside a
10
+ WAL database that no process has open.
11
+
12
+ Connection settings such as busy_timeout, synchronous, foreign_keys and
13
+ cache_size are not stored in the file, so no database inspection can show what
14
+ an application sets. `--source` is a text scan: for each mention of
15
+ journal_mode it reports whether a busy_timeout mention comes earlier in the
16
+ same file. It does not establish run order or which connection each belongs
17
+ to. Treat those lines as places to read, not verdicts. A source path that cannot
18
+ be read is listed as not scanned and the exit status is 2.
19
+ """
20
+ import argparse
21
+ import json
22
+ import os
23
+ import pathlib
24
+ import re
25
+ import sqlite3
26
+ import sys
27
+
28
+ NETWORK_FILESYSTEMS = {"9p", "drvfs", "cifs", "smb3", "smbfs", "nfs", "nfs4", "fuse.sshfs", "virtiofs"}
29
+ SHADOW_SUFFIXES = ("_data", "_idx", "_content", "_docsize", "_config", "_segments", "_segdir", "_stat", "_node", "_parent", "_rowid")
30
+ SOURCE_SUFFIXES = {".py", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rs", ".go", ".rb", ".java", ".kt", ".cs", ".c", ".cc", ".cpp", ".swift", ".php", ".sql"}
31
+
32
+
33
+ def mount_for(path):
34
+ """Return (mount point, filesystem type, options) for the longest mount prefix."""
35
+ try:
36
+ with open("/proc/self/mountinfo", encoding="utf-8", errors="replace") as handle:
37
+ lines = handle.read().splitlines()
38
+ except OSError:
39
+ return None
40
+ real = os.path.realpath(path)
41
+ best = None
42
+ for line in lines:
43
+ left, _, right = line.partition(" - ")
44
+ fields = left.split()
45
+ if len(fields) < 6 or len(right.split()) < 3:
46
+ continue
47
+ point = fields[4].replace("\\040", " ")
48
+ if real == point or real.startswith(point.rstrip("/") + "/") or point == "/":
49
+ if best is None or len(point) > len(best[0]):
50
+ kind, _, super_options = right.split()[:3]
51
+ best = (point, kind, ",".join([fields[5], super_options]))
52
+ return best
53
+
54
+
55
+ def scan_source(paths):
56
+ """For each file that mentions journal_mode, note where busy_timeout appears.
57
+
58
+ Returns (findings, errors). A path that cannot be read is an error, so a
59
+ failed scan never looks like a scan that found nothing.
60
+ """
61
+ findings = []
62
+ errors = []
63
+ files = []
64
+
65
+ def walk_error(error):
66
+ errors.append({"file": str(error.filename), "error": error.strerror or str(error)})
67
+
68
+ for entry in paths:
69
+ if os.path.isdir(entry):
70
+ for root, dirs, names in os.walk(entry, onerror=walk_error):
71
+ dirs[:] = [d for d in dirs if d not in {".git", "node_modules", ".venv", "target", "dist"}]
72
+ files.extend(
73
+ os.path.join(root, n) for n in names if os.path.splitext(n)[1] in SOURCE_SUFFIXES
74
+ )
75
+ else:
76
+ files.append(entry)
77
+ for file in sorted(files):
78
+ try:
79
+ with open(file, encoding="utf-8", errors="replace") as handle:
80
+ lines = handle.read().splitlines()
81
+ except OSError as error:
82
+ errors.append({"file": file, "error": error.strerror or str(error)})
83
+ continue
84
+ journals = [i + 1 for i, l in enumerate(lines) if re.search(r"journal_mode", l, re.I)]
85
+ timeouts = [i + 1 for i, l in enumerate(lines) if re.search(r"busy_timeout|busyTimeout|set_busy_timeout", l, re.I)]
86
+ for journal in journals:
87
+ earlier = [t for t in timeouts if t < journal]
88
+ if earlier:
89
+ status = "busy_timeout at line %d comes before journal_mode at line %d" % (earlier[-1], journal)
90
+ elif timeouts:
91
+ status = "journal_mode at line %d comes before busy_timeout at line %d" % (journal, timeouts[0])
92
+ else:
93
+ status = "journal_mode at line %d; no busy_timeout in this file" % journal
94
+ findings.append({"file": file, "line": journal, "status": status, "ok": bool(earlier)})
95
+ return findings, errors
96
+
97
+
98
+ def inspect(path, quick_check=False):
99
+ result = {"path": os.path.abspath(path)}
100
+ size = os.path.getsize(path)
101
+ result["fileBytes"] = size
102
+ uri = pathlib.Path(result["path"]).as_uri() + "?mode=ro"
103
+ connection = sqlite3.connect(uri, uri=True, timeout=2)
104
+ try:
105
+ connection.execute("PRAGMA busy_timeout = 2000")
106
+
107
+ def pragma(name):
108
+ row = connection.execute("PRAGMA " + name).fetchone()
109
+ return row[0] if row else None
110
+
111
+ result["library"] = {"version": sqlite3.sqlite_version}
112
+ options = [r[0] for r in connection.execute("PRAGMA compile_options")]
113
+ result["library"]["memstatusDisabled"] = "DEFAULT_MEMSTATUS=0" in options
114
+ result["library"]["threadsafe"] = next((o for o in options if o.startswith("THREADSAFE")), None)
115
+ result["journalMode"] = pragma("journal_mode")
116
+ result["pageSize"] = pragma("page_size")
117
+ result["pageCount"] = pragma("page_count")
118
+ result["freelistCount"] = pragma("freelist_count")
119
+ result["autoVacuum"] = pragma("auto_vacuum")
120
+ result["userVersion"] = pragma("user_version")
121
+ result["encoding"] = pragma("encoding")
122
+ names = connection.execute(
123
+ "SELECT name, sql FROM sqlite_master WHERE type = 'table' ORDER BY name"
124
+ ).fetchall()
125
+ tables = [n for n, _ in names if not n.startswith("sqlite_")]
126
+ virtual = {n: s for n, s in names if s and s.upper().startswith("CREATE VIRTUAL TABLE")}
127
+ shadow = {n for table in virtual for n in tables if n in {table + s for s in SHADOW_SUFFIXES}}
128
+ plain = [n for n in tables if n not in virtual and n not in shadow]
129
+ result["tables"] = len(plain)
130
+ result["indexes"] = connection.execute(
131
+ "SELECT count(*) FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_%'"
132
+ ).fetchone()[0]
133
+ result["virtualTables"] = {
134
+ n: (re.search(r"USING\s+(\w+)", s, re.I) or [None, "unknown"])[1].lower()
135
+ for n, s in virtual.items()
136
+ }
137
+ has_stat = any(n == "sqlite_stat1" for n, _ in names)
138
+ analyzed = set()
139
+ if has_stat:
140
+ analyzed = {r[0] for r in connection.execute("SELECT DISTINCT tbl FROM sqlite_stat1")}
141
+ result["statistics"] = {
142
+ "sqliteStat1": has_stat,
143
+ "rows": connection.execute("SELECT count(*) FROM sqlite_stat1").fetchone()[0] if has_stat else 0,
144
+ "tablesWithoutStatistics": [n for n in plain if n not in analyzed],
145
+ }
146
+ if quick_check:
147
+ result["quickCheck"] = [r[0] for r in connection.execute("PRAGMA quick_check(10)")]
148
+ finally:
149
+ connection.close()
150
+ for suffix, key in (("-wal", "walBytes"), ("-shm", "shmBytes")):
151
+ side = result["path"] + suffix
152
+ result[key] = os.path.getsize(side) if os.path.exists(side) else None
153
+ mount = mount_for(result["path"])
154
+ if mount:
155
+ point, kind, options = mount
156
+ result["filesystem"] = {"mount": point, "type": kind, "options": options}
157
+ else:
158
+ result["filesystem"] = None
159
+ return result
160
+
161
+
162
+ def notes(result):
163
+ out = []
164
+ if result["journalMode"] != "wal":
165
+ out.append("journal_mode is %s, not wal; in a rollback journal a writer's commit waits for readers and blocks new ones while it commits" % result["journalMode"])
166
+ stats = result["statistics"]
167
+ if not stats["sqliteStat1"]:
168
+ out.append("no sqlite_stat1: the planner has no statistics; run PRAGMA optimize at a time you choose (the first run on a large file can take over a minute and hold the write lock)")
169
+ elif stats["tablesWithoutStatistics"]:
170
+ out.append("tables with no statistics rows: " + ", ".join(stats["tablesWithoutStatistics"][:8]))
171
+ pages = result["pageCount"] or 0
172
+ if pages and result["freelistCount"] and result["freelistCount"] / pages > 0.2:
173
+ out.append("more than 20%% of pages are free (%d of %d); the file is larger than its content" % (result["freelistCount"], pages))
174
+ if result["walBytes"] and result["walBytes"] > result["fileBytes"]:
175
+ out.append("the WAL file is larger than the database; look for a long-running reader or a blocked checkpoint")
176
+ fs = result["filesystem"]
177
+ if fs:
178
+ if fs["type"] in NETWORK_FILESYSTEMS:
179
+ out.append("filesystem %s: WAL needs shared memory on one host and does not work reliably here" % fs["type"])
180
+ if re.search(r"(^|,)discard(,|$)", fs["options"]):
181
+ out.append("mounted with online discard; this stalled fsync on one host (see the evidence), so check fsync latency")
182
+ if "quickCheck" in result and result["quickCheck"] != ["ok"]:
183
+ out.append("quick_check reported: " + "; ".join(result["quickCheck"][:3]))
184
+ return out
185
+
186
+
187
+ def render(result, source, source_errors=()):
188
+ lines = ["database: %s" % result["path"]]
189
+ lines.append("size: %d bytes; %s pages of %s bytes; %s free" % (
190
+ result["fileBytes"], result["pageCount"], result["pageSize"], result["freelistCount"]))
191
+ lines.append("journal_mode: %s; auto_vacuum: %s; user_version: %s; encoding: %s" % (
192
+ result["journalMode"], result["autoVacuum"], result["userVersion"], result["encoding"]))
193
+ lines.append("tables: %d; indexes: %d; virtual tables: %s" % (
194
+ result["tables"], result["indexes"],
195
+ ", ".join("%s (%s)" % kv for kv in result["virtualTables"].items()) or "none"))
196
+ stats = result["statistics"]
197
+ lines.append("statistics: sqlite_stat1 %s, %d rows" % ("present" if stats["sqliteStat1"] else "absent", stats["rows"]))
198
+ lines.append("wal file: %s; shm file: %s" % (result["walBytes"], result["shmBytes"]))
199
+ fs = result["filesystem"]
200
+ lines.append("filesystem: %s" % ("%s at %s (%s)" % (fs["type"], fs["mount"], fs["options"]) if fs else "not available on this platform"))
201
+ lines.append("inspector library: SQLite %s, %s" % (result["library"]["version"], result["library"]["threadsafe"]))
202
+ lines.append("not in the file: busy_timeout, synchronous, foreign_keys, cache_size, mmap_size, temp_store; read the code that opens the connection")
203
+ for note in notes(result):
204
+ lines.append("note: " + note)
205
+ if source is not None:
206
+ lines.append("source scan (journal_mode mentions): %d" % len(source))
207
+ for item in source:
208
+ lines.append(" %s: %s%s" % (item["file"], item["status"], "" if item["ok"] else " <- read this"))
209
+ for item in source_errors:
210
+ lines.append(" not scanned: %s (%s)" % (item["file"], item["error"]))
211
+ return "\n".join(lines)
212
+
213
+
214
+ def main(argv=None):
215
+ parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
216
+ parser.add_argument("database")
217
+ parser.add_argument("--source", nargs="+", metavar="PATH", help="files or directories to scan for busy_timeout order")
218
+ parser.add_argument("--quick-check", action="store_true", help="also run PRAGMA quick_check (reads the whole file)")
219
+ parser.add_argument("--json", action="store_true")
220
+ args = parser.parse_args(argv)
221
+ if not os.path.isfile(args.database):
222
+ print("error: not a file: %s" % args.database, file=sys.stderr)
223
+ return 2
224
+ try:
225
+ result = inspect(args.database, args.quick_check)
226
+ except (sqlite3.Error, OSError) as error:
227
+ print("error: %s" % error, file=sys.stderr)
228
+ return 2
229
+ source, source_errors = scan_source(args.source) if args.source else (None, [])
230
+ if args.json:
231
+ payload = dict(result)
232
+ payload["notes"] = notes(result)
233
+ if source is not None:
234
+ payload["source"] = source
235
+ payload["sourceErrors"] = source_errors
236
+ print(json.dumps(payload, indent=2, sort_keys=True))
237
+ else:
238
+ print(render(result, source, source_errors))
239
+ for item in source_errors:
240
+ print("error: could not scan %s: %s" % (item["file"], item["error"]), file=sys.stderr)
241
+ return 2 if source_errors else 0
242
+
243
+
244
+ if __name__ == "__main__":
245
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hr-stock-ui-audit
3
- description: Find template-default styling in frontend code that nobody chose for this product (stock violet accents, purple-to-blue gradients, one stock font, emoji icons, glow and blur effects), triage each hit in context, and report or fix the real ones as the request asks. Use when asked to audit or remove generic or AI-looking styling in existing UI. It does not set a visual direction.
3
+ description: Find template-default styling nobody chose for this product (stock violet accents, purple-to-blue gradients, one stock font, emoji icons, glow and blur effects), triage each hit in context, and report or fix the real ones. Use when asked to audit or remove generic or AI-looking styling in existing UI. It does not set a visual direction.
4
4
  ---
5
5
 
6
6
  # Stock UI audit
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaron Queen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.