cursedops 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/README.md +10 -0
- package/package.json +7 -1
- package/src/boundLists.ts +86 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ bun add cursedops
|
|
|
14
14
|
| `cursedops/serve` | the static tier's four helpers — the path-traversal guard, the MIME table, the hashed-asset test, the crash handlers |
|
|
15
15
|
| `cursedops/api-floor` | the rule that an unmatched `/api/...` is a phrase and never the app shell — the namespace predicates, the trailing-slash normaliser and the default 404 body. No `node:` import, so it mounts inside a Worker |
|
|
16
16
|
| `cursedops/build-info` | which commit a checkout-served process is running and whether its tree was dirty — read once at load, and a `null` retried in the background rather than cached for the life of the process. `node:child_process`, so never in a Worker |
|
|
17
|
+
| `cursedops/bound-lists` | the static check that no SQL builds an `IN (…)` list one `?` per value — the shape D1 500s on past 100 ids. Test-time only; the recorded third exception, see below |
|
|
17
18
|
| `cursedops/public-surface` | the ratchet on a LIBRARY's public surface — a symbol count per export subpath against a committed baseline that may only fall — and its `public-surface` bin. Not an app's: the one entry here admitted for three published libraries, see below |
|
|
18
19
|
|
|
19
20
|
Bun, zero runtime dependencies, ships TypeScript source. Nothing here knows an app's
|
|
@@ -71,6 +72,15 @@ adoption all three measured **identical per-subpath counts** to their own copies
|
|
|
71
72
|
(`cwip` 60 subpaths/2,089 symbols, `cursedbelt` 51/1,837, `cursedbelt-server` 34/992) —
|
|
72
73
|
a consolidation that moves a count has changed the measurement, not the plumbing.
|
|
73
74
|
|
|
75
|
+
🔴 **`bound-lists` is the third recorded exception, admitted by rule 3 at three consumers.**
|
|
76
|
+
It was written once, by `collections` (`src/server/boundLists.test.ts`, 2026-09-22), after
|
|
77
|
+
the shape it refuses drew an empty gallery for every folder over 100 files on D1. The same
|
|
78
|
+
day `music` was found carrying 24 sites and `vault` four — the identical trap in two more D1
|
|
79
|
+
Workers — so the choice was two more copies of the scanner or one here. Copies are what
|
|
80
|
+
`check-copies` exists to refuse, and a forked scanner is a check that quietly stops matching.
|
|
81
|
+
It is mechanism only: each app passes its own root and directories and keeps its own
|
|
82
|
+
"not vacuous" assertion about which files the scan must reach.
|
|
83
|
+
|
|
74
84
|
### What was deliberately left in the apps
|
|
75
85
|
|
|
76
86
|
The comparison that produced this library found three areas that are genuinely
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The build-and-ops answers this generation's apps wrote independently and identically: finding a generation's roots without knowing a path, macOS launchd agent install/replace/remove, the scaffolding of a deployed smoke, and the static-serving helpers eight apps copied — the path-traversal guard among them — and the API floor that keeps an unmatched /api/... from ever being answered with the app shell — the commit and dirty flag a checkout-served process reports, retried rather than cached when git loses a boot race — and the public-surface ratchet three published libraries each carried a forked copy of. Mechanism only — no app knows its name from here. Bun, zero runtime dependencies (typescript is an optional peer, for public-surface only), ships source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
"source": "./src/smoke.ts",
|
|
43
43
|
"import": "./src/smoke.ts"
|
|
44
44
|
},
|
|
45
|
+
"./bound-lists": {
|
|
46
|
+
"types": "./src/boundLists.ts",
|
|
47
|
+
"bun": "./src/boundLists.ts",
|
|
48
|
+
"source": "./src/boundLists.ts",
|
|
49
|
+
"import": "./src/boundLists.ts"
|
|
50
|
+
},
|
|
45
51
|
"./build-info": {
|
|
46
52
|
"types": "./src/buildInfo.ts",
|
|
47
53
|
"bun": "./src/buildInfo.ts",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔴 No SQL builds a parameter list one `?` per value.
|
|
3
|
+
*
|
|
4
|
+
* `IN (${ids.map(() => "?").join(", ")})` is green on every fixture and a 500 on D1 the moment
|
|
5
|
+
* the list passes 100 — the cap is per statement, and every other parameter in the statement
|
|
6
|
+
* shares it. Measured 2026-09-22 in `collections`: about twenty call sites had the shape, and
|
|
7
|
+
* the first one found drew an empty gallery for every folder of more than 100 files. The same
|
|
8
|
+
* day `music` was found carrying 24 of them and `vault` four, which is what put the scanner
|
|
9
|
+
* here instead of in a third copy.
|
|
10
|
+
*
|
|
11
|
+
* The fix everywhere is `inArray` from `cursedbelt-server/d1`: the whole list as ONE JSON
|
|
12
|
+
* parameter through `json_each`, whatever its length. This proves the shape cannot come back
|
|
13
|
+
* in a line no test happens to drive.
|
|
14
|
+
*
|
|
15
|
+
* A line that genuinely needs one `?` per value — a fixed, known-small arity, like a multi-row
|
|
16
|
+
* VALUES list already sized by `chunkForBind` — carries `bound-list:ok <why>` on the same line
|
|
17
|
+
* or the one above. The reason is the point: an excuse that does not say why the list is
|
|
18
|
+
* bounded is the bug, filed under a comment.
|
|
19
|
+
*/
|
|
20
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { join, relative } from "node:path";
|
|
22
|
+
|
|
23
|
+
export interface BoundListFinding {
|
|
24
|
+
file: string;
|
|
25
|
+
line: number;
|
|
26
|
+
shape: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SHAPES: { name: string; pattern: RegExp }[] = [
|
|
31
|
+
{ name: "an IN list interpolated into SQL", pattern: /\bIN\s*\(\s*\$\{/i },
|
|
32
|
+
{ name: "one placeholder generated per value", pattern: /=>\s*(["'`])\?\1/ },
|
|
33
|
+
];
|
|
34
|
+
const EXCUSE = /bound-list:ok\s+\S/;
|
|
35
|
+
|
|
36
|
+
/** The scan, over one file's text. */
|
|
37
|
+
export function scanBoundLists(file: string, source: string): BoundListFinding[] {
|
|
38
|
+
const lines = source.split("\n");
|
|
39
|
+
const out: BoundListFinding[] = [];
|
|
40
|
+
lines.forEach((text, index) => {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
// A comment that QUOTES the shape is documentation of the trap, not the trap.
|
|
43
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("//") || trimmed.startsWith("/*")) return;
|
|
44
|
+
for (const { name, pattern } of SHAPES) {
|
|
45
|
+
if (!pattern.test(text)) continue;
|
|
46
|
+
if (EXCUSE.test(text) || EXCUSE.test(lines[index - 1] ?? "")) continue;
|
|
47
|
+
out.push({ file, line: index + 1, shape: name, text: trimmed });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sources(dir: string): string[] {
|
|
54
|
+
const out: string[] = [];
|
|
55
|
+
for (const name of readdirSync(dir)) {
|
|
56
|
+
const path = join(dir, name);
|
|
57
|
+
if (statSync(path).isDirectory()) {
|
|
58
|
+
if (name !== "node_modules") out.push(...sources(path));
|
|
59
|
+
} else if (/\.tsx?$/.test(name) && !/\.(test|spec)\.tsx?$/.test(name)) out.push(path);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Scan every non-test `.ts`/`.tsx` file under `dirs` (relative to `root`). `scanned` is
|
|
66
|
+
* returned so the caller can prove the scan is not vacuous — assert it contains the files
|
|
67
|
+
* that held the shape when the check was adopted.
|
|
68
|
+
*/
|
|
69
|
+
export function boundListFindings(
|
|
70
|
+
root: string,
|
|
71
|
+
dirs: readonly string[],
|
|
72
|
+
): { scanned: string[]; findings: BoundListFinding[] } {
|
|
73
|
+
const files = dirs.flatMap((dir) => sources(join(root, dir)));
|
|
74
|
+
return {
|
|
75
|
+
scanned: files.map((file) => relative(root, file)),
|
|
76
|
+
findings: files.flatMap((file) => scanBoundLists(relative(root, file), readFileSync(file, "utf8"))),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One line per finding, the form an `expect(...).toEqual([])` prints usefully. */
|
|
81
|
+
export const describeFindings = (findings: readonly BoundListFinding[]): string[] =>
|
|
82
|
+
findings.map((f) => `${f.file}:${f.line} — ${f.shape}: ${f.text}`);
|
|
83
|
+
|
|
84
|
+
/** The message to hang on that `expect`. */
|
|
85
|
+
export const BOUND_LIST_ADVICE =
|
|
86
|
+
"use `inArray` from cursedbelt-server/d1, or excuse the line with `bound-list:ok <why it is bounded>`";
|