Skip to content

Commit 0bd8eb6

Browse files
authored
Chore/audit fixes 20260601 2103 (#84)
* fix(docs): strip all backticks when parsing script catalog cells stripBackticks only removed leading/trailing backticks, so a cell like `path` (note) left a trailing backtick on the split token — scripts-catalog.json shipped 'dev/preflight-host-dev.sh`'. Strip every backtick and regenerate. Audit: F001 * ci(infra): unify docker action versions across workflows validate-compose pinned docker/build-push-action@v6.18.0 and setup-buildx@v3.10.0 while the release workflow used v7.2.0 / v4.1.0. Align on the newer pins so each action resolves to one SHA repo-wide. Audit: F003 (parity lint-meta rule tracked as follow-up) * chore(ui): align eslint-plugin-env-access to 0.1.3 (matches api) Shared env-access rule was enforced at 0.1.1 in ui vs 0.1.3 in api; unify so the same env-access checks run in both apps. Audit: F004 * chore(api,docs): declare engines.node to match ui ui pins engines.node >=24.0.0; api had only bun and docs none, despite all three using Node tooling (tsx/knip/node *.mjs). Declare consistently so older-Node environments get a warning. Audit: F006 * chore(docs): pin dependencies to exact versions (match api/ui) apps/docs used caret ranges while api/ui pin exact. Pinned each dep to its currently-resolved version (astro 6.3.6, sharp 0.34.5, etc. — not the caret floor) so installs are deterministic and --frozen-lockfile stays satisfiable. Audit: F005 * test(api): cover web-push channel + extend test-sibling rule to .channel.ts Guardrail-first: web-push.channel.ts had no test while its email/in-app/sse peers did. Extend logic-files-require-test-sibling to include .channel.ts so the gap surfaces as a lint:meta violation, then add the missing test (suppressed-on-no-subscriptions + missing-delivery-row no-op). Full api suite green (1052 pass). Audit: F008 * docs: list the audit-monorepo and execute-audit skills on /skills/ Adds an 'Audit skills' section documenting the repo-root /audit-monorepo (read-only JSON audit) and /execute-audit (autonomous guardrail-first remediation) skills, and notes that root .claude/skills/ holds cross-app/audit skills. Audit: docs follow-up (user request)
1 parent 8688015 commit 0bd8eb6

11 files changed

Lines changed: 215 additions & 70 deletions

File tree

.github/workflows/infra-compose-validate-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,11 @@ jobs:
196196
197197
- name: Set up Docker Buildx
198198
if: steps.filter.outputs.code == 'true'
199-
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
199+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
200200

201201
- name: Build apps/api prod image
202202
if: steps.filter.outputs.code == 'true'
203-
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
203+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
204204
with:
205205
context: apps/api
206206
file: apps/api/Dockerfile.prod
@@ -212,7 +212,7 @@ jobs:
212212

213213
- name: Build apps/ui prod image
214214
if: steps.filter.outputs.code == 'true'
215-
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
215+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
216216
with:
217217
context: apps/ui
218218
file: apps/ui/Dockerfile.prod

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"private": true,
77
"module": "src/index.ts",
88
"engines": {
9+
"node": ">=24.0.0",
910
"bun": "1.3.14"
1011
},
1112
"packageManager": "bun@1.3.14",

apps/api/scripts/lint-meta/rules/testing/logic-files-require-test-sibling.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { join } from "node:path";
44
import { collectSourceFiles } from "../../context";
55
import type { IMetaRule, IViolation } from "../../types";
66

7-
const SUFFIX_REQUIRES_TEST = /\.(service|utils|jobs|check)\.ts$/u;
7+
const SUFFIX_REQUIRES_TEST = /\.(service|utils|jobs|check|channel)\.ts$/u;
88

99
export function checkLogicFilesHaveTests(root: string): IViolation[] {
1010
const violations: IViolation[] = [];
@@ -31,7 +31,7 @@ export function checkLogicFilesHaveTests(root: string): IViolation[] {
3131
rule: "logic-files-require-test-sibling",
3232
message: `Missing unit-test sibling. Expected \`${expectedTest.slice(
3333
root.length + 1
34-
)}\` to exist alongside this \`*.{service,utils,jobs,check}.ts\` module — every piece of logic ships with a test.`,
34+
)}\` to exist alongside this \`*.{service,utils,jobs,check,channel}.ts\` module — every piece of logic ships with a test.`,
3535
});
3636
}
3737

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2+
import {
3+
cleanDatabase,
4+
db,
5+
eq,
6+
notification,
7+
notificationDelivery,
8+
requireDb,
9+
users,
10+
} from "../../../helpers/db";
11+
import {
12+
defineNotificationEvent,
13+
webPushChannel,
14+
} from "../../../../src/lib/notifications";
15+
import { t } from "elysia";
16+
17+
const insertTestUser = async (): Promise<string> => {
18+
const [created] = await db
19+
.insert(users)
20+
.values({
21+
email: "web-push-channel-test@example.com",
22+
firstName: "W",
23+
lastName: "P",
24+
})
25+
.returning();
26+
27+
if (!created) {
28+
throw new Error("Failed to insert test user");
29+
}
30+
31+
return created.id;
32+
};
33+
34+
const testEvent = defineNotificationEvent({
35+
type: "test.web_push.channel",
36+
schema: t.Object({ message: t.String() }),
37+
defaultChannels: ["in-app"],
38+
render: {
39+
inApp: ({ payload }) => ({
40+
title: "web-push test",
41+
body: payload.message,
42+
}),
43+
},
44+
});
45+
46+
describe("webPushChannel.dispatch", () => {
47+
beforeEach(async () => {
48+
if (!(await requireDb())) {
49+
return;
50+
}
51+
52+
await cleanDatabase();
53+
});
54+
55+
afterEach(async () => {
56+
if (!(await requireDb())) {
57+
return;
58+
}
59+
60+
await cleanDatabase();
61+
});
62+
63+
test("suppresses the delivery when the recipient has no push subscriptions", async () => {
64+
if (!(await requireDb())) {
65+
return;
66+
}
67+
68+
const userId = await insertTestUser();
69+
const [noti] = await db
70+
.insert(notification)
71+
.values({
72+
recipientUserId: userId,
73+
eventType: testEvent.type,
74+
payload: { message: "hi" },
75+
rendered: { title: "t", body: "b" },
76+
})
77+
.returning();
78+
79+
if (!noti) {
80+
throw new Error("Failed to insert notification");
81+
}
82+
83+
await db.insert(notificationDelivery).values({
84+
notificationId: noti.id,
85+
channel: "web-push",
86+
status: "pending",
87+
});
88+
89+
await webPushChannel.dispatch({
90+
notificationId: noti.id,
91+
recipientUserId: userId,
92+
event: testEvent,
93+
payload: { message: "hi" },
94+
rendered: { title: "t", body: "b" },
95+
});
96+
97+
const [row] = await db
98+
.select()
99+
.from(notificationDelivery)
100+
.where(eq(notificationDelivery.notificationId, noti.id));
101+
102+
expect(row?.status).toBe("suppressed");
103+
expect(row?.error).toBe("no_subscriptions");
104+
});
105+
106+
test("is a no-op when the web-push delivery row is missing", async () => {
107+
if (!(await requireDb())) {
108+
return;
109+
}
110+
111+
const userId = await insertTestUser();
112+
const missingNotificationId = "00000000-0000-0000-0000-000000000000";
113+
114+
await webPushChannel.dispatch({
115+
notificationId: missingNotificationId,
116+
recipientUserId: userId,
117+
event: testEvent,
118+
payload: { message: "hi" },
119+
rendered: { title: "t", body: "b" },
120+
});
121+
122+
const rows = await db
123+
.select()
124+
.from(notificationDelivery)
125+
.where(eq(notificationDelivery.notificationId, missingNotificationId));
126+
127+
expect(rows).toHaveLength(0);
128+
});
129+
});

apps/docs/bun.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/docs/package.json

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"version": "0.1.0",
55
"private": true,
66
"description": "boringstack.xyz docs site for the BoringStack project. Astro Starlight, deployed via Cloudflare Pages.",
7+
"engines": {
8+
"node": ">=24.0.0",
9+
"bun": "1.3.14"
10+
},
711
"packageManager": "bun@1.3.14",
812
"scripts": {
913
"dev": "astro dev",
@@ -25,24 +29,24 @@
2529
"a11y": "bash scripts/a11y.sh"
2630
},
2731
"dependencies": {
28-
"@astrojs/react": "^5.0.5",
29-
"@astrojs/sitemap": "^3.7.2",
30-
"@astrojs/starlight": "^0.39.2",
31-
"@tailwindcss/vite": "^4.3.0",
32-
"astro": "^6.3.5",
33-
"astro-mermaid": "^2.0.1",
34-
"clsx": "^2.1.1",
32+
"@astrojs/react": "5.0.5",
33+
"@astrojs/sitemap": "3.7.2",
34+
"@astrojs/starlight": "0.39.2",
35+
"@tailwindcss/vite": "4.3.0",
36+
"astro": "6.3.6",
37+
"astro-mermaid": "2.0.1",
38+
"clsx": "2.1.1",
3539
"mermaid": "11.15.0",
36-
"react": "^19.2.6",
37-
"react-dom": "^19.2.6",
38-
"reading-time": "^1.5.0",
39-
"sharp": "^0.34.0",
40-
"tailwindcss": "^4.3.0",
40+
"react": "19.2.6",
41+
"react-dom": "19.2.6",
42+
"reading-time": "1.5.0",
43+
"sharp": "0.34.5",
44+
"tailwindcss": "4.3.0",
4145
"ws": "8.20.1"
4246
},
4347
"devDependencies": {
44-
"@axe-core/cli": "^4.11.3",
45-
"http-server": "^14.1.1",
48+
"@axe-core/cli": "4.11.3",
49+
"http-server": "14.1.1",
4650
"starlight-llms-txt": "0.6.1",
4751
"wrangler": "4.93.0"
4852
},

apps/docs/scripts/docs-catalog-lib.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ function parseMarkdownTableSection(text, heading) {
9494
}
9595

9696
function stripBackticks(value) {
97-
return value.replace(/^`+|`+$/gu, "").trim();
97+
// Remove every backtick, not just leading/trailing. Markdown cells like
98+
// `` `path/to/script.sh` (then X) `` carry an inline closing backtick that a
99+
// start/end-only strip would leave on the token after a whitespace split.
100+
return value.replace(/`/gu, "").trim();
98101
}
99102

100103
function parseCommandName(cell, runner) {

apps/docs/src/content/docs/skills/index.mdx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Agent skills
33
description: Slash-command skills shipped with BoringStack. Each one encodes a task, its conventions, and a ready-to-paste prompt.
44
---
55

6-
BoringStack ships agent skills in `apps/api/.claude/skills/` and `apps/ui/.claude/skills/`. Each skill is a markdown file with a prompt that an agent follows. Type the skill name (e.g., `/add-full-feature`) in Claude Code or Cursor and the agent runs the workflow.
6+
BoringStack ships agent skills in the repo root `.claude/skills/` (cross-app and audit skills) and per-app in `apps/api/.claude/skills/` and `apps/ui/.claude/skills/`. Each skill is a markdown file with a prompt that an agent follows. Type the skill name (e.g., `/add-full-feature`) in Claude Code or Cursor and the agent runs the workflow.
77

88
## Full-stack skills
99

@@ -41,6 +41,28 @@ Checks BoringStack-specific invariants: ACL ability checks on every route, multi
4141

4242
On the UI: no raw `fetch` outside `openapi.ts`, no `dangerouslySetInnerHTML`, no `import.meta.env` outside `src/lib/env/`, no token storage in `localStorage`, no JSX strings without i18n, CSRF + content-type validation.
4343

44+
## Audit skills
45+
46+
Two repo-root skills that pair up for autonomous, agent-to-agent maintenance. The
47+
audit is machine-readable on purpose: one agent finds the work, another executes it.
48+
49+
### /audit-monorepo
50+
51+
Read-only, full-monorepo audit (apps/api, apps/ui, apps/docs, infra, scripts, CI). Fans
52+
out parallel read-only subagents per app and concern, then writes a single
53+
evidence-backed JSON report to `.audit/audit-report.json` — every finding cites exact
54+
files, ranked for an autonomous agent to execute. Each finding is also classified by
55+
whether a linter/parser **guardrail** could enforce it, so fixes extend the tooling
56+
rather than patch one instance ("lint as a contract").
57+
58+
### /execute-audit
59+
60+
Reads `.audit/audit-report.json` and works every finding to completion on a single
61+
branch. Guardrail-first: when a finding is a class a rule can catch, it extends the
62+
relevant `lint:meta` rule so the bug surfaces as a violation, then fixes the code —
63+
validating each fix with the repo's own `bun run check`, committing per finding, and
64+
reverting-and-continuing on failure.
65+
4466
## Related
4567

4668
- [First feature in 10 minutes](/skills/first-feature-tutorial/) - end-to-end walkthrough.

0 commit comments

Comments
 (0)