- Update apps/api/test/setup.ts header: clarify tests run against familysync_test
(provisioned by global-setup.ts), document users-cleanup decision (intact across
tests), and note CI-vs-local env difference
- Add apps/api/README.md "Running API tests locally" section: documents the test
DB isolation, run command, DB_ROOT_PASSWORD requirement, and CI no-op behaviour
- Fix apps/api/test/global-setup.ts: switch from drizzle({ client, mode }) to
drizzle(pool, { mode }) — drizzle-orm@0.45.2 isConfig() has a tautological OR
in the `mode` branch that always returns false, causing the combined-config form
to pass the config object as the client (client.query is not a function); two-arg
form routes correctly; 244/244 tests pass against familysync_test
Long-standing PWA phone-layout defect (BottomTabBar from Phase 04, not a
Phase 10 regression): the fixed bottom bar overlaps the New Event FAB and
the calendar colour legend at <=767px. Added Phase 17 to the v1.1 roadmap
with the defect as the seed item + fix sketch, and a pending todo
(resolves_phase: 17) with full repro/file detail.
dorny/paths-filter combines patterns with Array.some, and picomatch compiles
'!.gitea/**' as 'any path not under .gitea'. The '- !.gitea/**' line (from
quick task 260613-dmw) therefore matched every non-.gitea file — including
.planning/** and *.md — so code=true for doc-only PRs and the heavy api/harness
jobs ran anyway. It also never excluded .gitea (the **/*.yml glob already
matched workflow files), regressing the Phase 15 doc-only skip.
Switch to predicate-quantifier 'every' with negation-only globs: a file counts
as code only if it is outside .gitea/, outside .planning/, and not Markdown.
Verified with picomatch against representative file sets.
Add paths-ignore ['.gitea/**', '.planning/**'] to publish.yml's push
trigger. Doc/CI-only pushes to main (planning docs push straight to main;
workflow edits merge via PR) produce a byte-identical image (.dockerignore
already excludes .planning), so the build + :latest re-push was wasted.
paths-ignore skips only when every changed file matches; mixed code+docs
pushes still publish.
Workflow-only edits shouldn't pay for the multi-minute MariaDB/integration/harness
suite. A negation '- !.gitea/**' (ordered after the yml/yaml globs) makes CI-config-
only PRs resolve code=false so api/harness skip like docs. fast-checks (format:check
still validates the workflow YAML) + gate continue to gate them. PRs that also touch
app code or lockfile still resolve code=true and run the full suite.
The AUDIT-ADVISORY tier cross-checks pnpm outdated (direct deps) against
advisory module_names (mostly transitive), so it almost never fires.
Relabel to OUTDATED-WITH-ADVISORY and document the limitation in the
header, header text, and inline comment; point to check-audit.mjs as the
authoritative advisory gate. No risky full-tree rewrite.
WR-02: capture docker run exit directly (not the piped head exit) so a
chatty booting image can't SIGPIPE to 141 and false-PASS; require the
FATAL guard marker in output as a positive assertion.
WR-05: strip comment lines and use anchored fixed-string (grep -qF)
matching so a commented-out rule can't satisfy the hygiene check and
patterns aren't treated as regexes.
Bind github.base_ref/base.sha/head.sha through env: and reference quoted
shell vars (no inline ${{ }} in run:) to close the script-injection vector.
Add a HEAD_SHA rev-parse fallback mirroring BASE_SHA and echo the final
${BASE_SHA}..${HEAD_SHA} range before gitleaks.
Add isWaived() predicate: a waiver with a past 'expires' date is treated
as absent so the High/Critical advisory re-blocks. Applied in both
selectBlocking and partitionAdvisories. Add expired-waiver unit tests.
isMain now compares fully-resolved real paths (mirrors index.ts).
- Static assertions step: verifies .dockerignore exists, covers all forbidden
patterns (.env, node_modules, apps/api/scripts, .git, .planning, apps/api/tests,
apps/pwa/e2e), and --target production is still pinned in publish.yml (T-16-19)
- Boot-smoke step: runs freshly-built sha_tag image with NODE_ENV=production +
DEV_AUTH_BYPASS=true; fails if image starts (EXIT==0) or times out (EXIT==124),
passes on any other non-zero exit — proves D-08 guard fires in shipped image (T-16-18)
- Both steps ordered after build and before push; a hygiene failure blocks publish (T-16-20)
- Separate 'Build and push' into 'Build production image' (build only) and 'Push image' steps
- Creates insertion point between build and push for hygiene assertions (Task 2)
- Preserves --target production, Dockerfile path, both tags, and root context
- WR-04 immutable-tag-first push ordering preserved in push step
- gate needs: expanded to include security
- individual needs.security.result check added (requires success, not success-or-skipped)
- security not folded into api/harness for loop — gitleaks always runs so it cannot be skipped
- follows Gitea #31007 individual needs.X.result pattern
- 613 commits scanned, ~23 MB of git history
- Zero findings after allowlisting crypto.test.ts AES test fixture
- Human-verified: single baseline finding (TEST_KEY) was a Vitest fixture, not a real credential
- Baseline is empty JSON array [] — PR-diff scans in 16-05 start from clean known state
- Add 4th [[allowlists]] block for apps/api/tests/broker/crypto.test.ts
- Human-verified: TEST_KEY in Vitest beforeAll is a synthetic AES-256-GCM fixture, not a real credential
- Original 3 allowlists (VAPID fixture, .env.example, .env.spike) intact
- Disable detect-object-injection globally in eslint.config.js: all hits were
numeric loop array indices (ranks[i]) — not user-controlled keys; zod guards
real API input boundaries; justification comment added (T-16-09)
- Add inline eslint-disable for detect-non-literal-fs-filename at 2 sites:
- apps/api/src/index.ts: realpathSync(process.argv[1]) — runtime entry path, not user input
- apps/api/tests/broker/expand.test.ts: readFileSync of test fixture path — test-controlled
- pnpm lint exits 0 across both apps with --max-warnings 0
- 14 of 15 security rules remain active at error; no blanket file disables
- Install eslint-plugin-security@3.0.1 to root devDependencies (pinned exact version; 3.0.1 chosen over 4.0.1 for bake time per RESEARCH)
- Add pluginSecurity import to eslint.config.js
- Insert security config block (files: apps/**/*.{ts,tsx}) with configs.recommended spread, placed before prettierConfig which stays last
- ESLint version unchanged at 9.39.4 (D-13-ESLint-PIN)
- Classifies outdated packages into four tiers: AUDIT-ADVISORY, MAJOR-BEHIND-INTENTIONAL,
MAJOR-BEHIND-UNPINNED, ROUTINE-DRIFT
- Reads outdated-pins.json for intentional pin reasons (eslint, @eslint/js, zod, @types/node)
- Cross-checks pnpm audit --json to flag pinned versions with active advisories
- Always exits 0 — never gates the build (D-06)
- Exports selectBlocking() and partitionAdvisories() as pure functions for unit testing
- Main body guarded by import.meta.url check (only runs when invoked directly)
- Uses pnpm audit --json (no --audit-level — Pitfall 1 honored)
- Exits 1 on unwaived High/Critical; exits 0 with advisory report on moderate/low
- All 5 unit tests pass (node --test)
- Tests four behavioral cases: unwaived High → blocking, waived High → not blocking,
moderate/low only → not blocking, no advisories → not blocking
- Uses node:test + node:assert (no extra dependencies)
- Fails at RED: check-audit.mjs does not yet exist