Skip to content

Promote dev to main: v0.3.1 - #205

Merged
tyler-rich merged 42 commits into
mainfrom
dev
Aug 14, 2026
Merged

Promote dev to main: v0.3.1#205
tyler-rich merged 42 commits into
mainfrom
dev

Conversation

@tyler-rich

@tyler-rich tyler-rich commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Promotes dev to main to cut v0.3.1 — a maintenance release with no new features, no schema change and no configuration change. 42 commits since v0.3.0.

Security

  • cryptography 49.0.0 → 50.0.0, closing CVE-2026-69247 (HIGH) — a Bleichenbacher-style oracle in the PKCS7 decryption helpers, where pkcs7_decrypt_der and its variants exposed distinguishable errors and timing while unwrapping an encrypted key. This is the finding that failed the dogfood gate against the published :latest image ([image-rescan] Fixable HIGH/CRITICAL CVEs in ghcr.io/tyler-rich/scrye:latest #194). Scrye uses four symbols from the library and never calls PKCS7, so no deployed instance was exposed; the bump is taken because the finding is fixable.
  • Runtime base image moved to CPython 3.14.7, the first release carrying backported fixes for the six interpreter CVEs ci/grype.yaml waives (CVE-2026-15308, CVE-2026-11940, CVE-2026-11972, CVE-2026-12003, CVE-2025-15366, CVE-2026-0864). All six were confirmed present by reading CPython at the v3.14.7 tag and diffing against v3.14.6. The waivers stay regardless: Grype's DB records the fixes only in 3.15.x, so it still reports all six against a 3.14.7 image. The locked runtime floor moves to 3.14.7 to match.
  • pip no longer ships in the runtime image, closing GHSA-6v7p-g79w-8964 (msgpack 1.1.2) and CVE-2025-47273 (setuptools 70.3.0) — versions vendored inside the base image's pip and therefore not ours to bump. Nothing in the runtime used pip, so it is deleted rather than excused with a scanner exception.
  • Frontend lockfile refreshed, closing GHSA-5p4m-2wfm-xmqj (js-yaml) and GHSA-2v37-7h3g-55p8 (nanoid). Both are transitive devDependencies and neither ships in the image, which copies only built output.

Fixed

Five state-carryover and rendering defects on the scans pages, surfaced while working through the react-hooks/set-state-in-effect sites #176 enumerates — most importantly the findings list no longer flashing "No findings match the current filters" over a request that has not answered yet, and a delete-confirm dialog left open across a navigation now closing instead of re-arming against the newly-opened scan (which could delete a scan the operator was never asked to confirm).

Changed

  • Bundled scanners updated to Trivy 0.73.0, Grype 0.116.1 and Syft 1.50.0, so scan results may differ from 0.3.0's for the same target. Syft's JSON schema moves only at patch level, so stored SBOMs and Scrye's parsing are unaffected.
  • The frontend toolchain sweep (frontend: bump the frontend-dependencies group across 1 directory with 25 updates #86) completed — TypeScript 6, ESLint 10, Vite 8, Vitest 4, jsdom 30 and @types/node 26 — with byte-identical build output throughout. The documented native-development floors move to Python 3.14.7+ and Node 22.22.2+ or 24.15+, stated in both README.md § Requirements and CONTRIBUTING.md § Prerequisites.
  • Backend dependency currency: fastapi 0.141.1, uvicorn[standard] 0.52.1, alembic 1.19.1, with backend/requirements.lock regenerated.

Upgrading

Pull and restart. No migration runs on first start — 0009_oidc_link_flows is still head — and no environment variable was added, removed or renamed.

Full detail in CHANGELOG.md § [0.3.1]; the release-prep record is in docs/ARCHIVE.md §14 (2026-08-11).

tyler-rich and others added 30 commits August 3, 2026 04:18
…147)

* backend: bump fastapi to 0.141.1 and uvicorn to 0.52.0

Applies the runtime bumps proposed by the post-v0.3.0 Dependabot group PR
(#144) and regenerates the hash-pinned lockfile, which Dependabot does not
do — its branch edits pyproject.toml only and so fails CI's drift gate.

FastAPI 0.141.x adds an app.frontend() dev-server convenience that Scrye
does not use; no routing, dependency-injection or response-model behaviour
changes. uvicorn 0.52.0 adds an opt-in experimental --http zttp parser that
upstream marks not-for-production; docker/entrypoint.sh passes no --http
flag, so the default parser selection is unchanged.

starlette holds at 1.3.1 — the pin carried for CVE-2025-62727 /
CVE-2026-48818 / CVE-2026-54283 — confirming FastAPI 0.141.1 still accepts
it. ruff moves 0.16.0 -> 0.16.1 in the dev extra.

Lock regenerated with the pinned uv 0.8.17 command from CONTRIBUTING.md
§ Backend dependency lock; the diff is those two packages and their hashes.

* ci: re-pin codeql-action to the commit behind v4.37.4

Dependabot (#146) proposed moving github/codeql-action/init and .../analyze
from ea14db8 to f205ea1 while leaving the trailing comment at v4.37.4. That
is not a mislabelled bump — resolving the tag upstream shows both SHAs are
the same release:

  ea14db8afdef5d462e69d78c4ca45002d4522418  refs/tags/v4.37.4
  f205ea1c3313d32999d8d6a48b4f6530d4437b38  refs/tags/v4.37.4^{}

codeql-action publishes annotated tags, so the old pin named the tag OBJECT
and the new one names the commit it dereferences to. The advanced-setup
migration pinned the tag object; a SHA-pin is meant to name an immutable
commit, which is what every other action pinned here already does — the
rest publish lightweight tags, where refs/tags/vX is the commit, which is
why this is the only ref affected.

No functional change: same release, same CodeQL bundle (2.26.2), same
security-extended suite. The old pin worked — Actions dereferences a tag
object — so this is pin correctness, not a fix for a broken workflow.

* frontend: apply the routine bumps from the post-v0.3.0 dependency group

Takes 8 of the 20 updates Dependabot proposed in #145 and narrows a 9th.
The Mantine/React ignores held — the PR proposed no locked major, and
@mantine/* moved 7.15.2 -> 7.17.8, a minor inside v7.

Applied: @mantine/core/form/hooks 7.17.8, @tabler/icons-react 3.46.0,
@testing-library/react 16.3.2, @testing-library/jest-dom 7.0.0,
postcss-preset-mantine 1.18.0, globals 17.8.0, prettier 3.9.6.

The two majors were checked rather than waved through. jest-dom 7 requires
@testing-library/dom as a peer (already a direct devDependency at 10.4.1)
and Node >= 22 (CI is on 24); it removes no matchers. globals 17 feeds
eslint.config.js's globals.browser, where a shrunken set would leave lint
passing while losing coverage — the set was inspected: 1191 entries, with
window/document/fetch present.

@types/node is narrowed from the proposed 26.1.2 to 24.13.3. Its major
tracks Node's, and the SPA builds on Node 24; tsconfig.node.json sets
types: [node], so types ahead of the pinned runtime describe APIs the build
does not have and feed them into the type-aware ESLint gate — the same
failure mode that put @types/react* in the ignore list.

Prettier 3.9 collapses short union types onto one line instead of the
leading-| form, which is the whitespace-only change to three source files.

Held back: typescript 7, eslint 10, @eslint/js 10, typescript-eslint 8.65,
vite 8, @vitejs/plugin-react 6, vitest 4, jsdom 30,
eslint-plugin-react-hooks 7 and eslint-plugin-react-refresh 0.5 — the
deferred #86 toolchain sweep, which lands on the type-aware ESLint gate and
is its own scoped work.

* ci: decline @types/node majors against the pinned Node 24

@types/node's major tracks Node's, and this repo builds and runs on Node 24
(the Dockerfile builder stage and ci.yml's node-version), with Node majors
already declined for the docker ecosystem on a support-lifecycle argument.
tsconfig.node.json sets types: [node], so a @types/node ahead of the pinned
runtime feeds APIs the build does not have into the type-aware ESLint gate
— the same reason @types/react* are majors-locked.

Without this, #145's @types/node 26 returns inside the grouped PR every
week. The comment also records what is deliberately NOT ignored: the
frontend toolchain majors are wanted work tracked in docs/ROADMAP.md and
should keep being surfaced.

* docs: log the post-v0.3.0 Dependabot triage

Adds the dated ARCHIVE.md §14 entry covering all three grouped PRs (#144,
#145, #146) — the base-branch check, the changelog findings, the annotated-
tag SHA-pin discovery, the applied/narrowed/held split on the frontend, and
the verification evidence. Refreshes the ROADMAP #86 sweep list to the
versions #145 surfaced and adds the four members that were not on the
original list. Records the bumps under CHANGELOG's Unreleased § Changed.
…#156)

CVE-2026-69247 (HIGH) is a Bleichenbacher-style oracle in the PKCS7
decryption helpers: pkcs7_decrypt_der and its variants exposed
distinguishable errors and timing while unwrapping an encrypted key.
Upstream substitutes a random key on failure, per RFC 3218.

Verified against pyca/cryptography's CHANGELOG.rst rather than the
scanner's FIXED IN column, per CLAUDE.md § Interpreter CVEs: the fix is
recorded under 50.0.0 (2026-07-31), and 49.0.0 (2026-06-12) predates it.

Scrye never reached the vulnerable path — it uses four symbols from the
library, all in backend/app/core/crypto.py (AESGCM, HKDF, hashes,
InvalidTag), and never calls PKCS7. The bump is taken because the finding
is fixable, which is what the dogfood gate keys on; unreachability is a
reason not to panic, not a reason to waive.

50.0.0 is a major whose breaking changes miss that surface: FFDH
deprecated, X.509 verification APIs stabilised, SCT/X.509 validation
tightened. The ChaCha20 counter change landed in 49.0.0 and was already
absorbed. All four APIs were exercised against 50.0.0 first, including an
AES-GCM round trip under a row-bound AAD and the InvalidTag raise on AAD
mismatch that secret_store.py's row_aad() binding depends on.

requirements.lock regenerated with the pinned uv 0.8.17 command from
CONTRIBUTING.md § Backend dependency lock; the diff is that one package
and its hashes, with no transitive churn.

Surfaced by the dogfood self-scan going red on an unrelated docs-only PR
(#155) — see docs/ARCHIVE.md § Deviations for why that matters now that
the gate is a required check.
The post-v0.3.0 Dependabot triage entry recorded uvicorn 0.52.0's WebSocket
header fix as applying to websockets 13.0. Upstream's release notes say
17.0 — the fix prepares for websockets 17.0 encoding headers as ISO-8859-1.
13.0 came from a misread of the changelog while writing the entry.

Nothing about the triage decision changes: the fix is unrelated to how
Scrye runs uvicorn either way, and the lockfile pins websockets 16.1.
…A + Track B) (#155)

* docs(roadmap): replace ROADMAP.md with the two-track revision (Track A + Track B)

Replaces docs/ROADMAP.md wholesale with an externally-drafted revision. The
previous document becomes Track A — Engineering & hardening, carried across
verbatim; Track B — Features is new, covering six phases plus a Deferred list,
an out-of-scope policy list, a licensing & bundling policy table, and the
guiding principles. Known limitations & accepted trade-offs is unchanged.

Track A's only substantive edits are three † prerequisite markers, on the items
that gate a Track B feature: content-addressed SBOM identity (Phase 2
fix-watch), cancel a running scan (endpoint-scanning target class), and the
generated API client (Phase 4 MCP server).

Track A and Known limitations were diffed line by line against the outgoing
file before replacing, and every §14 pointer was re-checked against the entry
it names rather than against its presence in the old file. One regression was
caught: the draft's "Frontend tooling majors from Dependabot #86" paragraph
predated 004d2b5 (#147), so taking it as written would have reverted the #145
shopping-list refresh, dropped four toolchain members, reverted jsdom 26 -> 30
to 26 -> 29, and removed the sentence that mirrors dependabot.yml's
"Deliberately NOT ignored" comment. The current paragraph is spliced in
verbatim.

See docs/ARCHIVE.md § Deviations for the full verification record.

* docs(archive): link the ROADMAP replacement deviation entry to its PR
…eue audit (#159)

uvicorn[standard] 0.52.0 -> 0.52.1 and alembic 1.18.5 -> 1.19.1 in
backend/pyproject.toml, with backend/requirements.lock regenerated by the
pinned uv 0.8.17 command. Dependabot edits pyproject.toml only and leaves the
lock stale, so #157's branch fails CI's lock-drift gate; its content is
reapplied here instead.

- uvicorn 0.52.1: four WebSocket-only fixes; Scrye serves no WebSocket route,
  so none of it is reachable — pure currency.
- alembic 1.19.1, not the 1.19.0 #157 proposes: 1.19.1 (published hours after
  #157 opened) fixes a defect in the named-CHECK-constraint autogenerate
  feature 1.19.0 added. Both confined to migration authoring.

Also adds the docs/ARCHIVE.md §14 entry for the full open-queue audit (#149,
#150, #151, #153, #154, #157) and a CHANGELOG [Unreleased] entry.
…ker-images group (#151)

Routine digest refresh of the pinned debian:bookworm-slim base image for the
scanners and runtime stages, proposed by Dependabot's docker ecosystem.
github/codeql-action init/analyze moved to v4.37.6, pinned to the commit the
annotated tag dereferences to (5595ccaf = refs/tags/v4.37.6^{}, verified with
git ls-remote against upstream) — the correct pin form for this repo's
annotated tags, consistent with the #146 correction.
….0 (#160)

The build ARGs are invisible to every Dependabot ecosystem (they are not FROM
images), so these are moved by hand. Verified at the source: each version is
the current upstream release per git ls-remote, no release crossed documents a
breaking, deprecation, or CLI change, and syft's JSON schema moves only at
patch level (16.1.5 -> 16.1.10), so JSON parsing and persisted SBOMs are
unaffected.

Moved in lockstep, per the pins' own documentation: ci.yml and rescan.yml
scan-image pins (documented as pinned to the bundled versions), with digests
resolved from the Docker Hub registry; THIRD_PARTY_LICENSES/ re-verified at
the new tags (every LICENSE and Trivy's NOTICE byte-identical upstream,
Grype/Syft still ship no NOTICE), so only its version table moves; README's
Integrations versions; a CHANGELOG Unreleased entry.

test_scanner_symlink_containment.py re-run against the real downloaded
syft 1.50.0 and grype 0.116.1 binaries on CPython 3.14.6 (7 passed), and
repeated by CI's image job against the binaries the image ships.
…cker-images group (#150)

Moves the optional trivy-server sidecar in docker-compose.yml to 0.73.0 in
lockstep with the bundled TRIVY_VERSION bumped by #160. The proposed digest
was independently re-resolved against the Docker Hub registry and matches.
…decision, and the governance re-verification (#161)

docs/ARCHIVE.md gains the dated §14 entry for the 2026-08-09 round: the
merge/close outcome for every queue member, the #158 -> #159 branch
replacement and its mechanics corrections, the DECLINED profile-display-name
decision, the verified-unchanged alembic timing claim, the re-verified
ignore-list-on-main finding with its options and costs, the live
governance/ruleset re-verification, the sandbox-interpreter resolution, and
the environment's footer-append behaviour on GitHub API writes.

CLAUDE.md § Git & PR conventions and docs/ROADMAP.md now record the display
name as declined-not-deferred so future sessions do not re-raise it. The
ROADMAP's CodeQL item is reduced to alert disposition (both settings edits
verified done via live ruleset reads), the 2026-08-02 governance list is
re-dated with what was re-verifiable, and the #86 sweep entry names #153 as
its open, deliberately unactioned Dependabot expression.
…ument dependabot ignore-list promotion lag (#162)

- CLAUDE.md: remove the instruction to re-check and strip PR-body attribution
  footers (verified not to work; a PATCH stripping the footer gets it
  re-appended server-side), replace with a short Attribution section that
  says never write one and never try to remove one that appears after the
  fact.
- .gitignore: ignore .pr-body.md, a prior-session scratch artifact.
- CONTRIBUTING.md: document that .github/dependabot.yml's ignore list is
  read from main (not target-branch), same root cause as security updates
  targeting main; confirmed Dependabot security updates are enabled.
- docs/ARCHIVE.md: dated entry recording the maintainer's manual Settings
  verification (Actions secrets, Dependabot toggles, workflow permissions,
  code scanning, Actions SHA-pin requirement), Secret Protection enabled,
  the SHA-pinning audit (all uses: lines already pinned to resolved commit
  SHAs), the attribution-stripping ban, and the declined dependabot.yml
  main-promotion option.
- docs/ROADMAP.md: close the CodeQL alert-disposition item (0 open / 6
  closed on both branches) and update the governance-checklist paragraph
  with the 2026-08-09 manual verification.

No code, schema, or workflow-trigger changes.
…nce (#163)

Adds docs/upgrades/frontend-toolchain-86.md, turning docs/ROADMAP.md § Track A's
"Frontend tooling majors from Dependabot #86" — one PR (#153) carrying eleven
majors behind a single red check — into eight independently verifiable steps.

Every constraint is cited to a published peerDependencies range or an upstream
migration guide; thirteen package tarballs were unpacked to compare shipped
config objects directly.

- TypeScript 7 is out, as of 2026-08-09: typescript@7.0.2 is the Go-native
  compiler and no longer exposes the classic compiler API that
  @typescript-eslint/typescript-estree uses (114 distinct ts.* symbols). Written
  as a dated ceiling with a one-command re-check, not a permanent blocker.
- #153's red check is an unsatisfiable dependency graph, not lint churn: npm ci
  fails on ERESOLVE and ESLint never runs. Corrected here and on the PR.
- typescript-eslint is the only unblocking move (8.19.0 caps TypeScript at
  <5.8.0; 8.56.0 admits ESLint 10; 8.58.0 admits TypeScript 6), and its
  recommendedTypeChecked rule set is unchanged across the span.
- ESLint 10 peer-forces eslint-plugin-react-hooks 7, whose recommended config
  adds the React Compiler rules; that adoption is split into its own step.
- Vitest 4 needs no Vite major; @vitejs/plugin-react 6 does require Vite 8.
- No sweep member declares a react/react-dom/@Mantine peer, so no locked
  decision is at risk. react-router 8 does, and is called out separately.

Resolved at source: the pinned node digest ships Node 24.18.1 (satisfies jsdom
30); TypeScript 6.0's breaking changes, audited option by option against both
tsconfigs; jsdom 27/28/29's changelog; and GHSA-qwww-vcr4-c8h2 confirmed as an
upstream re-cut rather than a registry quirk.

Scoping only: no dependency version, lockfile, or config file changed.
docs/ROADMAP.md deliberately left unedited.

See docs/ARCHIVE.md § Deviations for the dated entry.
…edit to its step (#164)

Follow-up to #163 on the same document. Sequence membership is unchanged; only
estimates, ordering rationale, and the placement of one edit moved.

The jsdom step is re-costed against the real test code now that the 27/28/29
changelogs are readable. Two channels are live: the 27.0.0 selector-engine swap
(nwsapi -> @asamuzakjp/dom-selector) sits under 116 Testing Library query call
sites, and the re-derived UA stylesheet reaches the suite through exactly one
path, verified in the installed @testing-library/dom 10.4.1 —
isSubtreeInaccessible() reads getComputedStyle(el).display, defaultHidden is
false, and the repo never calls configure(), so all 34 getByRole sites run that
filter. Four channels are provably inert, including the two that sound worst:
the CSSOM rewrite has no author CSS to act on (Vitest defaults to css: false),
and click() -> PointerEvent is unreachable since there is no .click() anywhere
in src/.

Net: effort widens S-M to S-L, risk holds at medium, because detection is
complete — 79 assertions in ~15s, with no failure mode that survives a green
run.

jsdom and Vite 8 swap, making jsdom step 6 and Vite step 7: steps 4-7 have no
mutual dependency, so their order is a judgement about verification cost, and
this spends the cheap total oracle before the expensive partial one. The
original order is recorded as not wrong.

Adds an explicit risk ranking to section 8. jsdom is riskier than
typescript-eslint and already was in the first draft, but typescript-eslint was
never the top of the ranking — step 3 and the Vite step outrank both, and jsdom
now sits third. Every other estimate was re-checked and confirmed unchanged.

The "types": [] recommendation now appears as a checklist item under step 4
rather than only as prose in the section 7.2 audit.

Scoping only: no dependency version, lockfile, or config file changed.
docs/ROADMAP.md deliberately left unedited.

See docs/ARCHIVE.md for the dated entry.
…fy it (#165)

The jsdom step's medium rating rested on "detection is complete — no failure
mode that survives a green run". That was asserted rather than argued, and it is
too strong: a green suite catches a query that finds nothing or too much, but
not one that resolves to a different element while downstream assertions still
pass. Section 8 now carries a four-part audit against the suite instead.

- getBy* throws on zero matches and on more than one, so silent one-to-one drift
  needs the engine to stop matching A and start matching exactly one B in the
  same pass; any widening raises "found multiple elements" instead.
- 62 of the 116 query sites are discriminated by accessible name or label text
  computed in JS, so B would need an identical accessible name to A — which is
  the >1-match throw condition.
- 85 of the 116 are assertion subjects, categorised rather than estimated.
- The suite's only set-valued query feeds a helper asserted with toEqual on the
  full ordered array, which also pins its only raw selector use.

The residual it cannot argue away — roughly 17 interaction targets — becomes a
checklist item on step 6: a resolved-element snapshot diffed across the bump,
turning a judgement into a two-run measurement.

Two corrections fell out of the same check: "79 assertions" was the test count,
not the assertion count (the suite runs 151 expect calls), and "every query
bottoms out in querySelectorAll" overstated the selector engine's reach, since
getByText and getByLabelText use '*'-class candidate selectors.

Rating unchanged: jsdom stays medium and stays third. No estimate, ordering, or
sequence membership changed.

Scoping only: no dependency version, lockfile, or config file changed.
docs/ROADMAP.md deliberately left unedited.

See docs/ARCHIVE.md for the dated entry.
…and GHSA-2v37-7h3g-55p8 (#168)

Two HIGH advisories in the frontend build/dev toolchain, both transitive and
both fixed inside the ranges their requiring packages already declare, so this
is a package-lock.json change only — package.json is untouched.

- js-yaml 4.3.0 -> 4.3.1 (GHSA-5p4m-2wfm-xmqj, CVSS 7.5, CWE-407): quadratic
  CPU consumption resolving a !!omap. Reached via eslint 9.39.4 ->
  @eslint/eslintrc 3.3.5, which declares js-yaml ^4.1.1.
- nanoid 3.3.16 -> 3.3.18 (GHSA-2v37-7h3g-55p8, CVSS 5.9, CWE-835): a custom
  generator loops indefinitely when size is zero. Reached via postcss 8.5.25,
  which declares nanoid ^3.3.16. The advisory's fix is 3.3.17; npm update
  resolves to 3.3.18, the highest release in range, which extends the same
  zero-size guard to the async native entry point.

Both fixes were verified against the published tarballs rather than taken from
the advisory metadata. Neither package ships in the image: both are dev-only,
neither appears in the built bundle, and the runtime stage copies
frontend/dist with no node_modules.

Refreshed with the targeted `npm update <pkg>` documented in CONTRIBUTING.md.
The lockfile diff is exactly those two entries, six lines each way, with no
transitive requirement moved. npm audit afterwards reports 0 at every
severity. ESLint clean, Prettier clean, 79 tests across 21 files passing, and
the build unchanged at 7,035 modules -> 645.18 kB JS / 201.38 kB CSS.

Deliberately separate from the #86 frontend toolchain sweep: this is none of
its eight steps, takes no major, and leaves #153 untouched.

See docs/ARCHIVE.md § Deviations for the full record.
Step 1 of the eight-step frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md, taken alone because it is the only
unblocking move in that sequence. The pinned 8.19.0 capped typescript at
<5.8.0 and eslint at ^9; 8.66.0 raises those to <6.1.0 and ^10.0.0, which
is what steps 2 (ESLint 10) and 4 (TypeScript 6.0.3) are gated behind.

Only typescript-eslint moves. No ESLint or TypeScript config was edited,
typescript stays at 5.7.2 and eslint at 9.39.4, and no later sweep step is
touched.

Two of the scoping document's predictions did not hold:

- An existing rule's implementation got stricter. The rule set is unchanged
  as predicted (recommended-type-checked.js is identical across the span,
  50 entries either side), but no-unnecessary-type-assertion now catches
  what it previously missed and reported two redundant `'' as string`
  assertions in NewScanPage.tsx. Removed by hand at those two sites only;
  tsc -b confirms the inferred useForm type is unchanged and the emitted
  bundle keeps the same content hashes. "No new rules" does not mean "no
  new findings."
- The effective rule set did change. The document's byte-for-byte claim
  diffed one of the three files recommendedTypeChecked composes; the layer
  it did not diff gains `no-with: 'off'`, dropping that rule from error to
  off in the resolved config. Accepted rather than restored: `with` is a
  hard compile error under this repo's tsconfigs, verified at the compiler
  (TS1101 plus TS2410) rather than argued from the strict flag.

Lockfile movement is confined to the typescript-eslint subtree, 374 -> 362
packages, with each of the 17 removals checked to have no surviving
requirer. Written with npm 11.19.0 to match CI's Node 24.

Suites unchanged against a re-measured pre-bump baseline: lint clean,
Prettier clean, 79 tests across 21 files, 7035 modules to 645.18 kB JS /
201.38 kB CSS, npm audit 0 at every severity.

See docs/ARCHIVE.md section 14 for the full record.
§0.3 claimed the shipped recommendedTypeChecked config was byte-for-byte
identical across the typescript-eslint bump. That was only checked for one of
its three composite layers (base + eslint-recommended + recommended-type-checked);
the undiffed eslint-recommended-raw.js layer went 22 to 23 entries, adding
no-with: 'off', which does change the resolved config this repo lints with.

Rewrites §0.3 to state what was verified vs. not, reworks the headline so it
cannot be read as predicting zero findings, adds a method note binding on
every remaining step (re-verify against the installed tree, every composite
layer, via eslint --print-config before/after), updates Step 1's and Step 2's
rows accordingly, and records the correction in docs/ARCHIVE.md §14.

Sequence membership and ordering are unchanged. No dependency, lockfile, or
config file touched; no sweep step executed.

Also closes #170, Dependabot's regenerated frontend-dependencies group, which
still proposes typescript 7.0.2 against typescript-eslint's <6.1.0 peer cap
(verified against the PR's actual diff) -- the same unsatisfiable graph #153
carried. docs/upgrades/frontend-toolchain-86.md remains the tracking surface.
…les inert (#174)

Step 2 of the frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md:

  eslint                       9.39.4  -> 10.8.1
  @eslint/js                   9.39.4  -> 10.0.1
  eslint-plugin-react-hooks    5.1.0   -> 7.1.1   (peer-forced)
  eslint-plugin-react-refresh  0.4.16  -> 0.5.3

Unblocked by step 1: typescript-eslint@8.66.0 is the first release peering
eslint ^10.0.0. Lint-only — no source file changed, the suites are unchanged,
and the emitted bundle carries the same content hashes as before.

eslint-plugin-react-hooks 7.x folds the React Compiler rule set into
configs.recommended, taking it from 2 rules to 16 (13 error, 3 warn). Since
eslint.config.js spread that object, the spread is replaced by the two rules
5.1.0 contributed, written out. Adopting the compiler set is step 3 and is a
separate decision; eslint --print-config confirms exactly those two
react-hooks rules resolve, at their original severities, for all three file
classes.

The resolved rule set moves 118 -> 121 across app .tsx, library .ts and the
test override alike: @eslint/js 10's recommended adds no-unassigned-vars,
no-useless-assignment and preserve-caught-error at error (61 -> 64 entries,
nothing removed or re-severitied), and no-shadow-restricted-names now reports
globalThis by default. Lint is clean regardless.

ESLint 10 drops @eslint/eslintrc, so eslintrc and js-yaml leave the tree
entirely. None of v10's removals apply here: no .eslintrc*, no eslint-env
comments, no custom rules or SourceCode API use, and the new engine floor is
satisfied by CI's Node 24 and the pinned node:24 digest.

See docs/ARCHIVE.md § Deviations for which of the scoping document's
predictions held and which did not.
…ect back (#177)

Step 3 of the frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md. No dependency version moves:
package.json and package-lock.json are byte-identical. Step 2's holding
edit is replaced by the ...reactHooks.configs.recommended.rules spread it
was standing in for, taking the resolved config 121 -> 135 rules across all
three file classes.

Twelve of the fourteen added rules report nothing against the current tree,
including immutability, purity and preserve-manual-memoization. The cost is
two rules and 24 findings.

react-hooks/refs (6 findings) is fixed by hand: all six are one idiom in
ScansPage, a useRef whose .current was read during render to seed six
useState initializers. A lazy useState initializer runs viewFromParams once
on first render exactly as the ref did, without the render-time ref read.

react-hooks/set-state-in-effect (18 findings) is left off, with the reason
in the config and the work tracked in #176. Only 6 of the 18 are the
synchronous setState the rule's rationale describes, and two of those are
deliberate effects that each closed a real bug (L17/P2-2 and P3-2), so #176
names them so they are not "fixed" blind. The other 12 are the
fetch-on-mount idiom where every setState runs after an await; a probe
showed the report tracks what the compiler can see rather than a
behavioural difference, so they are reported rather than worked around and
are deliberately outside #176's scope.

See docs/ARCHIVE.md § Deviations for changes made in this step.
The reset effect in ScanDetailPage has guarded against two scans' state
mixing since 2026-07-13, but nothing tested it. The three existing
ScanDetailPage suites — findingsTable, latestwins, poller — never navigate
between two :scanId values, so the effect could have been deleted with the
whole suite staying green. The gap surfaced while auditing #176, which
lists that effect as one of six sites a future change may touch and warns
against "fixing" it blind; that warning was leaning on a test that did not
exist.

Added as its own file rather than folded into an existing suite: findingsTable
never mounts the page, and latestwins and poller are each scoped to one
concern with deliberately minimal fixtures, while this needs two scans
differing in id, target, tags, findings and artifacts. One concern per file,
named for it, is the existing convention.

The test renders /scans/1, edits the tag draft away from the server value,
navigates to /scans/2 with the second fetch held open, and asserts none of
scan 1's header, findings, artifacts or tag draft survives — then that the
stale draft does not reappear once scan 2 lands.

Verified to catch the regression rather than merely to pass: with the reset
effect temporarily deleted the test fails on the first in-flight assertion,
with scan 1's target still rendered. The effect was then restored and the
file confirmed byte-identical to dev.

No production code changed. The effect was not refactored toward the
key-prop remount #176 names, and none of the other five findings it tracks
was touched.

See docs/ARCHIVE.md § Deviations for changes made in this step.
Step 4 of the frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md. Only `typescript` moves: the
lockfile diff is one entry, 346 packages either side, nothing added,
removed, or bumped transitively.

6.0.3 is the ceiling, not the latest. Re-checked at the registry before
editing anything: typescript-eslint 8.66.0 (latest) and its
8.66.1-alpha.10 canary both peer `typescript: ">=4.8.4 <6.1.0"`, as do
@typescript-eslint/parser and /typescript-estree, so no published
typescript-eslint accepts the Go-native TypeScript 7. 6.0.3 is also
still the head of the 6.x line — 6.0.2 and 6.0.3 are the only stable
releases inside `<6.1.0`.

tsconfig.app.json pins `"types": []` rather than inheriting TypeScript
6.0's new default. Verified a no-op for src/ by audit (no
process/Buffer/__dirname, no NodeJS. namespace, all 22 test files import
their globals from 'vitest', timers go through window.setTimeout) and
then empirically, by applying the edit under 5.7.2 first — where the old
enumerate-everything default was still live — and confirming `tsc -b
--force` and lint stayed clean. tsconfig.node.json already set
`"types": ["node"]` and is unchanged.

The `this`-less function inference change surfaced with no error and no
deprecation diagnostic, and was found by diffing emitted declarations
across both compilers: one line in 79 .d.ts files, where React.memo's
overload for FindingsTable resolves to MemoExoticComponent instead of
NamedExoticComponent. A type probe confirms both compilers accept the
exact prop object and reject an extra and a missing prop identically,
and the repo emits no declarations, so nothing consumes the printed
form. Reported, not worked around.

`eslint --print-config` is byte-identical before and after on an app
.tsx, a library .ts and the test override (135 rules each). Suites from
a fresh `rm -rf node_modules && npm ci`: lint clean, format:check clean,
80 tests across 22 files, npm audit 0, and the build emits the same
content hashes as the baseline.
Step 5 of the frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md. `vitest` is the only package
bumped; jsdom stays at 26.1.0 (step 6) and vite at 6.4.3 (step 7).

vitest@4.1.10 peers `vite: "^6.0.0 || ^7.0.0 || ^8.0.0"` as a required
peer, re-read at the published manifest, so Vitest 4 needs no Vite major
and the 5/6/7 ordering holds. 4.1.10 is still `dist-tags.latest`.

vite.config.ts needed no edit, confirmed against the shipped 4.1.10
tarball: `declare module "vite"` still augments UserConfig with `test`,
`extends?: string | true` survives, and `projects` is already the current
spelling. Nothing else in v4's breaking surface has a consumer here.

One type error had to be fixed, in test code. Vitest 4 widened `vi.fn`'s
type-parameter constraint to `Procedure | Constructable`, so the alias
`ReturnType<typeof vi.fn>` — which instantiates at the constraint, not
the default — no longer satisfies a plain call signature, and `tsc -b`
failed where such a mock is passed to `.mockImplementation()` on a
`History.replaceState` spy. Typed against the real method signature at
that one site; no autofix.

`vi.restoreAllMocks()` also changed meaning (v4 restores only `vi.spyOn`
spies), which reaches OidcLinkCard.test.tsx. Measured on both versions
and instrumented in the real suite: the carryover is real and inert, and
no test passes for a different reason than it did on 3.2.7.

Suites from a fresh `rm -rf node_modules && npm ci`: lint clean,
format:check clean, 80 tests across 22 files — identical to the baseline
per test name and status — audit 0, and a build whose emitted assets
carry the same content hashes as the baseline.

See docs/ARCHIVE.md § Deviations for the full record of this step.
…ping ban (#182)

Adds a docs/ARCHIVE.md §14 entry documenting that PR #169 reversed the
attribution-stripping ban a few hours after it was recorded, and that
the reversal itself was never logged per this repo's own deviation
rule. Does not relitigate which policy is correct — CLAUDE.md's current
strip-and-reverify text stands as-is.

Adds one line to CLAUDE.md § Attribution: future attribution-policy
changes must include their §14 entry in the same PR that changes the
policy, not as a follow-up.
* chore(deps): bump jsdom 26.1.0 -> 30.0.1 (#86 sweep step 6)

Step 6 of the eight-step frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md. jsdom is the only package bumped;
vite stays at 6.4.3 (step 7) and no config, source or test file changed.

jsdom is a devDependency reached only through Vitest's environment: 'jsdom',
so the emitted bundle is unchanged down to its content hashes.

jsdom 27.0.0 swapped the CSS selector engine (nwsapi ->
@asamuzakjp/dom-selector), and a Testing Library query can resolve to a
different element after such a swap while every downstream assertion still
passes. Per the document's Step 6 checklist, a throwaway setupFiles shim
logged each resolved element across the whole suite on both jsdom versions
and the two logs were diffed: 176 query resolutions across 46 tests in 17
files, diff empty. The shim was deleted before this commit.

Written as the checklist specifies -- outerHTML.slice(0, 120) -- the shim is
not deterministic: Mantine's useId mints fresh ids per run, and one button
was caught mid-loading-state. Two same-version runs diffed non-empty before
the version was touched. The shim now logs the element's DOM index path plus
a normalised outerHTML, and its determinism was verified on both versions.

The Node floor rises: jsdom@30.0.1 requires
^22.22.2 || ^24.15.0 || >=26.0.0. CI's node-version "24" resolves to 24.19.0
and the pinned node:24-bookworm-slim digest ships 24.18.1, so ci.yml and the
Dockerfile need no change; README.md and CONTRIBUTING.md said "Node 22+",
under which Node 22.13 installs and then runs npm test unsupported, and are
raised to "Node 22.22.2+ or 24.15+".

Lockfile 338 -> 340 packages, every movement attributed to jsdom or its
transitive closure. Suites unchanged: lint clean, format:check clean, 80
tests across 22 files (compared per test name and status, not per total),
build identical, npm audit 0.

See docs/ARCHIVE.md section 14 for the full record.

* docs: record the drift-shim specification defect as a standing note

The section 8 shim spec in docs/upgrades/frontend-toolchain-86.md -- log
element.outerHTML.slice(0, 120) keyed by test name and call index -- is
methodologically broken, not just in this step's execution. Any app whose
component library mints identifiers per render puts a fresh random id
inside the first 120 characters of most elements, so a diff of two logs
measures the id generator rather than the DOM. Mantine's useId does this
here; React's own useId, Emotion, Radix, Headless UI, Chakra and MUI all
do the same thing. outerHTML also captures contents, so an element caught
mid-transition differs between runs while being the same element.

The failure points the wrong way rather than merely adding noise: a
spec-conformant shim yields a large non-empty diff on a bump that changed
nothing, which is exactly the signal the check exists to raise, and the
likely response is to discredit the measurement and fall back on the green
suite -- the state the checklist item was written to escape.

Records the replacement as a template for future drift checks: log the DOM
index path as the element's identity with an id-normalised outerHTML slice
as a readable label, and calibrate the noise floor by running the shim
twice on the unchanged version before touching the dependency.

The sweep document itself is left unedited for maintainer review.
…-> 6.0.5 (#185)

Step 7 of the frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md. The two move in lockstep because
@vitejs/plugin-react@6.0.5 declares vite: "^8.0.0" as a required, non-optional
peer. These are the only two packages bumped; vite.config.ts, both tsconfigs,
eslint.config.js and every source and test file are untouched.

Vite 8 replaces Rollup + esbuild with Rolldown + Oxc and moves CSS
minification to Lightning CSS, so this is the one step in the sweep that
changes what ships. Output was compared against the baseline rather than
assumed equivalent:

  modules  7,035 -> 7,018 (-17)
  JS       645.14 kB -> 630.29 kB (-2.30%), gzip 193.61 -> 187.36 kB
  CSS      201.38 kB -> 196.79 kB (-2.28%), gzip 29.30 -> 28.63 kB
  build    7.30 s -> 1.28 s

The module delta is entirely CommonJS-interop scaffolding: a module census
under both bundlers shows the 19 Vite-6-only ids are commonjsHelpers.js plus
the ?commonjs-* proxy modules @rollup/plugin-commonjs mints for react,
react-dom, scheduler, cookie, fast-deep-equal and set-cookie-parser. Rolldown
handles CommonJS natively. No application or library module was added or
removed.

The CSS was diffed declaration by declaration, with selector lists split into
individual selectors so rule merging and splitting cancel out: 1,171 selector
keys on each side, none gained or lost, and zero declarations added or
dropped. All 46 differing declarations are semantics-preserving minifier
rewrites (29 vendor prefixes dropped where the unprefixed property is present,
`ease` elided as the initial timing function, transparent -> #00000000,
shorthand merges, and the six ::-webkit-* selectors split out of one comma
list, which is a correctness improvement).

Rendering was compared pixel by pixel across six routes in both light and dark
mode. A first pass diffed non-empty; re-running the same build twice exposed a
~135-pixel noise floor from in-flight animations, so that pass measured
nothing. With animations frozen the noise floor is exactly zero across all
twelve views, and against it all twelve are pixel-identical.

The one genuine behaviour change is the browser target: Vite 8 defaults to
baseline-widely-available (chrome111/edge111/firefox114/safari16.4), up from
esbuild's modules default. Every syntax Lightning CSS newly emits is inside
that target. No project document states a browser floor.

plugin-react 6.0.0 removed all Babel features. This repo passes no babel
option and has no babel config anywhere, so nothing moved to
@rolldown/plugin-babel and nothing was dropped. React Fast Refresh now runs on
Oxc and was smoke-tested on the dev server.

Lockfile 339 -> 305 packages, every movement attributed to a requirer.
@babel/core and @babel/parser remain, now required only by
eslint-plugin-react-hooks. Lint, format:check and audit clean; 80 tests across
22 files, compared per test name.

See docs/ARCHIVE.md § Deviations for the full record, including a correction
to the scoping document's §3.3.
 sweep step 8) (#187)

Step 8 — the final step — of the frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md. Three routine minors/patches with no
config change and no coupling; all three re-confirmed as dist-tags.latest
before the bump.

The lockfile diff is +13/-13: 306 packages before and after, zero added, zero
removed, exactly the three targets bumped. postcss@8.5.26 raises its own nanoid
requirement to ^3.3.17, which installs nothing — nanoid@3.3.18 has been in the
tree since the 2026-08-09 advisory refresh.

globals.browser was inspected rather than trusted to a green lint run, per the
row's inherited caution: 1191 -> 1196 keys, five added and none removed.
eslint --print-config on one file of each class holds at 135 rules with nothing
added, removed, or re-severitied. All three emitted assets are byte-identical
to the baseline by SHA-256 — the check that matters because postcss runs in the
build path via postcss.config.cjs.

Suites on both sides from a clean npm ci: lint clean, format:check clean, 80
tests across 22 files (diffed per test name and status, empty), build 630.29 kB
JS / 196.79 kB CSS, npm audit 0. Nothing was autofixed; there were no findings.

See docs/ARCHIVE.md § Deviations for the full record, including the
sweep-completion assessment.
…r 8 (#189)

Three Track A edits in docs/ROADMAP.md, each gated on a live check made
this session rather than on a prior session's claim:

- Strike the "Frontend tooling majors from Dependabot #86" item as Done
  2026-08-09, naming all eight sweep PRs (#171, #174, #177, #179, #180,
  #183, #185, #187) and both deliberately-excluded packages (TypeScript 7
  and @types/node 26). All eight squash commits re-confirmed present on
  dev before writing the bullet.

- Strike the GHSA-qwww-vcr4-c8h2 re-cut request: the advisory has already
  been re-cut upstream to >= 7.12.0, < 7.18.2 plus >= 8.0.0, < 8.3.0,
  with the 8.x range untouched. Verified against the record in
  github/advisory-database and independently against the npm registry's
  advisory endpoint, which returns nothing for 7.18.2.

- Reword react-router 7 -> 8 as blocked on a React 19 decision rather
  than on a tooling bump, and move it out of the tooling-majors grouping
  into Longer-term / speculative. react-router@8.3.0 peers react and
  react-dom at >=19.2.7; this repo pins both at 18.3.1.

See docs/ARCHIVE.md section 14 for the dated record of what was verified
for each.
Takes the one package the #86 frontend toolchain sweep deliberately
declined (docs/upgrades/frontend-toolchain-86.md §5), on the maintainer's
instruction and on currency grounds — nothing in the toolchain requires
it. The only @types/node constraints in the resolved tree are the
optional peers of vite@8.2.1 and vitest@4.1.10, both already satisfied by
24.13.3.

§5's argument against it was that tsconfig.node.json's `"types": ["node"]`
feeds runtime-ahead types into the type-aware lint gate. Step 4 (#179)
retired that premise by writing `"types": []` into tsconfig.app.json:
`tsc --listFiles` now loads 1,063 files for the app project and zero of
them are @types/node, against 82 for the node project. The whole surface
of this bump is vite.config.ts, whose only Node API use is `process.env`
— and `interface ProcessEnv extends Dict<string> {}` is character-identical
across the two versions.

Node 24 compatibility measured rather than inferred from the major
number, in both directions. Every exported symbol of every node: module
was enumerated from both packages with the installed TypeScript 6.0.3
compiler API: 26.2.0 adds two modules and 263 symbols and drops 72 (31 of
them value exports). Checked against a real Node 24.19.0 — the version
ci.yml's `node-version: "24"` resolves to — 26 of those 31 still exist on
Node 24 (24 top-level zlib.Z_* aliases, assert.CallTracker,
buffer.SlowBuffer), and 44 of the 65 added value exports do not
(node:ffi and node:quic wholesale). Neither list is referenced anywhere
in this repository, and neither is reachable from src/.

Lockfile: 306 -> 306 packages, +8/-8, zero added, zero removed, two
bumped — @types/node and its sole dependency undici-types 7.18.2 -> 8.3.0.
Written with npm 11.19.0 in a scratch prefix to match CI's Node 24.

eslint --print-config diffed before and after on the three standard file
classes plus vite.config.ts, the one file this bump can reach: all four
byte-identical at 135 rules. Lint, format:check, tsc -b --force and
npm audit clean on both sides; 80 tests across 22 files, diffed per test
name rather than by total; all three emitted assets byte-identical by
SHA-256.

No lint rule was disabled, downgraded or suppressed, and no other package
moved — typescript stays at 6.0.3.
…192)

@types/node 26.2.0 landed on dev in #190 (5948b73), so the ignore's
premise is spent. The comment it carried argued that tsconfig.node.json's
`"types": ["node"]` feeds a runtime-ahead @types/node straight into the
type-aware ESLint gate — true of the tree it was written against, and no
longer true of this one. Step 4 (#179) wrote an explicit empty `types`
array into tsconfig.app.json, which narrows the package's reach to a
single file: `tsc --listFiles` loads 1,063 files for the app project and
zero of them are @types/node, against 82 for the node project. The rule
and its comment are removed together rather than the comment being
rewritten, since a comment whose premise has been retired reads as a live
argument.

Deliberately NOT done: no ignore rule was added for typescript. TypeScript
7 is wanted; what blocks it is upstream — typescript-eslint's typescript
peer range, whose upper bound has never exceeded <6.1.0 across all 1,510
published versions (re-verified today from the full packument, not from
`latest` alone). The regenerating Dependabot PR proposing typescript 7.x
is the notification that tells us when upstream ships support, so an
ignore rule would suppress the signal while changing nothing about the
blocker. Dependabot's own automated reply on the PR closed today
recommended exactly that ignore; it is correct on mechanics and wrong as
advice here.

Like every prior change to this file, it is inert on main until the next
dev->main promotion — Dependabot reads its configuration, ignore list
included, from the default branch. main's copy has never carried the
@types/node stanza (added on dev by #147, never promoted), so this
removal deletes a rule that was never live and brings the file closer to
the effective configuration rather than further from it.

No other key in dependabot.yml changed: the six ecosystems, every
target-branch, schedule, groups and commit-message block, and the five
remaining npm ignores enforcing locked decision §2 are untouched.
…a current reason (#193)

Comment-only change to the npm /frontend entry. No ignore rule, group,
schedule, target-branch or any other key is added, removed or modified.

The paragraph's instruction — leave the frontend tooling majors unignored
so Dependabot keeps surfacing them — is still correct. Its stated reason
was not: it called them "the deferred #86 sweep" and said to surface them
"until that PR is done". The sweep completed 2026-08-09 across #171,
#174, #177, #179, #180, #183, #185 and #187, and docs/ROADMAP.md marks
the item Done (verified before writing). So the comment had begun
arguing against its own instruction — a reader who checked the roadmap
would find the work finished and reasonably conclude the exemption had
expired, whose obvious next move is to add the typescript ignore, which
is the one thing that must not happen.

Rewritten so the instruction rests on two current reasons. The tooling
majors stay unignored as ordinary policy: we want to see them, evaluate
them and land them, which is exactly what the sweep did (pointing at
docs/upgrades/frontend-toolchain-86.md rather than restating it).
typescript stays unignored for a sharper reason: TypeScript 7 is wanted,
the blocker is entirely upstream, and the regenerating Dependabot PR
proposing 7.x IS the notification that tells us when typescript-eslint
ships support — an ignore would suppress the signal while changing
nothing about the blocker. The re-check command is inlined so the next
reader can test it without a session, along with the expected shape of
support (a new typescript-eslint MAJOR built against TS 7's ./unstable/*
API, not a point-release range widen).

Verified by parsing rather than by reading: both versions load with
yaml.safe_load to byte-identical canonical JSON (SHA-256 07e71c6c on each
side), and every changed raw line matches ^[+-]\s*#.
…ivers kept, pending Grype-DB catch-up (#98, #116) (#195)

* test(ci): lift the Group A-1/A-2 interpreter waivers to capture a pre-bump baseline

Deliberately temporary, and expected to turn the Grype gate RED. There is no
unit test for a base-image digest pin, so the fail-first evidence for the
Python 3.14.7 bump has to come from the dogfood scan itself: this commit
removes the six `- vulnerability:` entries that Group A-1 (#98) and Group A-2
(#116) waive, leaving the runtime still pinned to the 3.14.6 digest, so CI
records exactly which findings those waivers were suppressing and which of
them trip `--fail-on high`.

Three of the six are HIGH (CVE-2026-15308, CVE-2026-11940, CVE-2026-11972), so
the Grype gate fails on this commit by design. The next commit bumps both
`FROM` digests to the 3.14.7 image and re-runs the same scan; the two runs are
the before/after pair.

Group B (CVE-2025-15367, poplib, issue #52) is untouched: its fix is still
`main`-only — verified again at the v3.14.7 tag, where Lib/poplib.py is
byte-identical to v3.14.6 — so no 3.14.x point release clears it.

* fix(docker): bump the runtime base image to Python 3.14.7 (#98, #116)

Both `FROM python:3.14-slim-bookworm@…` lines — the backend-builder stage and
the runtime stage — move together from
sha256:86f975aca15cf04a40b399eebede9aea7c82eae084d1f1a0a6ef6bcaae871a30 (3.14.6)
to
sha256:23c59390fc717bf09f9336908199a0ae75d9c4264bf296123f94ad772fea3b52 (3.14.7).

Nothing else changes in this commit: the Group A-1/A-2 waivers stay lifted from
the previous commit, so the dogfood scan re-runs with the digest as the only
variable against that baseline.

How the digest was established, without trusting a rendered page:
  - `HEAD /v2/library/python/manifests/3.14-slim-bookworm` on
    registry-1.docker.io returns docker-content-digest sha256:23c59390…, and the
    same request for the `3.14.7-slim-bookworm` tag returns the identical digest
    (`3.14.8-slim-bookworm` is 404 — 3.14.7 is the current 3.14.x).
  - That index is an OCI image index carrying both linux/amd64 and linux/arm64
    children, which publish.yml's multi-arch build needs.
  - Its per-arch annotations name the build source
    docker-library/python@228f71e:3.14/slim-bookworm,
    created 2026-08-05; that Dockerfile declares `ENV PYTHON_VERSION 3.14.7`.
    The same lookup on the outgoing digest resolves to rev 7914d06 with
    `ENV PYTHON_VERSION 3.14.6`, confirming what is being replaced.

The six fixes were verified in CPython itself, at the v3.14.7 tag versus
v3.14.6, per CLAUDE.md § Dependency hygiene — not from Grype's FIXED IN column,
which reports 3.15.x for all six and does not record the 3.14 backports. The
per-file, per-CVE evidence is in docs/ARCHIVE.md §14 (2026-08-11).

* fix(docker): strip pip from the runtime image, closing two vendored-dependency HIGHs

The 3.14.7 base-image bump turned the Trivy dogfood gate red with two fixable
HIGHs that are not Scrye dependencies:

  msgpack     GHSA-6v7p-g79w-8964  HIGH  1.1.2   -> 1.2.1
  setuptools  CVE-2025-47273       HIGH  70.3.0  -> 78.1.1

Both versions are exactly pip's vendored pins (`pip/_vendor/vendor.txt`:
`msgpack==1.1.2`, `setuptools==70.3.0`). `backend/requirements.lock` pins
setuptools 83.0.0 and carries no msgpack at all, so neither is ours to bump —
they are whatever the digest-pinned base image's bundled pip vendors. 3.14.6
bundled pip 26.1.2 and 3.14.7 bundles 26.2.1 (`Lib/ensurepip/__init__.py`
`_PIP_VERSION`), which is what surfaced them.

Rather than excuse them in ci/trivyignore, the runtime image stops shipping pip.
Nothing there needs it: the entrypoint runs `alembic upgrade head` then
`exec uvicorn`, and no application code imports pip, ensurepip, or
pkg_resources. `backend-builder` keeps pip — it installs the hash-pinned lock
with `pip --require-hashes` (SC-1) — so this is a runtime-stage change only.

Both prefixes are cleaned (/opt/venv, seeded by `python -m venv`, and
/usr/local, from the base image's `--with-ensurepip` build), along with
`ensurepip` itself, whose entire payload is that same pip wheel. The step
asserts `pip` is actually off PATH and that the venv still imports alembic,
fastapi, sqlalchemy and uvicorn, so a version glob that stops matching after a
future base bump fails the build instead of silently shipping pip again.

Guarded by a new static regression test in
backend/tests/test_dockerfile_supply_chain.py, verified to fail against the
pre-strip Dockerfile (1 failed, 7 passed) and pass after (8 passed).

No stage boundary, layer ordering, or build-cache scope changed — this only
removes files from the final stage (docs/ARCHIVE.md § Build performance
§ Invariants).

* docs: record the 3.14.7 outcome — fixes verified at the source, waivers kept

The measurement the branch existed to produce: after the digest bump, with the
Group A-1/A-2 waivers lifted, Grype reported the same six CVEs it reported on
3.14.6, now against `python 3.14.7`, and the gate exited 2 (CI run 31457878086).
So no waiver is retired. That is the Grype-DB lag #98 and #116 each predicted in
as many words, and the `FIXED IN 3.15.0*` column is the mechanism: Grype's data
records these as fixed only in 3.15.x, has no entry for the 3.14 backports, and
compares 3.14.7 < 3.15.0b4.

What establishes the fix is the source diff, not the scanner. All six were read
at the `v3.14.7` tag and diffed against `v3.14.6` — the second verification for
each, and the first against a *released* tag rather than the `3.14` branch, which
could only ever show a fix was queued.

ci/grype.yaml therefore restores all seven waivers unchanged in membership and
inverts their stated reason: from "unfixable at the pinned version, waiting on a
release" to "fixed in the pinned interpreter, verified at the source, waived
because the scanner's data lags." The new trigger is a Grype-DB refresh, at which
point both Group A blocks are deleted outright rather than re-dated; the shared
2026-10-25 review date is kept deliberately, since a daily-cadence DB refresh is
not a release months away. The observed scan output is quoted in the file so the
next reader doesn't have to reconstruct why a waiver outlived its own trigger.
Group B (CVE-2025-15367, poplib) is byte-identical to dev — `Lib/poplib.py` is
byte-identical between the two tags, so 3.14.7 clears nothing there.

Also here:
- CLAUDE.md § Locked decisions #2: runtime floor 3.14.6 -> 3.14.7, keeping the
  incremental-GC rationale and adding the interpreter-CVE one alongside it. The
  two bind independently — the GC reason floors at 3.14.5, the CVE reason raises
  to 3.14.7 — so neither replaced the other, and a future reader resolving the
  CVEs cannot conclude the floor may drop back and reintroduce the GC leak.
- CONTRIBUTING.md's prerequisite moves with it.
- CHANGELOG [Unreleased] § Security: the bump, why the waivers stay anyway, and
  the pip strip.
- docs/ARCHIVE.md: two dated §14 entries — one for the CVE/scan outcome, one for
  the locked-decision floor change, deliberately separate so the floor doesn't
  read as a consequence of a scan whose result was "all six still report" — plus
  their index lines. The index header count was stale at 157 against 158 entries
  before this change; it now reads 160 and matches.

Both entries state the environment limitation explicitly: the image build, the
dogfood scans and the interpreter version come from CI, which pulls and builds
the real image (the `INSTALLED 3.14.7` column is the built image reporting
itself), because this sandbox's egress policy denies the registry blob hosts and
python.org. The backend suite has not run on 3.14.7 anywhere — CI's
`Set up Python 3.14` resolves to 3.14.6 — and that is recorded as an open gap,
not claimed as done.

The minor-version pins in ci.yml and pyproject.toml are untouched, as are the
historical 3.14.6 references in released CHANGELOG sections, CLAUDE.md's
source-verification rule, and the AboutPanel mock payload.
…and #116 (#196)

* docs(ci): make the Group A waiver blocks stand alone after closing #98/#116

Both issues tracked deferrals whose resolution trigger has fired: 3.14.7
ships all six fixes, verified at the source against the released tag in
PR #195. What remains behind the waivers is Grype-DB lag, not an open
risk acceptance, so tracking moves out of GitHub issues for this pair.

ci/grype.yaml: replace the forward-looking "Tracked in issue #NN"
pointers in the A-1 and A-2 blocks (and in the block->issue index) with
the verified-fixed / waived-pending-DB-refresh statement, cited to
PR #195, tracked by this file plus docs/ARCHIVE.md §14. Move the shared
review date 2026-10-25 -> 2026-11-01 and label it advisory: nothing in
the repo notices when the DB catches up, the waivers just go inert.
Historical #98/#116 references are kept.

Waiver membership, format, and gate behaviour are unchanged - the
ignore: list still parses to the same ten entries.

Adds one dated docs/ARCHIVE.md §14 entry recording the decision, and
updates the §14 index (160 -> 161).

* docs(ci): retarget the dogfood log's block->issue header too

The "Waived by ci/grype.yaml (informational)" step hardcodes its own
header line, so it printed "A-1 (issue #98) · A-2 (issue #116)" on every
run - the same stale forward pointer just outside the file being edited.

Bare echo string only: no jq filter, gate threshold, --exclude,
--fail-on, step condition, or job structure changed.
…rule (#197)

Two separate problems, two separate fixes.

The footer on PR bodies and issue comments is a Claude Code harness feature
configured through settings, not text a session composes. It was configured in
the maintainer's user-scope ~/.claude/settings.json, which has never applied
here: each Cloud session is a fresh VM with no home-directory provisioning, so
that file does not exist in the environment the work runs in. Add a committed,
project-scope .claude/settings.json instead — the repo is cloned into every VM,
and project scope outranks user scope. It sets attribution.commit "",
attribution.pr "", and attribution.sessionUrl false; the file parses and
validates with zero errors against the published settings schema.
includeGitInstructions is deliberately not used — it would strip all built-in
git/PR guidance from the system prompt and is unnecessary once the attribution
keys are set.

The strip-and-reverify instruction is removed from CLAUDE.md and CONTRIBUTING.md
alike. A strip PATCH runs under a bot identity: on PR #196 the edit history
shows the comment created by tyler-rich followed by two "claude (Bot)" edits —
the two strip attempts — with the footer re-appended anyway. It is the only
write in this workflow that lands under a non-maintainer identity, it has never
succeeded, and it leaves a permanent, public attribution leak worse than the
footer it targets. The reason is stated inline in the rule so a future session
cannot read the prohibition as an oversight to repair.

Also: restate the repo-local git identity step as a hard gate (the VM image
presets both repo-local and global identity to Claude <noreply@anthropic.com>,
and settings.json has no git-identity key, so those two commands are the only
thing producing correct authorship); ignore .claude/settings.local.json by
specific path, not a blanket .claude/ rule; and exclude .claude/ from the
denylist-style .dockerignore so it never enters the build context this project
dogfoods a scan against.

See docs/ARCHIVE.md § Deviations for the full record.
…skill (#198)

The skill is outside every repository, so no commit can reach it and it will lag
until the maintainer syncs it by hand. Say plainly in the §14 entry that
CLAUDE.md and CONTRIBUTING.md govern where the two disagree on attribution, and
that the skill is known-stale on the subject — on the strip instruction and on
the declined profile-display-name point alike. A session weighing two sources
that disagree and picking the wrong one is the #169 failure mode; writing the
precedence down is what stops it being rediscovered.
)

Issue #52's source-verification table named v3.14.6 as the pinned
runtime, which went stale when the runtime moved to 3.14.7 in PR #195.
Re-verified independently at the source (Lib/poplib.py at v3.14.7 vs
main): the POP3._putcmd() control-character guard is still absent from
v3.14.7 and present only on main. No backport has appeared on gh-143923
since 2026-07-26. The acceptance is unchanged.

- ci/grype.yaml: Group B block now cites v3.14.7 and records the
  2026-08-11 re-check.
- #52: verification table, date, and its cross-references to the now
  closed #98/#116 updated to point at their current tracking location
  (ci/grype.yaml + docs/ARCHIVE.md §14) instead of the closed issues.
- docs/ARCHIVE.md §14: dated entry recording the re-verification
  method, result, and cross-reference retargeting.

Not changed: the waiver itself, #52's review date/cadence, the Group
A-1/A-2 blocks and their review date, or anything scoping a 3.15 move.
See docs/ARCHIVE.md §14 (2026-08-11) for the full record.
)

- .claude/settings.json: add includeGitInstructions: false. Attribution
  block is untouched. Motivated by #199, where the attribution keys
  were confirmed live but the PR-creation path still appended a footer
  while a direct issue-body edit in the same session came back clean —
  the append is specific to the built-in PR workflow instructions this
  setting controls, not attribution.pr. Second attempt at the same
  problem, not a replacement for the attribution block.

- CLAUDE.md: add a standing rule that a PR body describes the change,
  not the session's own process — no authorship-check narration,
  footer-status commentary, merge-permission statements, or
  scope/stop-condition meta-commentary. That content belongs in the
  session's chat summary instead.

See docs/ARCHIVE.md §14 (2026-08-11) for the full record, including
the accepted tradeoff (CLAUDE.md becomes the sole source of git
conventions for sessions running with this setting) and confirmation
that the settings file still validates against the published schema.

Not changed: the never-PATCH rule, the attribution block, #199's
branch, or CONTRIBUTING.md.
…'s six sites (#201)

Refactors findings 1, 2, 3 and 5 from #176 so none of them calls setState
synchronously from an effect body. Behaviour is preserved at every site.

- LoginPage: the oidc_error message is seeded by a lazy useState initializer;
  the effect keeps only the history.replaceState that strips the parameter.
- OidcLinkCard: same treatment for the oidc_link / oidc_link_error banners,
  over a shared linkParams() helper.
- NewScanPage: the scanner clamp moves out of an effect into the target-type
  handler, the one place that can invalidate the pairing. Deliberately not a
  derive-during-render, which would resurrect a displaced scanner choice
  instead of overwriting it.
- ScanDetailPage: findingsLoading is derived from a settled-request key rather
  than stored, which also removes a flash of "No findings match the current
  filters" over a request still in flight. The per-scanId reset effect keeps
  its shape; its one uncompilable line becomes setFindingsSettledKey(null).

Each site gains a test verified to fail against the pre-refactor version of its
own file. The react-hooks/set-state-in-effect override in eslint.config.js is
unchanged: findings 4 and 6 and the rule flip are a follow-up, per #176.

See docs/ARCHIVE.md § Deviations for the full account, including why
ScanDetailPage's void loadFindings() still reports after the refactor.
…-attempt cap (#202)

* docs: delete .claude/settings.json; reinstate footer strip with a one-attempt cap

The committed project-scope settings file is removed in full — both the
attribution block from #197 and includeGitInstructions from #200. Three
sequential settings-layer attempts did not stop the attribution footer:
#197 added the attribution keys, #200 added includeGitInstructions after
#199 still showed a footer, and #201 ran with both merged and live and
still got one on its PR body. The approach is abandoned, not paused.

With no settings-level backstop left, the strip instruction returns to
CLAUDE.md § Attribution and CONTRIBUTING.md § Pull request process — but
capped at one PATCH per body, with a hard stop if the footer comes back.
The cap prices #197's evidence rather than disputing it: each PATCH runs
under a bot identity and leaves a permanent "claude (Bot)" entry in
GitHub's public edit history, and #196 showed a second attempt only
doubles that cost without changing the outcome.

Commit-authorship verification is unchanged. The .gitignore comment that
explained why the settings file was tracked is rewritten, and docs/ARCHIVE.md
§14 records the deletion, the cap, and the fact that this is the fourth
reversal of this policy.

* docs(archive): record the strip's first outcome on #202's own body

The §14 entry documented the policy change but not its result, since it
was written before the PR was opened. Adds the outcome: the strip held on
this PR's own body — footer found on creation, one PATCH, clean confirming
re-read — which is the first success in this project's history.

Bounds it on both sides rather than claiming more than it shows. The
footer appeared on creation with #197's and #200's keys live and merged,
the same result #201 got, so the settings demonstrably did not stop the
append; whatever they may have contributed is confined to the re-append
after the PATCH, and that is now an open and unfalsifiable question since
the file is gone. The next PR is the harder test, running with no settings
file at all. Also notes that #196's failures were on an issue comment
while this success was on a PR body, so it does not refute #196.
…t-state-in-effect at 'warn' (#203)

* refactor(frontend): replace #176 findings 4 and 6 with behaviour-preserving equivalents

Finding 4 (L17/P2-2): ScanDetailPage's per-:scanId reset effect is replaced by
a keyed remount — a ScanDetailRoute wrapper keys the page by the scan id, so
React Router's element reuse across /scans/:id navigations can no longer leak
the previous scan's state. The scanIdReset regression test now routes through
the same wrapper the app uses.

Finding 6 (P3-2): ScansPage's compare-selection reconcile effect moves into
load(), the only place the visible rows ever change, so the selection is
reconciled in the same commit as the rows instead of one commit later.

The react-hooks/set-state-in-effect override in eslint.config.js is
deliberately untouched pending a maintainer decision on the remaining
fetch-on-mount findings.

* refactor(frontend): enable react-hooks/set-state-in-effect at 'warn'; strengthen the P3-2 deleted-scan test

With #176's six genuine sites all refactored, the override moves from 'off' to
'warn' (maintainer decision): the rule takes effect, its 13 remaining reports —
all the fetch-on-mount idiom, which #176 puts out of scope with no honest fix —
stay visible in lint output without failing lint or CI, and the config comment
records why 'warn' was chosen over per-site disables.

The P3-2 deleted-scan regression test gained the two assertions that actually
discriminate ("Comparing 1/2 selected", Compare disabled): fail-first
verification showed its existing assertions all held vacuously against the
unprotected version. It now fails without the reconciliation and passes with it.

docs/ARCHIVE.md §14 (2026-08-11, "#176 part 2") carries the full record;
docs/upgrades/frontend-toolchain-86.md step 3 now marks its held-back rule
enabled.

* docs(archive): record #203 as the second consecutive PR-body strip success, the first with no settings file

The strip-cap entry predicted the first post-deletion PR would be the harder
test of the reinstated rule; #203 was that PR and its strip held. Two-for-two
on PR bodies since the reinstatement — #202 with both key sets live, #203 with
no settings layer at all — so the settings keys are not needed for a hold.
Issue comments remain untested either way.
Runs the CONTRIBUTING.md § Releasing "Before you tag" checklist on dev,
ahead of the promotion PR.

- Version 0.3.0 -> 0.3.1 in sixteen places across nine files
  (frontend/package-lock.json written with `npm version`, never by hand).
- CHANGELOG [Unreleased] cut to [0.3.1] - 2026-08-11, with a fresh empty
  [Unreleased] above it and the compare links added and re-pointed.
- Adds a ### Fixed section for the #176 work landed in #201/#203, removes
  the test-only ### Added entry (§14 already carries it in full), and
  corrects the step-3 set-state-in-effect claims that #203 superseded.
- THIRD_PARTY_LICENSES re-verified byte-for-byte against upstream at the
  pinned Trivy/Grype/Syft tags; backend/requirements.lock confirmed
  drift-free; the Dependabot queue is empty.

Issue #194 is a stale published image rather than a defect: its only
gate finding is CVE-2026-69247 (cryptography 49.0.0), already closed on
dev at 50.0.0 and verified in upstream's source.

See docs/ARCHIVE.md § Deviations for the full record.
…ts (#206)

README § Requirements named only "Python 3.14", so it did not carry the
patch-level floor that CONTRIBUTING.md § Prerequisites and CLAUDE.md
§ Locked decisions both set at 3.14.7. Anyone reading README alone for the
native-development prerequisites could land on an interpreter below the
floor. README now states the floor; CONTRIBUTING keeps the reasoning.

The Node entry is unchanged — it already matched.
@tyler-rich
tyler-rich merged commit baaad86 into main Aug 14, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant