test: prepare phase 9 requalification harness and blocker register
MariaDB persistence qualification / mariadb (push) Failing after 7s

This commit is contained in:
cesnimda
2026-10-24 13:20:47 +02:00
parent 780b9bc64f
commit 1a1b7c8221
29 changed files with 7637 additions and 4 deletions
@@ -29,7 +29,13 @@ This establishes source capability only. The installed binary has not been match
## Bounded workflow change
Retain the existing push branch and add `fix/p9-ci-and-requalification`. Add an unconditional failing isolation preflight before all workflow Docker/Compose/database commands. It reports this handoff and requires a later reviewed workflow change after operator evidence; no self-attested repository variable bypass exists. Existing checkout, SDK, five guard tests, integration command and artifact action remain. No services, socket mount, privileges, Docker CLI installation, deployment workflow or runner configuration was added.
Retain the existing push branch and add `fix/p9-ci-and-requalification`. Add an unconditional failing isolation preflight as the first step, before checkout, SDK setup, repository scripts and all workflow Docker/Compose/database commands. It reports this handoff and requires a later reviewed workflow change after operator evidence; no self-attested repository variable bypass exists. Existing checkout, SDK, five guard tests, integration command and artifact action remain downstream. No services, socket mount, privileges, Docker CLI installation, deployment workflow or runner configuration was added.
### Fresh hosted blocker verification
Owner-pushed commit `75cd04cfc2da0837b556bdcf1424846349c4c73d` triggered [run 847](https://git.cesnimda.uk/cesnimda/jobtrackingapp/actions/runs/847), job 1737, on runner 5 `Live-Runner`. The raw log records the first isolation step emitting explicit `BLOCKED` at **2026-10-24 06:56:14 UTC**, then exit 1. Checkout and SDK main steps were skipped; no repository guard/integration script or workflow Docker/Compose command executed. The artifact action found no integration files. No MariaDB service was declared or started. This verifies fail-closed workflow behavior only; hosted MariaDB qualification remains BLOCKED.
Runner preparation still creates its job container with the existing host network/socket, pulls the job image and prefetches action repositories before the first step. The preflight cannot prevent runner-owned preparation. Its container and generated workspace volumes were removed afterward; this does not establish private database service lifecycle behavior. The job API again labels all steps failure, so raw logs distinguish the actual blocker from skipped downstream steps. Read-only follow-up refreshed `.artifacts/mariadb/requal-ci-api.json`; the JSON report carries its new hash and separate run-847 API/log hashes.
Do not remove the blocker merely because Docker becomes available. Under the observed configuration that could grant the job control of an unidentified host daemon. The desired future native service must use `mariadb:12.2.2@sha256:42a27baaba3e8f3f0159d16381d5a7156661aa91dcc65f663cabb6a6f9503c00` on the already documented linux/amd64 platform, no published service ports, private job networking and verified disposable storage. No version upgrade is proposed.
@@ -1,6 +1,6 @@
# Hosted MariaDB lane: operator handoff
Status: **BLOCKED**. The workflow intentionally exits before Docker/Compose/database execution. No repository-variable toggle can enable it. The existing runner already injects a host Docker socket and host networking; this task has not changed or used those facilities.
Status: **BLOCKED**. The workflow intentionally exits at its first step before checkout, SDK setup, repository scripts and workflow Docker/Compose/database execution. Run 847 at `75cd04cfc2da0837b556bdcf1424846349c4c73d` verifies the explicit BLOCKED/exit1 outcome. No repository-variable toggle can enable it. The existing runner already injects a host Docker socket and host networking; this task has not changed or used those facilities. Runner-owned job-container creation and action prefetch still occur before the first step.
The infrastructure owner must supply sanitized evidence for the following before a reviewed workflow change can enable native services:
@@ -0,0 +1,104 @@
# Reproducing the qualification lanes
Run from the repository root on the dedicated development branch. Use the pinned .NET9 SDK
and the existing frontend lock. Build Release API/tests and the production static export before
browser execution. Preserve each prior run directory; a successor is separate evidence, never a
hidden retry. The accepted result ledger records actual commands, timestamps, process outcomes,
native case identities and report hashes. Commands below describe the mechanism, not passing results.
All shell commands in this environment use `rtk proxy`. Windows examples use the existing native
MariaDB client under `.artifacts/mariadb/runtime/mariadb-12.2.2-winx64/bin/`; obtain and verify it
using the [database setup instructions](../../../database/mariadb-standardisation/README.md).
No command connects to production. Development credentials in fixtures are intentionally local.
## Existing full suites
- Backend: `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --logger "trx;LogFileName=backend.trx" --results-directory <new-output>/backend`.
- Real provider: `python scripts/mariadb-integration.py --output <new-output>/mariadb`.
- Frontend, from `job-tracker-ui`: `node node_modules/jest/bin/jest.js --runInBand --json --outputFile=<new-output>/frontend.json`.
- Types/lint/static build, from that directory: `node node_modules/typescript/bin/tsc --noEmit --incremental false`, `node node_modules/eslint/bin/eslint.js . --max-warnings=0`, `node --max-old-space-size=4096 node_modules/next/dist/bin/next build`.
- Sidecar, from `tools/summarizer`: set `SKIP_MODEL_LOAD=1` and `AI_SERVICE_SKIP_MODEL_LOAD=1`, then `.venv/Scripts/python.exe -m pytest -q tests --junitxml=<new-output>/sidecar.xml`. This does not qualify a model.
For ordinary browser discovery and execution, use the production static export with
`PLAYWRIGHT_STATIC_EXPORT=true`, `PLAYWRIGHT_PREBUILT_EXPORT=true`,
`PLAYWRIGHT_REUSE_SERVERS=false`, `NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api` and
`DOTNET_HOST_PATH=<pinned dotnet.exe>`. Direct fidelity outputs to fresh
`CV_FIDELITY_OUTPUT` and `CV_SETTINGS_FIDELITY_OUTPUT` directories. From `job-tracker-ui`, run
`node node_modules/@playwright/test/cli.js test --list --reporter=json`, then four **serial**
executions with `--shard=1/4` through `--shard=4/4`, each with `--retries=0 --reporter=list,json`,
a unique `--output` directory and `PLAYWRIGHT_JSON_OUTPUT_NAME`. Discovery-only empty results
are not runtime skips. Reconciliation requires exact discovery membership across all four shards.
Run each separate config serially with the same zero-retry/report settings:
| Lane | Config relative to `job-tracker-ui` |
| --- | --- |
| AI contract | `playwright.cv-ai.config.ts` |
| Auth recovery | `playwright.auth-recovery.config.ts` |
| Rate limits | `playwright.rate-limits.config.ts` |
| Ingress | `playwright.ingress.config.ts` |
| PDF admission | `playwright.pdf.config.ts` |
| Preferences | `../docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/playwright.config.ts` |
| RD21 | `../docs/redesign/phase-9/rd21-remediation/diagnostics/playwright.config.ts` |
| Corrected diagnostics | `../docs/redesign/phase-9/p9-05/diagnostics/playwright.config.ts` |
| Volume | `../docs/redesign/phase-9/p9-05/volume/playwright.config.ts` |
| RD21 volume | `../docs/redesign/phase-9/rd21-remediation/volume/playwright.config.ts` |
Ordinary and these historical contract fixtures retain transitional SQLite where their current
configuration specifies it. They are never counted as MariaDB qualification.
## Dedicated MariaDB browser and sustained workload
Use only the pinned disposable service from `docker-compose.database.yml`, project
`jobtracker-it-p9-requal`, integration profile, with `MARIADB_TEST_PORT=33308`. Start only
`mariadb-test` with `up -d --wait`. The persistent `mariadb-dev` service and its volume are separate.
The helper verifies the exact container, Compose ownership, image pin, tmpfs, loopback binding
and server version before creating its generated schema. It refuses to overwrite earlier evidence.
With ports5302/3300 free and no other qualification workload, run these separately:
```powershell
rtk proxy python docs/redesign/phase-9/p9-requal-01/mariadb-lane.py mariadb-browser
rtk proxy python docs/redesign/phase-9/p9-requal-01/mariadb-lane.py mariadb-observations
rtk proxy python docs/redesign/phase-9/p9-requal-01/mariadb-lane.py performance
```
The performance lane takes at least15minutes after setup/preflight. Do not shorten it or run
competing tests/builds. Read [PERFORMANCE.md](PERFORMANCE.md) for its bounded claims.
Each invocation drops only the exact schema it created, after stopping its own test process.
Failures retain logs and cleanup evidence; an uncertain process shutdown retains the schema.
After all lanes, remove only this owned disposable service using Compose `rm --stop --force mariadb-test`
with the same file/project/profile. Never use `down -v` or remove the development volume.
## Native and Linux PDF fidelity
Once both existing CV producer files have completed, use Python with the repository's existing
PDF tooling (`pdfplumber`, `pypdfium2`, Pillow):
```powershell
rtk proxy <pdf-python> -B docs/redesign/phase-9/p9-requal-01/pdf-proof.py --platform windows --fidelity-root .artifacts/p9-requal-01/fidelity --settings-root .artifacts/p9-requal-01/settings-fidelity --output .artifacts/p9-requal-01/windows-pdf-oracle --record .artifacts/p9-requal-01/native-pdf.json
```
The helper stages copies, merges all eight settings states, runs the **unchanged** existing oracle,
checks fixture/page membership and hashes original inputs before/after. It will not overwrite output.
The Linux lane builds the **existing** API Dockerfile locally with tag
`jobtracker-p9-requal-api:security-check`, while retaining the build log and source identity.
With all other browser/API lanes stopped and the same verified disposable MariaDB service running,
execute `rtk proxy python docs/redesign/phase-9/p9-requal-01/linux-run.py`. It starts a generated,
task-labelled API container with tmpfs data and a private network, publishes only loopback5302,
verifies API identity/readiness and actual migration history, and executes five unchanged CV
producer cases. It records renderer/runtime/image identities and removes only its own resources.
Then run `pdf-proof.py --platform linux` against `.artifacts/p9-requal-01/linux/fidelity` and
`linux/settings-fidelity`, using fresh `linux-pdf-oracle` output and `linux-pdf.json` record.
This qualifies the Linux API renderer with a Windows Chromium preview client; physical printing,
Linux desktop browser behavior and human visual acceptance remain distinct.
## Fail-closed evidence checks
`test-reconcile.py`, `test-linux-run.py` and `mariadb-lane.py self-check` are offline harness
checks, not provider tests. `reconcile.py` reads the fixed fresh artifact paths and writes
`validation.json`; its default process fails if required gates are missing or not passing.
`--collect-only` deliberately collects partial evidence and cannot establish acceptance.
Frozen runtime input hashes must still match. Advisory findings and unavailable human/provider/
production evidence retain their own dispositions even when functional tests pass.
@@ -0,0 +1,92 @@
# P9-REQUAL-01 findings and independent gate register
Source review dated 24 October 2026 on `fix/p9-ci-and-requalification`, baseline `549b2cba5a791862ba3f854312ea819a86dc42a0`. This register covers RD01–RD28, every retained P9 finding, and M09-01–10. [Machine-readable register](findings.json) contains source, evidence, impact, dependency, owner, status and required closure for each entry. Fresh execution is pending the root-owned validation ledger; every result referenced below is historical unless explicitly updated there.
The original [REQUEST §§6,13,18](../REQUEST.md) distinguishes completed assessment from acceptance and explicitly says “Carry uncompleted checks into the release gates.” It permits transparent human-check carry-forward; it does not invent human PASS results. Production-only storage/backup/deployment conditions do not automatically veto isolated functional QA. Each unmet functional criterion still needs a specific disposition. The [master cleanup criterion](../../IMPLEMENTATION_PLAN.md#phase-10--cleanup) requires proof that no route still uses removed code, tests pass and the functionality inventory is satisfied. Neither conditional Phase9 acceptance nor an open manual checklist satisfies these deletion criteria by itself. No Phase10 implementation is authorized.
The historical [Phase9 report](../p9-07/FINAL_ACCEPTANCE.md) and [handoff](../p9-07/PHASE10_HANDOFF.md) remain intact. The [RD21 addendum](../rd21-remediation/ACCEPTANCE_ADDENDUM_2026-10-23.md) repaired SQLite but withheld provider closure; the later [MariaDB acceptance](../../../database/mariadb-standardisation/ACCEPTANCE.md) supplies bounded real-provider closure. Thus the old RD21 stale-save FAIL and missing-MariaDB statement describe their original source, not the latest qualified save/concurrency state. [RD21 closure](RD21_CLOSURE.md) preserves separate creation-idempotency, mixed-version deployment and historical-recovery limits.
| ID | Current disposition | Phase9 implication | Phase10 implication | Production / debt | Owner and exact closure source |
|---|---|---|---|---|---|
| RD01 | OPEN; local static/loopback ingress contracts only | operator release gate | candidate-specific ingress compatibility proof | BLOCKED pending actual ingress identity; open operational qualification | Release operator: actual TLS/IPv6/firewall/topology/trusted peer and deployed auth evidence |
| RD02 | NOT TESTED: independently verified off-disk complete backup/restore | no independent veto of isolated functional QA | candidate restore dependency | BLOCKED backup/restore; open operational qualification | Release operator: isolated restore of DB/files/CVs/keys/tombstones, hashes and recovery rehearsal |
| RD03 | MariaDB 12.2.2 RD21 runtime scope verified historically (32 provider cases, 1 native journey); broader provider/deployment parity still PARTIAL | bounded provider save qualification historical PASS; full fresh matrix pending | provider migration proof per candidate | BLOCKED exact deployed parity/migrations; partial broad provider qualification | Database owner: actual approved MariaDB migration, restart, CRUD/constraints/concurrency and effective configuration proof |
| RD04 | NOT TESTED: intended Linux rollback | no independent veto of isolated functional QA | rollback proof per candidate | BLOCKED actual rollback; open operational qualification | Release operator: isolated failure/rollback rehearsal using complete prior images/configuration/recovery set |
| RD05 | Hosted database CI BLOCKED: merged host socket binding found; private isolation/lifecycle unproven; deployed identity remains separate | fresh required CI pending root evidence | testpass criterion pending root evidence | BLOCKED deployed identity and operator diagnosis; open CI/release qualification | Infrastructure owner: native core/operator diagnosis, successful protected CI, exact deployed SHA and authenticated smoke |
| RD06 | PARTIAL: finite axe/DOM/keyboard evidence; human checks open | manual carry-forward allowed only as explicit release qualification (REQUEST §6), no human PASS | affected functionality must remain satisfied; no deletion permission | BLOCKED applicable human accessibility; open human qualification | Accessibility tester: [M09-01–06](../p9-03/MANUAL_CHECKLIST.md), contextual contrast/reader evidence; triage every applicable incomplete |
| RD07 | NOT TESTED: physical input/AT/alternate browsers/forced colors | manual carry-forward release qualification; no physical/AT PASS | affected functionality proof still required | BLOCKED supported device/browser/input acceptance; open human qualification | Product/accessibility owners: actual supported devices/browser/input versions and complete workflows |
| RD08 | DEFERRED; no new model qualification or activation | C11 explicitly withheld; separate scope | no activation/deletion eligibility | BLOCKED AI activation unless separately qualified; DEFERRED product/model scope | Separate AI qualification owner: production-equivalent identity/inference, original 366-call protocol, independent human semantics, review UI/integration evidence |
| RD09 | Local withheld-operation contract retained; deployed withholding NOT TESTED | local withholding contract needs fresh successor | retain withholding and callers | BLOCKED deployed withholding untested; open deployment qualification | Release/AI owners: keep 503/no-provider/no-write guards, verify actual deployment independently |
| RD10 | PARTIAL: native local PDF corpus and visual sheets; deployed fonts/photo/print absent | finite output qualification; human/Linux/print not passed | affected rendering must preserve output contracts | BLOCKED intended output environment; open output qualification | Document owner: intended Linux/fonts/photo/crop/print and alternate-browser proof, original bytes/geometry |
| RD11 | Scoped SQLite bootstrap/WAL mechanism repaired before entry; broad reliability remains PARTIAL | scoped WAL repair retained; broad reliability qualification open | retain provider/concurrency safeguards | provider contention remains qualified; retained broader reliability | Database owner: retain held-reader/write proof, distinguish old reproduced mechanism from unproven provider/contention cases |
| RD12 | PARTIAL: notification draft/failure recovery and SQLite tests; complete contention/provider behavior unqualified | no newly established failure; provider overlap remains qualification | candidate must preserve drafts and recovery | intended-provider qualification open; retained reliability | Database/settings owners: intended-provider overlapping writes and failure/reload recovery with no data loss |
| RD13 | Historical Rules 500 mechanism remains UNCONFIRMED | historical causal mechanism UNCONFIRMED; not relabelled resolved | candidate must preserve Rules recovery | no complete reliability claim; unconfirmed historical observation | Rules/API owners: original failure trace plus controlled causal reproduction and successor regression; passing retry alone insufficient |
| RD14 | Historical command/dialog focus incident remains UNCONFIRMED; settled drawer proof is a separate finite result | historical causal mechanism UNCONFIRMED; human focus qualification | candidate must preserve trap/return/error states | applicable human checks open; unconfirmed historical observation | Shared UI owner: preserve original observations, reproduce affected modal transitions and actual reader/trap/return behavior |
| RD15 | Historical intermittent CV section expansion cause unproven | historical cause UNCONFIRMED; no invented current defect | candidate must preserve expansion state | broader reliability qualification; unconfirmed historical observation | CV owner: retain failed observation and reproduce causally before closing; do not weaken expansion assertions |
| RD16 | Scoped company-error repair retained; wider reader/dialog/date contract PARTIAL | scoped repair historical PASS; broader human contract open | candidate-specific recovery proof | human/provider qualification open; partial preserved contract | Company/UI owners: native failed-save → retained draft → successful retry plus human dialogue checks |
| RD17 | Board remains O(N); 200/1,000-job and new bounded 250-job observations are finite | finite capacity evidence; fresh sustained lane pending | measured bottleneck and full membership before optimization | no production capacity/SLA claim; O(N) known design limit | Jobs owner: agreed budget, bounded sustained profile and CPU/frame attribution before optimization; preserve full membership |
| RD18 | NOT TESTED: physical drag/drop/cancel/browser variants | manual carry-forward release qualification; physical behavior not passed | retain accessible reorder alternatives | BLOCKED physical input variants; open device qualification | Input/device tester: real Board/CV selection and named alternatives, no hidden result cap |
| RD19 | DEFERRED: reviewed `followMode` consumer | accepted deferred reviewed consumer; no activation | ineligible to remove metadata/paths | separate activation gate; DEFERRED product scope | Product/mail owner: separate explicit mode→review/action/fallback design; no auto-send or uncertain resend |
| RD20 | Optional P5-05 quick preview DEFERRED; Phase 8 Dashboard scope was delivered separately | optional preview DEFERRED; full workspace retained | no deletion of full workspace/aliases | no independent veto while optional scope remains deferred; DEFERRED optional scope | Product owner: explicit separate brief; retain existing full workspace and all routes |
| RD21 | RESOLVED within qualified MariaDB 12.2.2 save/concurrency scope; current branch successor qualification pending root ledger; creation-intent idempotency remains separate OPEN debt | bounded save/concurrency resolved historically; fresh full successor pending; creation idempotency separate | no compatibility removal without all-caller/rollback proof | BLOCKED deployment/rollback parity; no historical recovery claim; creation-intent idempotency OPEN, separate concern | Domain/API/UI owners: separate bounded contract issue covering both callers, server compare-and-swap/transaction semantics, conflict recovery and backward compatibility; characterization is not safety acceptance |
| RD22 | NOT TESTED: enabled-worker outage recovery/durability | isolated fixtures do not accept enabled producer | retain stable IDs and recovery paths | BLOCKED enabled-worker recovery; open operational qualification | Worker owner: isolated enabled-producer failure/restart/recovery, no-loss/no-duplicate proof before activation |
| RD23 | OPEN: actual calendar offset/DST/date-only semantics | actual offset/DST/date-only unqualified | retain calendar semantics until dedicated proof | BLOCKED calendar activation qualification; open provider semantics | Calendar owner: dedicated boundary/DST/provider proof in authorised sandbox before semantic changes |
| RD24 | NOT TESTED: billing/deletion/retention/provider cleanup policy and configuration | policy and destructive lifecycle outside functional proof | no removal without approved lifecycle policy | BLOCKED policy/restore/provider cleanup; open policy qualification | Product/security/operator owners: approved policy, explicit destructive-action authority, isolated recovery/cleanup proof |
| RD25 | PARTIAL: local SMTP/HTTP/provider contracts; actual OAuth/Turnstile/mail/calendar/model unavailable | sandbox/provider qualifications remain; fixtures do not pass them | candidate must preserve auth/delivery contracts | BLOCKED applicable real provider evidence; open provider qualification | Approved provider/human tester: sandbox consent/revoke/reconnect/origin/state/delivery/uncertain-attempt proof |
| RD26 | Legacy Job/Application cutover OPEN | no legacy cutover authorized | BLOCKED deletion until expand/verify/contract and all readers/writers | BLOCKED unqualified cutover; open migration scope | Migration owner: expand/verify/contract, restore/rollback and all readers/writers/aliases before removal |
| RD27 | NOT TESTED: actual monitoring/alert/recovery coverage | no independent veto of isolated functional QA | candidate observability proof where affected | BLOCKED actual alert/recovery; open operational qualification | Operations owner: effective sink, alert trigger, responder/runbook and recovery observations |
| RD28 | Ongoing evidence/provenance obligation | fresh exact evidence and source binding required | required exact candidate evidence | required exact deployment evidence; ongoing obligation | QA owner: exact source hashes/counts, original failures, no retries/skips/assertion relaxation; separate characterization from safe behavior |
| P9-OPS-01 | Previously reported EXT4/Docker storage errors UNRECHECKED | not a standalone functional defect; retain stated qualification | no permission to delete; affected-candidate proof | BLOCKED until stated operator closure; retained gate | Production operator only: independent storage-health/repair and recovery qualification under separate authority |
| P9-SEC-01 | Inherited advisory/review gate retained. Fresh live audit observations linked as P9-REQUAL-SEC01–03; compatible local pins and post-pin audits zero, full final-source successors pending. No production deployment or app-reachable exploit claimed. | local dependency fixes and post-pin zero audits recorded; complete frozen successors pending | no permission to delete; affected-candidate proof | BLOCKED independent deployed versions, exposure and release gates; no production fix claimed; inherited live-advisory gate retained; three new observations independently tracked | Dependency owner: authorised live advisory refresh, actual locked versions/paths and pre-expiry exception review; offline fixed-date policy tests do not clear current advisories. No environment bypass |
| P9-PUB-01 | Initial HTTP502 blockage recovered on23October; all checkpoint ancestors published in normal push; direct feature equality verified at `0fb108bd673ea91749f6fa057295e5b6d15d557f` | not a standalone functional defect; retain stated qualification | no permission to delete; affected-candidate proof | BLOCKED until stated operator closure; retained gate | QA: retain failed attempts and verify each subsequent normal push; no force push |
| P9-T01 | RESOLVED within historical tested scope; fresh successor pending | fresh affected regression required | candidate proof only; no cleanup authority | does not clear independent release gates; Resolved in tested Windows environment; Linux run remains separate. | QA; `.artifacts/phase9/sidecar-entry-failed.*`, `sidecar-fixture-red.*`, `sidecar-fixture-green.*`, `sidecar.xml` |
| P9-L01 | RESOLVED within historical tested scope; fresh successor pending | fresh affected regression required | candidate proof only; no cleanup authority | does not clear independent release gates; Resolved in observed local integration; no authorisation, pagination or data contract change. | Frontend; `admin-users-page.test.tsx`, `AdminUsersPage.tsx`; retained RED/GREEN logs/JSON |
| P9-V01 | RESOLVED within historical tested scope; fresh successor pending | fresh affected regression required | candidate proof only; no cleanup authority | does not clear independent release gates; Bounded fix verified by native geometry/build; the complete final 362-case ordinary successor also passed. | Frontend; `LandingPage.tsx`, new `landing-presentation.spec.ts`; `landing-red`, `landing-qualified`, `landing-accepted` raw reports |
| P9-V02 | RESOLVED within historical tested scope; fresh successor pending | fresh affected regression required | candidate proof only; no cleanup authority | does not clear independent release gates; bounded CTA repair separate from P9-V01 four-h2 repair | Frontend/QA: preserve existing LandingPage lineHeight1.4 and exact CTA+heading geometry assertions; qualify fresh complete landing and ordinary successors on frozen source, retain original RED/GREEN evidence and native-width images; original source: [FINAL_ACCEPTANCE.md](../p9-07/FINAL_ACCEPTANCE.md), line51. |
| P9-L02 | RETAINED low development debt | nonblocking development debt with visible limit | candidate proof only; no cleanup authority | does not clear independent release gates; Retained nonblocking development qualification; no data mutation proposed. | UI/localisation owner; separately map known event enums with unknown-value fallback and history/audit tests; human wording review |
| P9-T02 | RESOLVED within historical tested scope; fresh successor pending | fresh affected regression required | candidate proof only; no cleanup authority | does not clear independent release gates; Test-only repair; no product or authorisation change. | QA; `phase7-admin-users.spec.ts`; original shard report/trace and `admin-grid-green` retained. Other English-name callers belong to the separate English JobTable owner and remain unchanged. |
| P9-PERF-01 | RETAINED low development debt | nonblocking development debt with visible limit | candidate proof only; no cleanup authority | does not clear independent release gates; Accepted existing development debt; no Phase 10 implementation here. | Shared reminder read owner; measured benefit and independent shell/dashboard failure/loading tests before consolidation. |
| P9-T03 | RESOLVED historical fixture; fresh successor pending | fresh exact fixture/case successor | preserve isolation and assertions | not enabled-worker acceptance; no product fix inferred | QA: retain failed run, unchanged no-write assertions, fixture config delta and fresh full successor |
| M09-01 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | accessibility tester: Names, roles, values, selected state, headings, reading order and errors are understandable; retain reader/version and spoken observations. No hidden background content is announced. |
| M09-02 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | accessibility tester: Visible focus, logical order, genuine traps only while modal, Escape and focus restoration; no off-screen/hidden tab stops. Record any transition-specific failure. |
| M09-03 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | accessibility tester: Each action/position/save/error announces once and agrees with visible DOM order; pointer drag has an equivalent keyboard route. |
| M09-04 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | accessibility tester: Applicable normal text ≥4.5:1, large text ≥3:1 and necessary UI boundaries/focus ≥3:1. Record sampled colors/ratios/state. An axe incomplete is not sufficient to close the row. |
| M09-05 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | accessibility tester: Invisible zero badge is absent from announcements; live unread count remains meaningful; closed drawer is neither read nor tabbable. Runtime DOM checks cover only the tested states. |
| M09-06 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | accessibility tester: Essential controls remain reachable/readable, no lost content or focus; distinguish intentional table/document scrollers from root clipping. Preserve fixed A4/Letter geometry. |
| M09-07 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | product owner with physical devices: No keyboard-obscured primary action/data loss; reachable touch targets, no accidental destructive action; record device/input mode. Emulated touch does not close this. |
| M09-08 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | Bokmål reviewer: Natural professional Bokmål, consistent established terms, meaningful messages. Separate authored English content, technical names and provider strings from untranslated application copy. |
| M09-09 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | document reviewer: Legible fonts, intact glyphs/diacritics, margins, page boundaries, searchable text, photo/crop, correct language/date output and no missing/duplicated content. Retain original bytes, renderer/font versions and print observations. |
| M09-10 | NOT TESTED | explicit carry-forward qualification under REQUEST §6; no human result inferred | affected functionality proof required; no deletion authority | BLOCKED until applicable genuine human/provider evidence; open qualification | approved provider tester: Correlated origin/state, explicit confirmation, no accidental resend, correct ownership/timezone. Local SMTP/HTTP fixtures do not close external-provider qualification. |
## New requalification security findings
| ID | Current disposition | Phase9 implication | Phase10 implication | Production / debt | Owner and exact closure |
|---|---|---|---|---|---|
| P9-REQUAL-SEC01 | LOCALLY REMEDIATED; post-pin audits zero; complete final-source successor validation PENDING; production NOT DEPLOYED | bounded dependency remediation observed locally; full frozen successor qualification pending | testpass and functionality-inventory criteria still require final complete evidence; no cleanup authority | NOT DEPLOYED; no deployed fix or app-reachable exploit claim; independent release gates retained; published vulnerable package version repaired locally; remaining evidence qualification | Dependency/QA owner: Direct compatible package override System.Security.Cryptography.Pkcs8.0.2; retain MimeKit4.17.0 ownership. Retain before/after advisory paths, raw hashes and exact lock/pin delta; complete frontend/browser and required full successors, then bind all reports to final source. Production deployed dependencies/exposure remain independent. |
| P9-REQUAL-SEC02 | LOCALLY REMEDIATED; post-pin audits zero; complete final-source successor validation PENDING; production NOT DEPLOYED | bounded dependency remediation observed locally; full frozen successor qualification pending | testpass and functionality-inventory criteria still require final complete evidence; no cleanup authority | NOT DEPLOYED; no deployed fix or app-reachable exploit claim; independent release gates retained; published vulnerable package version repaired locally; remaining evidence qualification | Dependency/QA owner: Pin Next and eslint-config-next exactly16.4.1. Existing static export, unoptimized images and Sharp0.35.5 mitigate the image-serving path; they do not close every package advisory. Retain before/after advisory paths, raw hashes and exact lock/pin delta; complete frontend/browser and required full successors, then bind all reports to final source. Production deployed dependencies/exposure remain independent. |
| P9-REQUAL-SEC03 | LOCALLY REMEDIATED; post-pin audits zero; complete final-source successor validation PENDING; production NOT DEPLOYED | bounded dependency remediation observed locally; full frozen successor qualification pending | testpass and functionality-inventory criteria still require final complete evidence; no cleanup authority | NOT DEPLOYED; no deployed fix or app-reachable exploit claim; independent release gates retained; published vulnerable package version repaired locally; remaining evidence qualification | Dependency/QA owner: Use compatible existing ^4 dependency range via Zod4.6.6 override; do not add runtime functionality. Retain before/after advisory paths, raw hashes and exact lock/pin delta; complete frontend/browser and required full successors, then bind all reports to final source. Production deployed dependencies/exposure remain independent. |
| P9-REQUAL-SEC04 | OPEN; locked Python advisory coverage INCOMPLETE; critical/high provider labels retained; exploitability NOT ASSESSED | REQUEST §9 explicit security risk review required and NOT CLOSED; zero NuGet/npm findings cannot clear Python findings | entry security uncertainty remains inherited and UNWAIVED; no cleanup authority | RELEASE BLOCKER pending security/sidecar disposition and qualified remediation; no production fix/deployment claimed; open locked Python security findings and coverage gap; major ML/parser transition requires separate validated scope | Security/sidecar owners: preserve exact lock hash and raw provider records, resolve/document PyPI torch+cpu query gap while retaining successful exact OSV evidence, assess applicability/reachability and supported fixes, propose bounded separately validated ML/parser dependency transition with affected regression and model/parser compatibility evidence. No automatic dependency rewrite, model activation or production action. |
The stable review IDs are P9-REQUAL-SEC01, P9-REQUAL-SEC02, P9-REQUAL-SEC03 and P9-REQUAL-SEC04. Original RED live audits remain under `.artifacts/p9-requal-01/security-before-pins/`. Post-pin [NuGet log](../../../../.artifacts/p9-requal-01/security/nuget-advisories.log), [frontend audit JSON](../../../../.artifacts/p9-requal-01/security/frontend-audit.json) and [npm log](../../../../.artifacts/p9-requal-01/security/npm-advisories.log) report zero NuGet findings and npm0 vulnerabilities. Root will record exact raw hashes and final source binding. This confirms package advisory remediation locally, not a reachable exploit before the fix or a deployed production fix.
The fresh locked Python evidence under `.artifacts/p9-requal-01/security/python-locked/` is **OPEN / coverage INCOMPLETE**. It contains49 pins/98 queries:97 successful,1 PyPI404 for `torch2.6.0+cpu`; exact OSV lookup succeeds.249 provider records yield109 advisory alias groups across certifi, filelock, pillow, pypdf, setuptools, torch, transformers and urllib3. Some provider labels are critical/high; exploitability is not assessed.109 alias groups are **not109 independent CVEs**. Locked-file SHA256:`36f2912965d183e59f2d8320f3817ca8dd0fee6d76e05cfd63ac92defe6c6031`. CI is preparing tracked `python-advisories.json` and `PYTHON_SECURITY.md`; this register does not claim those summaries complete. REQUEST §9 security review remains unclosed, Phase10 entry uncertainty remains unwaived and production release remains blocked. A major ML/parser dependency transition needs separately validated scope; no automatic dependency rewrite or model activation is authorized. Zero NuGet/npm findings cannot support blanket security PASS.
## Current requalification changes awaiting final source freeze
Root's authorized live audits identified the vulnerable dependency versions recorded above. Local compatible corrections are Pkcs8.0.2, Next/eslint-config-next16.4.1 and Zod4.6.6. Post-pin NuGet/npm audits show zero findings. Post-pin backend1123, MariaDB32 and build/types/lint pass; complete frontend and browser successors are still pending. Pre-pin frontend934 cannot qualify the final dependency source. Exact source/report hashes and final complete successors remain root-owned. No application-reachable exploit or production repair is claimed.
Hosted database CI remains **BLOCKED**: root investigation found merged host-Docker-socket binding; private service isolation/lifecycle is not proven. Keep the explicit workflow gate. Local real-MariaDB evidence does not cure hosted isolation, and hosted isolation does not invalidate local provider persistence results. Final evidence and operator handoff belong to the root CI report.
## Inherited accessibility causes
The [original triage](../p9-03/inherited-axe-triage.json) reconciles64 inherited occurrences into four cause groups. Fresh scans have separate repeated occurrences; neither count is a unique defect count. These four entries also appear with independent gate/dependency fields in findings.json.
| ID | Underlying cause / historical occurrences | Disposition / exact closure |
|---|---|---|
| P9-AX-01 | drawer/focus: focusable-modal-open, 1 occurrences | Requires manual inspection. Shared Dashboard drawer settled predicate, Escape focus and24Tab proof pass. Original Analytics transition cause is not proven; human reader/transition qualification remains. |
| P9-AX-02 | dashboard size selector: bgOverlap, 48 occurrences | Requires manual inspection. Contextual foreground/background measurement remains required. |
| P9-AX-03 | text/control with obscured background: bgOverlap, 5 occurrences | Requires manual inspection. Contextual foreground/background measurement remains required. |
| P9-AX-04 | hidden badge: shortTextContent, 10 occurrences | Not applicable in tested settled hidden state. Four current native EN/NB theme vectors: aria-hidden=true and zero area. Actual reader/live unread-state remains separate. |
No current final verdict is locked by this source register. Root must reconcile fresh hosted CI, full regression, source binding, sustained workload and review before issuing the five independent recommendations. Open rows remain visible even if another lane passes. P9-L02 and P9-PERF-01 are low development debt; RD19/RD20 and C11 are deferred product/model scope; production-only operations stay operator gates. The 57 preservation groups are capabilities with individual residuals, not 57 confirmed defects; see [preservation](PRESERVATION.md). No test counts are summed across overlapping lanes.
@@ -0,0 +1,27 @@
# Local socket recurrence — 24 October 2026
At11:05UTC the local Desktop engine pipe was absent and Desktop/backend processes were stopped.
Starting the local engine reproduced the exact `initializing Inference manager ... dockerInference`
startup error at11:07:41UTC. This was a local workstation failure, separate from the unsafe hosted
runner configuration and the application tests. No production host was accessed.
The [previous socket recovery](../../../database/mariadb-standardisation/DOCKER_LOCAL_RECOVERY.md)
was rechecked. Both named directories contained only zero-byte reparse-point socket entries:
three in `%LOCALAPPDATA%/Docker/run`, one in `%LOCALAPPDATA%/docker-secrets-engine`.
Only Desktop/backend processes started by this attempt were stopped after identity/start-time checks.
The directories were moved to verified sibling archive paths and fresh empty originals created:
- `C:/Users/Connor/AppData/Local/Docker/run.stale-20261024-p9-requal-cccb40b8`
- `C:/Users/Connor/AppData/Local/docker-secrets-engine.stale-20261024-p9-requal-660ce455`
Archive metadata records11:11:31UTC. Subsequent Desktop startup exited0 and the engine reported
29.7.2. The task-owned disposable MariaDB service alone was started and became healthy on loopback33308.
The persistent development service was left stopped; its container/volume were not removed.
There was no factory reset, image/data/volume deletion, WSL disk operation or production restart.
Raw scoped recovery script and metadata remain in `.artifacts/p9-requal-01/`. Initial recovery
guard attempts stopped on a timezone comparison and a changed process identity; neither moved
directories. The process set and UTC comparison were corrected before the successful archive.
No application qualification is inferred from recovery alone. Socket recurrence and workstation
lifecycle remain local tooling limitations; preserve the archives and diagnose another recurrence
before repeating any action. Do not archive broader Docker data directories.
@@ -0,0 +1,155 @@
# Manual acceptance cards — P9-REQUAL-01
**All M09-01–10 remain NOT TESTED by a human in this requalification.** These cards expand the unchanged [original checklist](../p9-03/MANUAL_CHECKLIST.md); they record no fabricated execution. Under [REQUEST §6](../REQUEST.md#6-accessibility-qualification), unfinished checks carry into release gates. They are not automatically57 preservation defects or proof of a present failure. Relevant RD06/07/10/14/18/23/25 qualifications remain independent of local automation.
Before each card, record exact final application commit, static-export/API assembly/source-manifest hashes, environment URL, MariaDB image/config digest and effective nonproduction configuration. Use the final qualified build from the root validation ledger; no card may silently run against a stale build. Record tester name/role, UTC start/end, OS/browser/AT/device/font/printer versions and test-data IDs. Use isolated development or specifically approved sandbox environments, never production. If any environment/permission/device is absent, leave that applicable portion NOT TESTED and record the dependency; no implication of failure or pass.
Synthetic pack: one owner plus separate admin and foreign tenant accounts, two independent same-owner sessions;250 jobs only when the approved isolated fixture provides them; one ordinary job with long title/notes/timeline; long Career history with8 roles/48 distinct bullets, Åse/æ/ø, one-/multi-page CVs, optional sections and synthetic photo; notifications0/1/many, all9 Dashboard widgets. Use controlled provider inboxes for M09-10. Verify seed readback first. Do not create/delete/reset live records. Preserve frozen document hashes before/after any relevant workflow. Controlled failure injection is an approved harness action, not an invitation to stop a real service.
Priority P1 identifies release/accessibility/data/output/provider qualification; P2 is professional language review. Priority does not grant production or provider authority. Complete cards against both English and Bokmål and light/dark where applicable, retaining per-state evidence rather than one unexplained aggregate checkmark.
## M09-01 — Reader landmarks and complete forms (P1)
Owner: Accessibility tester. Status: **NOT TESTED**.
Environment: NVDA with a supported Windows browser, then VoiceOver with Safari on supported Apple hardware. Record actual versions.
Steps: With reader speech enabled, traverse /login, /jobs, /jobs/{synthetic-id}, /career, /career/builder/{synthetic-id}, /documents and /settings by headings, landmarks, tab/form navigation. Trigger an invalid field and a failed save in the approved test harness, then recover. Open and settle-close drawer/dialog/menu. Repeat equivalent journey with VoiceOver.
Pass criterion: Names/roles/values/selection, hierarchy, errors and reading order understandable; modal background not announced; successful/error status accurately announced.
Fail criterion: Missing or misleading name/state, hidden content spoken, unreachable form or unannounced blocking error.
Evidence to retain: Spoken transcript or recording, reader settings and exact failing/successful control; no real personal data.
## M09-02 — Focus and modal transitions (P1)
Owner: Accessibility tester. Status: **NOT TESTED**.
Environment: Supported desktop browser, keyboard only; repeat drawer at narrow width. Actual reader can supplement, never substitute keyboard navigation.
Steps: Start at skip link; Tab/Shift+Tab through navigation and command dialog. Open Add/Edit Job, confirmation, select and menu; Cancel, Escape and submit/retry after a controlled failed save. Move through job sections and CV preview/history. Open/close drawer repeatedly including during ordinary transitions.
Pass criterion: Visible logical focus; modal trap only while open; Escape/cancel returns focus to initiating usable control; no closed/offscreen tab stops or lost focus.
Fail criterion: Keyboard trap outside modal, disappearing focus, background tabbing during modal or inability to recover after failure.
Evidence to retain: Short video plus focus sequence/control names and transition timing; preserve historical RD14 observation even if current run passes.
## M09-03 — Dashboard announcements and deliberate recovery (P1)
Owner: Accessibility tester. Status: **NOT TESTED**.
Environment: Supported browser and actual reader; two independent authenticated sessions for same synthetic owner. Approved isolated harness for failed response.
Steps: On /dashboard open Customize. Hide/show a widget, Earlier/Later, resize, Cancel; reopen, Reset cancel then confirm. Create stale layout in second session and save it; first session Save must conflict, retain draft and offer review/reload. Inject one save failure using approved harness, then deliberately recover. Save empty layout and restore visible widgets.
Pass criterion: Each action/order/save/error announced once, spoken position agrees with DOM order, keyboard alternative to pointer exists, conflicts/failures retain draft and never claim saved.
Fail criterion: Silent/misleading status, inaccessible reordering or lost rejected draft.
Evidence to retain: Reader transcript, before/after order/revision and response status; do not capture credentials.
## M09-04 — Contextual painted contrast (P1)
Owner: Accessibility tester. Status: **NOT TESTED**.
Environment: Actual supported browser, light/dark, painted-color sampling tool; include focused/hover/disabled/native-select/scrolled states.
Steps: Use original P9-AX-02/03 affected nodes from inherited triage. Inspect size controls, all53 painted-background occurrences grouped by cause, scrolled CV editor and fixed chrome. Open native select and inspect applicable text/UI/focus in both themes; retain each affected cause/node/state mapping.
Pass criterion: Normal text >=4.5:1; large text >=3:1; necessary UI boundaries/focus >=3:1. Apply documented WCAG exceptions only to truly inactive controls; record why.
Fail criterion: Any applicable ratio below threshold or no reliable painted sample for a required state; unresolved sample stays NOT TESTED.
Evidence to retain: Foreground/background actual paint colors, ratio, node/state and screenshot; automation incomplete alone is not closure.
## M09-05 — Hidden badges and settled drawer (P1)
Owner: Accessibility tester. Status: **NOT TESTED**.
Environment: Actual reader plus browser accessibility tree; Dashboard and original Analytics drawer context.
Steps: Inspect unread=0 hidden badge, then synthetic positive unread count. Open drawer; inspect modal background and reading/tab order; settle-close and repeat reader traversal and24 Tab presses. Repeat in Analytics context matching inherited occurrence.
Pass criterion: Zero badge absent from announcements, positive count meaningful, open modal excludes background appropriately; settled closed drawer neither spoken nor tabbable.
Fail criterion: Hidden zero announcement, missing positive count, closed drawer reader/focus access or background leakage during modal.
Evidence to retain: Accessibility tree, speech and focus record per exact state. Passing settled Dashboard does not retroactively explain original Analytics cause.
## M09-06 — Reflow, clipping and visible controls (P1)
Owner: Accessibility tester. Status: **NOT TESTED**.
Environment: Real browser100/200/400% zoom, text-only enlargement where supported, increased text spacing and forced colors; record OS/browser and actual zoom method.
Steps: At each setting traverse Jobs pagination, job/document section tabs, CV tabs/controls, Settings inputs and significant forms using keyboard. Scroll intentional table/document/Board scrollers, then reach every primary action. Check long EN/NB labels and error states.
Pass criterion: Essential content and focused actions readable/reachable, no root clipping or lost focus; intentional scrollers remain usable; fixed A4/Letter document geometry preserved.
Fail criterion: Unreachable clipped control/text, obscured primary action or focus, broken forced-colors affordance.
Evidence to retain: Screenshot at each setting, focused control and scroll path. The historical16 offviewport states remain explicit targets; zero root overflow alone is insufficient.
## M09-07 — Physical input and interruption (P1)
Owner: Product owner with physical devices. Status: **NOT TESTED**.
Environment: Actual supported phone and tablet, portrait/landscape, software keyboard, touch/pointer as applicable; record model/OS/browser/input.
Steps: Edit long Career/job/letter text while keyboard is open. Open drawer/dialog/select, rotate, recover after Cancel and controlled failed save. Reorder Dashboard/Board using supported touch action and named alternative; cancel drag. Traverse CV preview; download synthetic PDF.
Pass criterion: Primary action not obscured by keyboard; draft retained across intended interruptions; reachable touch targets; no unintended destructive/move operation; alternatives usable.
Fail criterion: Obscured save, lost draft, accidental destructive/move, unusable touch target or failed supported download.
Evidence to retain: Device video/screenshots including keyboard/rotation and result readback. Emulated touch cannot close this card.
## M09-08 — Professional Bokmål and terminology (P2)
Owner: Professional Bokmål reviewer. Status: **NOT TESTED**.
Environment: Application nb locale on exact build; both themes, narrow and desktop, ordinary user and separate synthetic admin. Include dates/numbers/plural states.
Steps: Review registration/recovery, errors/validation, onboarding, Dashboard customization, Jobs/Board/history, Career/import, CV/library, documents/mail, notifications, Settings/Admin. Use long names,0/1/many counts and boundary date/number values. Inspect accessible names with reader. Record raw Created metadata separately from application copy.
Pass criterion: Natural professional Bokmål, consistent established terms, meaningful validation/names and correct date/number presentation. Distinguish authored/provider English and technical names.
Fail criterion: Untranslated application string, unnatural/misleading term, incorrect locale format or inaccessible label; authored English alone is not defect.
Evidence to retain: Exact key/control and screenshot, proposed wording and reviewer rationale. P9-L02 remains low debt until separately implemented/reviewed.
## M09-09 — Intended-environment PDF fidelity (P1)
Owner: Document reviewer. Status: **NOT TESTED**.
Environment: Intended Linux renderer/font set, supported primary and alternate browsers and actual printer or approved print workflow. Record versions/fonts/template/page size.
Steps: Generate synthetic one- and multi-page EN/NB CVs in every supported template, with long words/employment/bullets, missing optional sections, Å/æ/ø/special characters and permitted synthetic photo. Compare preview with downloaded originals; inspect/search/copy text; print representative pages including first/last/page-break boundaries.
Pass criterion: Legible fonts/glyphs, correct language/date, no missing/duplicate content, intact margins/header/footer/page breaks, photo/crop and preview/export agreement.
Fail criterion: Missing glyph/content, overflow/cutoff, duplicate blocks, wrong pagination/font/photo or illegible printed output.
Evidence to retain: Original PDF bytes+SHA256, page count, renderer/font/browser versions, screenshots/print observations and named template. Existing60-page automated oracle is separate.
## M09-10 — Real provider consent, delivery and uncertain outcomes (P1)
Owner: Approved provider tester. Status: **NOT TESTED**.
Environment: Explicitly authorized nonproduction OAuth/Turnstile/mail/calendar sandbox accounts, controlled recipient inboxes and timezone/DST scenarios. No production credentials or real personal documents.
Steps: Test Google/Microsoft consent and denied/cancelled popup; validate expected origin/state. Verify/reset email to sandbox inbox including used/expired token. Link mailbox, revoke grant and reconnect. Simulate uncertain send only with approved sandbox fixture, then review rather than automatically resend. Create calendar events around DST transitions, explicit offsets and date-only boundaries.
Pass criterion: Correct owner/origin/state, explicit confirmation, recoverable revoke/reconnect, actual intended mail delivery, no accidental resend and correct timezone/date semantics.
Fail criterion: Cross-owner/mismatched state accepted, resend without deliberate review, failed intended delivery or wrong event time/date.
Evidence to retain: Correlated sanitized provider request/response/delivery/event timestamps, exact account scope and timezone expectation. Local SMTP/HTTP tests do not close provider semantics.
## Recording and closure
For each state, retain a row: `card ID | tester | UTC | commit/build hashes | environment | OS/browser/AT/device | synthetic IDs | steps actually executed | expected | observed | PASS/FAIL/NOT TESTED | evidence path/hash | defect ID/owner/retest`. A failed observation gets a bounded reproducible defect and linked evidence; keep the original failure, then record a new successor after repair. Untested subcases stay explicit; completing one browser/device never closes another. Redact tokens, credentials and unrelated personal information. Store recordings/reader transcript/PDF bytes in approved nonproduction evidence storage and link them from the final ledger.
A genuine tester reviews every applicable subcase before closing the card; product/accessibility/provider owners accept any documented non-applicable state with a reason. An AI screenshot review, DOM assertion, healthy endpoint or axe0-violation result cannot sign these cards. C11/P5-13 semantics, infrastructure/backup/restore/rollback, monitoring, creation idempotency and enabled-worker recovery have separate gates and are not authorized by this checklist. No Phase10 implementation or release approval follows automatically.
@@ -0,0 +1,54 @@
# P9-REQUAL-01 performance protocol
Status: **source prepared; real workload not executed by fixture author**. Historical P9-05 and RD21 sustained fixtures remain unchanged. Their short SQLite observations are not evidence for this lane. Actual runner results must be added below after the exclusive run.
The only new executable source is in `performance/`. It extends `job-tracker-ui/playwright.mariadb.config.ts`, preserving the canonical disposable-schema guard, MariaDB provider/version and `reuseExistingServer: false` on both servers. The additional guard requires port `33308` and a prebuilt production static frontend. No production code, dependencies, SQLite database, mock database or mocked API response is introduced.
## Runner prerequisites
- Run alone after other test/build/browser/load lanes stop. Free ports 5302 and 3300; existing listeners cause failure, never reuse.
- The trusted root runner creates and later cleans up one generated `jt_it_<32 lowercase hex>` schema on its pinned `mariadb:12.2.2` disposable Docker instance at `127.0.0.1:33308`.
- Supply `JOBTRACKER_MARIADB_BROWSER_PORT=33308`, `JOBTRACKER_MARIADB_CLIENT=<absolute native mariadb/mysql executable>`, and `JOBTRACKER_MARIADB_BROWSER_CONNECTION=Server=127.0.0.1;Port=33308;Database=<generated schema>;User ID=root;Password=jt-integration-only;SslMode=None`.
- Supply `PLAYWRIGHT_STATIC_EXPORT=true` and `PLAYWRIGHT_PREBUILT_EXPORT=true`. Build Release backend and frontend from the frozen source before this lane. Preserve source/build/report hashes and timestamps in the root evidence ledger. This fixture does not establish source freshness itself.
- Execute from `job-tracker-ui` so the inherited server commands and `.artifacts` paths resolve correctly. Do not enable server reuse or rerun failed tests silently. The config sets retries to zero even in CI.
Run after setting the above environment in the runner (all shell invocations use RTK):
```powershell
rtk node node_modules/@playwright/test/cli.js test --config=../docs/redesign/phase-9/p9-requal-01/performance/playwright.config.ts
```
Fresh output is at repository-root `.artifacts/p9-requal-01/performance/`: `playwright-results.json` plus `test-results/`, including a JSON attachment named `p9-requal-01-mariadb-performance`. Preserve failure attachments as well as successful ones.
## Measured workload and invariants
Setup uses the current API contract: create one free synthetic owner, one owned company, exactly 250 applications (record every returned ID), eight Career Profile roles with six long multilingual bullets each using the current expected revision, and one minimal CV variant. Free membership avoids the existing Premium creation-time model summarizer; this is a database/UI workload. Setup ends before the measured clock starts. SQL independently verifies MariaDB `12.2.2` and reads committed state in read-only transactions through the supplied native client.
Before starting that clock, fail-fast preflight checks each of the four endpoint response shapes/content once and performs one readiness-checked navigation to each workspace. Preflight navigation/telemetry is separately labelled and excluded from workload counts and percentiles. The attachment records setup/preflight/workload/preservation/complete phase and `preflightPassed`; a preflight failure does not pretend a 15-minute run occurred. Source checks confirm `CareerProfileService` derives `startDate`/`endDate` from supplied `start`/`end`, and `CvRenderModel` consumes normalized dates with source-date fallback. `JobTable` owns `data-job-row-id`; Dashboard renders the English `Tracked jobs: 250` accessible link without a paid-role condition when its default summary widget has jobs.
The measured interval is an actual monotonic-clock **900000 ms minimum**, enforced in source with no duration override. The test timeout is 18 minutes, including setup and final checks. Exactly two independent API reader contexts run until the deadline. Each sequential reader rotates stats, board, Career Profile and documents, waits at least 500 ms after completion before its next request, has a 10-second per-request timeout, and never catches up with bursts. Native browser requests are additional UI traffic and are not counted as synthetic reader operations. Response bodies are disposed after measurement to avoid accumulating load-generator storage.
Twenty browser navigations are scheduled every 45 seconds across the interval (five rounds of Dashboard, Table, Board and long CV). Dashboard must show 250 jobs, Table must render a row, and Board must contain the exact seeded ID set. CV assertions retain the historical page-readiness and final-marker-on-last-page checks. Errors are recorded once and the timed interval continues; all failures fail the final gate. Playwright locator polling is ordinary readiness checking, not a workload retry.
Before and after the interval, independent SQL snapshots must match exactly: 250 unique application IDs, owned/deleted counts, every tenant's application count, canonical profile JSON and revision, Identity projection JSON and Career Profile history count. API board membership and full saved-profile equality are checked again afterward; board and profile reader responses also assert exact content throughout. The fixture leaves synthetic data for the trusted disposable-schema cleanup; it never deletes or changes other tenants.
The attachment retains every reader attempt (worker/endpoint/start offset/duration/status/bytes/error), all navigation outcomes, p50/p95/p99/max successful response latency with explicit attempt/failure counts overall and per endpoint, and failures separately for navigation. Percentiles use the nearest-rank rule. Descriptive telemetry includes Chromium CDP metrics and DOM count, host free memory/load average, and Node process memory/CPU counters sampled on successful navigations. Windows load-average zeros are platform limitations; Node is the load generator, and no server CPU attribution is implied. Optional root-runner Docker/process telemetry may accompany the attachment but must identify its source and sampling interval.
Acceptance requires zero reader/navigation failures, all 20 navigations, both readers active into the final 15 seconds, pacing checks, minimum duration, exact before/after preservation and a passing process exit. Request counts vary with response duration; there is no invented fixed-count target, SLA threshold or capacity claim. This provides bounded local synthetic MariaDB evidence only.
## Harness validation
The fixture author ran the following without launching services, browsers, builds or the long workload; both exited 0:
```powershell
# From repository root
rtk node docs/redesign/phase-9/p9-requal-01/performance/metrics.mjs --self-test
# From job-tracker-ui
rtk node node_modules/typescript/bin/tsc --noEmit --skipLibCheck --strict --allowJs --esModuleInterop --target es2023 --module esnext --moduleResolution bundler ../docs/redesign/phase-9/p9-requal-01/performance/playwright.config.ts ../docs/redesign/phase-9/p9-requal-01/performance/sustained.spec.ts
```
The dependency-free self-check exercises empty percentiles, exact percentile boundaries, exclusion/counting of HTTP and transport failures, reversed membership, missing/duplicate/substituted IDs, and the fixed duration/pace constants. Strict TypeScript validates the fixture and inherited configs; this is harness validation, not workload evidence.
## Actual results
Pending root runner execution and evidence reconciliation. No measured duration, latency, throughput, preservation or capacity outcome is claimed yet.
@@ -0,0 +1,109 @@
# P9-REQUAL-01 preservation and route register
All **57 groups / 33 routes** are retained. The historical qualification states remain **56 PARTIAL / PRES-C11 DEFERRED, zero promoted to PASS**. These are capability qualifications, not 57 confirmed defects or automatic blanket failures. Each row retains its individual residual and owner in [preservation.json](preservation.json). This source review does not rerun a browser or certify a human workflow; fresh results belong to the root-owned validation ledger.
Source criteria: [original functionality matrix](../../phase-2/PRESERVATION_MATRIX.md), [later Phase7 residuals](../../phase-7/PRESERVATION.md), [Phase9 matrix](../PRESERVATION.md), [RD21 reconciliation](../rd21-remediation/PRESERVATION.md). The newer [MariaDB acceptance](../../../database/mariadb-standardisation/ACCEPTANCE.md) closes bounded RD21 save/concurrency provider proof; it does not close creation idempotency, every independent editor, production rollback or human acceptance. P2-D01 and existing CV variant/frozen contracts stay passing in their tested scope; canonical replacement risk is separately mapped.
For Phase9, examine each exact residual and classify its effect. REQUEST §6 carries unfinished genuine human checks into release gates. For Phase10, [IMPLEMENTATION_PLAN §Phase10](../../IMPLEMENTATION_PLAN.md#phase-10--cleanup) separately requires unused-route proof, passing tests and a satisfied functionality inventory. None of these statuses authorizes deletion or starts Phase10. Production retains applicable human/provider/output/operator gates. Partial capability coverage is not silently converted to complete acceptance.
| Group | Owner | Retained status | Exact remaining closure / dependency |
|---|---|---|---|
| PRES-E01 | LandingPage, planCatalog; AccountPlans | PARTIAL | Public-page complete mobile/keyboard review and actual billing availability; no invented prices Dependencies: applicable human rows M09-01–09. |
| PRES-E02 | LoginPage, TurnstileWidget, VerifyEmailPage; AuthController | PARTIAL | Real mail/reset submit/Turnstile and token expiry/used-token cases; query loss is repaired for its tested scope Dependencies: intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-E03 | GoogleAuthCard, MicrosoftAuthCard, MicrosoftLegacyRelinkPage; token validators | PARTIAL | Actual consent/popups/revoked grants/relink; mailbox grants remain separate from sign-in Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-E04 | TwoFactorSettingsCard, TwoFactorChallenge; TwoFactorController | PARTIAL | Real authenticator/recovery and reader/trusted-device traversal; sensitive confirmations retained Dependencies: applicable human rows M09-01–09. |
| PRES-E05 | SessionsSettingsCard, AppShell, auth; SessionsController | PARTIAL | Native session-card refresh after remote revocation and complete device/AT acceptance Dependencies: applicable human rows M09-01–09. |
| PRES-E06 | ProfilePage; AuthController, AvatarStorage | PARTIAL | Complete avatar/remove/password/email-change native flow; account identity remains distinct from career facts Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-E07 | AppShell, QuickCommandDialog; job/company search APIs | PARTIAL | Spoken names/live state, settled drawer AX-name triage and historical command/dialog timing; no route silently hidden Dependencies: applicable human rows M09-01–09. |
| PRES-E08 | themePrefs, I18nProvider, confirm, prompt; auth preference | PARTIAL | Full OS/locale/state cross-product, contrast/incomplete axe, reader/reflow; save feedback is distinct from server500 root-cause repair Dependencies: applicable human rows M09-01–09. |
| PRES-E09 | App, api, sessionBoundary; SessionContextMiddleware | PARTIAL | Broader BFCache/offline/old-bundle/deployed cases; presentation never becomes authorization Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-J01 | JobDiscoveryPage; JobDiscoveryController | PARTIAL | Live NAV availability/freshness and long-result keyboard/narrow traversal Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-J02 | JobDiscoveryPage; saved-search/alert services | PARTIAL | Actual scheduler-to-owned-notification delivery, human/mobile delete acceptance and intended-provider qualification; deterministic runner is not live NAV Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-J03 | AddJobModal; JobImportService, JobApplicationsController | PARTIAL | Duplicate/company/date/interrupted creation and distinct creation-intent idempotency debt; preview/open is no-write Dependencies: applicable human rows M09-01–09. |
| PRES-J04 | App, captureUrl, QuickCaptureCard; job/CV APIs | PARTIAL | Installed extension/share/no-replay and attachment failure recovery; keep payload and safe-return contracts Dependencies: applicable human rows M09-01–09. |
| PRES-J05 | JobTable, SavedViewsMenu; JobApplicationsController | PARTIAL | Full saved-view CRUD/dirty page-size/history combinations and supported capacity; server query semantics unchanged Dependencies: applicable human rows M09-01–09; separate AI/model qualification; no activation. |
| PRES-J06 | JobTable, EditJobDialog; job lifecycle/controller | PARTIAL | Complete selected-scope/partial-failure/dirty/manual combinations; no hard-delete substitution Dependencies: applicable human rows M09-01–09. |
| PRES-J07 | KanbanBoard, pipeline; status endpoint | PARTIAL | Physical drag/touch/browser variants and O(N) full-array capacity; no capped hidden results or pipeline redefinition Dependencies: applicable human rows M09-01–09. |
| PRES-J08 | CompaniesTable; company relationship APIs | PARTIAL | Scoped in-modal failure proof follows P6 manifest; complete contact/date/source-link and human acceptance remain Dependencies: applicable human rows M09-01–09. |
| PRES-J09 | OfferComparisonPage; job API | PARTIAL | Many-offer mobile/currency/date acceptance; no invented salary normalization Dependencies: applicable human rows M09-01–09. |
| PRES-J10 | ImportExportJobs; CSV/job controller | PARTIAL | Complete native roundtrip and target spreadsheet compatibility; job import is not account restore Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-W01 | ApplicationWorkspacePage, jobWorkspaceRoute; workspace API | PARTIAL | `followMode` metadata retained; compose consumer deferred. P5-05 optional preview lacks production host; full page remains Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-W02 | WorkspaceHeader, JobDetailsSection, ApplicationProgress/Timeline; workflow APIs | PARTIAL | Stored-event localisation/timezones/status-reload combinations ; job stale-replacement contract repaired within qualified MariaDB 12.2.2 scope Dependencies: applicable human rows M09-01–09. |
| PRES-W03 | ApplicationAnalysis/Match/StrategySnapshot/StatusSuggestion; intelligence/AI services | PARTIAL | Real-model truthfulness/freshness/cancel quality; route entry never auto-generates; unrelated AI survives C11 withholding Dependencies: applicable human rows M09-01–09; separate AI/model qualification; no activation. |
| PRES-W04 | ApplicationChecklist; checklist service/controller | PARTIAL | Mobile interruption/edit/delete/reader; item identity and keyboard alternatives retained; no invented due-date schema Dependencies: applicable human rows M09-01–09. |
| PRES-W05 | ApplicationCvSection/TailoringSection; ApplicationAssetsService | PARTIAL | **P2-D01 remains ordinary passing**; broader attach-render/model/deployed combinations remain; owner/job association unchanged Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification; separate AI/model qualification; no activation. |
| PRES-W06 | ApplicationCoverLetterSection, CoverLetterAiAssistant; assets/history | PARTIAL | Complete native history/reject/reader/model combinations; no canonical mutation or auto-send Dependencies: applicable human rows M09-01–09; separate AI/model qualification; no activation. |
| PRES-W07 | ApplicationPackageDraftsSection, SubmittedPackagesSection; package service/storage | PARTIAL | Actual production restore/rollback/all-storage combinations; current edits/delete cannot mutate frozen bytes; capture is not delivery Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25; independent output/storage/deployment qualification. |
| PRES-W08 | Attachments; AttachmentsController, AttachmentStorage | PARTIAL | Physical drop, full rename/download/reader and deployed storage; AI inclusion remains explicit Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-W09 | InterviewPrep; interview persistence/AI endpoints | PARTIAL | Real-model and physical software-keyboard/reload/reader; preparation is not a scheduled interview Dependencies: applicable human rows M09-01–09; separate AI/model qualification; no activation. |
| PRES-W10 | InterviewPrep; follow-up draft services | PARTIAL | Timezones/provider delivery; `followMode` reviewed compose UI remains deferred, no automatic send Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-W11 | RemindersView, RulesSettingsCard, ApplicationCalendarActions; calendar/rules APIs | PARTIAL | F21 DST/date-only/offsets, real provider events/scheduler delivery and historical unconfirmed Rules500 cause Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-W12 | OperationsPage, CareerProfilePage; operation store/worker | PARTIAL | Disconnect/reconnect and F20 enabled-worker fault recovery; stable operation IDs/no duplicate starts Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-C01 | CareerProfilePage, career views; CareerProfileService/controller | PARTIAL | Full long-tail mobile/human acceptance; canonical concurrency has bounded SQLite/MariaDB repair proof; singleton source distinct from identified variants Dependencies: applicable human rows M09-01–09. |
| PRES-C02 | CareerWorkspacePage, CareerProfilePage; version service | PARTIAL | Complete native historical restore/clipboard/avatar/reader; live inherited CVs may reflect later source changes Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-C03 | CareerProfilePage; profile-CV/processing/diff services | PARTIAL | Scanned/OCR corpus and complete low-confidence/discard/native-reader flow; extracted proposal is not canonical truth Dependencies: applicable human rows M09-01–09. |
| PRES-C04 | CareerEvidenceBank; CareerEvidenceController | PARTIAL | Many-item/delete/keyboard/human acceptance; SQLite compatibility repaired with owner SQL preserved, full frontend/production acceptance remains qualified Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-C05 | CvBuilderPage, thumbnails; CvVariantService/PublicCvController | PARTIAL | Library migration not complete; publish/unpublish/old-slug revocation and every template combination required Dependencies: applicable human rows M09-01–09. |
| PRES-C06 | CvBuilderEditor, CvPreviewPane; preview API | PARTIAL | Physical keyboard/touch/AT and broader short-height/wrapped/deployed cases; one editor/save/preview owner Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-C07 | CvBuilderEditor, CV controls/useDragReorder; resolver/settings | PARTIAL | Physical selection/drop/all-browser and exhaustive reset/inheritance combinations; sibling/canonical/frozen separation retained Dependencies: applicable human rows M09-01–09. |
| PRES-C08 | CvCustomizeTab, registry; ThemedCvRenderer | PARTIAL | Production fonts/portraits/wrapped lines/custom color/reader; schematic gallery is not final-photo qualification Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-C09 | cvBuilder, CvCustomizeTab; settings normalization/render model | PARTIAL | Advanced cross-product/manual print/deployed fonts; UI locale distinct from document locale; no print geometry styling change Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-C10 | CvBuilderEditor; CvVariantService version history | PARTIAL | Long offline/history and IF-CV-UNSAVED-01 trace; settings history is not immutable historical profile/PDF; profile/job saves remain separate Dependencies: applicable human rows M09-01–09. |
| PRES-C11 | AiSectionAssistant, DocumentAiTab; CV AI assist | DEFERRED | **0/366 configured-target calls;0 human semantic reviews;review UI absent.** Production503 `cv_ai_unavailable`/no provider/no write remains; safe withholding is not generation acceptance Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25; independent output/storage/deployment qualification; separate AI/model qualification; no activation. |
| PRES-C12 | PublicCvPage, cvBuilder; HtmlPdfGenerator/shared admission | PARTIAL | Deployed Linux/font/photo/browser/manual-print qualification and complete revoke/busy-recovery cases; white print pages retained Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification. |
| PRES-M01 | Correspondence; CorrespondenceController | PARTIAL | Full edit/delete/paste/reader native combinations; logging is not sending Dependencies: applicable human rows M09-01–09. |
| PRES-M02 | Correspondence; GmailController/provider | PARTIAL | Actual provider threads/multi-account revoke/reconnect/sync failures; cross-job changes owner-bound Dependencies: intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-M03 | CorrespondenceInboxPage; EmailController | PARTIAL | Large mailbox/native attachments/complete reader acceptance; global inbox remains Phase7 migration Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-M04 | CorrespondenceInboxPage; EmailDrafts/Send/attempt store | PARTIAL | Sandbox delivery/reconnect/dirty interruption; no automatic uncertain resend; reviewed `followMode` UI deferred Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-M05 | GmailReviewPage, CorrespondenceInboxPage; Gmail review | PARTIAL | Real provider and complete proposed-job create flow; route opening is no-write Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-M06 | EmailProviderConnections; Google/Graph/IMAP controllers | PARTIAL | Real grants/revoke/token expiry/reconnect/IMAP safety; keep sign-in identity separate Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-S01 | DashboardView; analytics/controllers | PARTIAL | Exact cohort/timezone/sample/unsupported metrics and whole reader acceptance; no fabricated statistics or Analytics rebuild Dependencies: applicable human rows M09-01–09. |
| PRES-S02 | DashboardView, OnboardingChecklist, NotificationsPopover; notification APIs | PARTIAL | Historical500 causes unproved; full human/notification acceptance; order/resize/reset/cross-device design remains Phase8 Dependencies: applicable human rows M09-01–09. |
| PRES-S03 | SettingsView, RulesSettingsCard, theme/i18n; rules/notification/auth | PARTIAL | Language failure visibility is separate from RD11 server cause; RD12 notification locking/RD13 Rules500 and human/provider acceptance retained Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-S04 | AiPrivacySettingsCard, AiUsageCard, AccountPlanProvider; billing/entitlement | PARTIAL | Checkout/webhook/portal and provider delivery; temporary C11 withholding cannot disable unrelated AI or imply successful review Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25; separate AI/model qualification; no activation. |
| PRES-S05 | BackupCard; export/backup/deletion services | PARTIAL | Current complete backup/restore/keys/tombstones/rollback and deletion/provider policy; downloading backup is not restore proof Dependencies: intended provider/operator sandbox evidence M09-10/RD22–25; independent output/storage/deployment qualification. |
| PRES-A01 | AdminUsersPage; UsersController | PARTIAL | Mounted-role revoke/full persisted mutations/reset delivery; hiding navigation never authorizes API calls Dependencies: applicable human rows M09-01–09; intended provider/operator sandbox evidence M09-10/RD22–25. |
| PRES-A02 | AdminAuditPage; AdminAuditController | PARTIAL | Native successful persisted undo/restore and unsupported-action denial; no generic undo semantics Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification; separate AI/model qualification; no activation. |
| PRES-A03 | AdminSystemPage; AdminSystemController/maintenance/benchmark | PARTIAL | Actual guarded diagnostic/SMTP/model/maintenance and sensitive-output qualification; no production jobs launched Dependencies: applicable human rows M09-01–09; independent output/storage/deployment qualification; separate AI/model qualification; no activation. |
## All 33 routes
Source: [App.tsx](../../../../job-tracker-ui/src/App.tsx) lines420–524. Public `/` and authenticated `/` share one unique path. Wildcard fallback/shell routes are structural and do not inflate the 33-path inventory. Legacy aliases remain consumers, not deletion candidates. Historical route captures and ordinary test mappings remain in [inventory](../p9-00/INVENTORY.md), [QA matrix](../QA_MATRIX.md) and [visual report](../p9-02/README.md); presence and finite captures do not prove every human/device/provider state.
| Route | Source owner | Current disposition / closure |
|---|---|---|
| `/` | Navigate, LandingPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/dashboard` | DashboardView | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/analytics` | AnalyticsPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/discover` | JobDiscoveryPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/jobs` | JobTable | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/jobs/:id` | ApplicationWorkspacePage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/reminders` | RemindersView | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/operations` | OperationsPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/kanban` | LegacyBoardRedirect | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/companies` | CompaniesTable | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/offers` | OfferComparisonPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/correspondence` | CorrespondenceInboxPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/correspondence/review` | LegacyCorrespondenceReviewRedirect | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/profile` | ProfilePage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/applications/:id` | LegacyApplicationRedirect | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/career` | CareerWorkspacePage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/career/builder` | CvBuilderPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/career/builder/:id` | CvBuilderEditor | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/documents` | DocumentsPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/admin/audit` | AdminAuditPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/admin/users` | AdminUsersPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/admin/system` | AdminSystemPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/trash` | JobTable | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/settings` | SettingsView | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/settings/connected-accounts` | ConnectedAccountsPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/login` | LoginPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/register` | LoginPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/forgot-password` | ForgotPasswordPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/reset-password` | ResetPasswordPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/verify-email` | VerifyEmailPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/confirm-email-change` | VerifyEmailPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/microsoft-legacy-relink` | MicrosoftLegacyRelinkPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
| `/cv/:slug` | PublicCvPage | Present in current source census; retain route/identity/query/hash/section semantics. Fresh connected workflow result pending root ledger; applicable manual/provider criteria remain. |
No cleanup candidate has been proven unused by this census. Any future candidate needs its own import/caller/route/alias census, affected preservation criteria, rollback path and passing regression. Historical evidence is preserved unchanged.
@@ -0,0 +1,55 @@
# Python locked advisory gate: OPEN
The Python sidecar dependency gate remains **OPEN**, with **INCOMPLETE** advisory coverage. NuGet/npm remediation is a separate result and cannot close this gate. No Python dependency, lockfile, model, parser or runtime was changed during this review.
The 2026-10-24 07:27:05-07:27:13 UTC review queried all **49 exact pins** in `tools/summarizer/requirements-linux.lock` through the public OSV version query and PyPI release-specific JSON APIs: **98 queries, 97 successes, one error**. PyPI returns 404 for the vendor CPU version `torch==2.6.0+cpu`; its exact OSV query succeeds. No substitute `torch==2.6.0` query or dependency resolution was used.
The responses contain **249 provider records** (137 OSV, 112 PyPI), combined by published alias equivalence into **109 advisory groups across eight packages**. These are separate counts; the groups are not a claim of 109 independent CVEs. No returned group has withdrawn metadata in this snapshot.
| Exact affected query pin | Published alias groups |
| --- | ---: |
| certifi 2026.7.22 | 1 |
| filelock 3.32.4 | 3 |
| pillow 12.3.0 | 41 |
| pypdf 6.16.2 | 19 |
| setuptools 84.0.0 | 1 |
| torch 2.6.0+cpu | 19 |
| transformers 4.48.3 | 22 |
| urllib3 2.7.0 | 3 |
[python-advisories.json](python-advisories.json) retains exact membership, every group ID/alias, package/version, published primary record links, fixed-version events and provider `database_specific.severity` labels. No severity score was invented. Fixed events can differ by branch and are not a compatible upgrade selection. Raw byte responses, query scopes, timestamps, response hashes, parser self-check and membership/accounting checks remain in `.artifacts/p9-requal-01/security/python-locked/`.
- Lock SHA-256: `36f2912965d183e59f2d8320f3817ca8dd0fee6d76e05cfd63ac92defe6c6031`.
- Raw 202-file evidence manifest SHA-256: `3dac31a50d803e4c4a27c43e683dcf7ee7f2587a122cfb8c38fc5276fb7233ec`.
- Raw aggregate response summary SHA-256: `31fe495d72a9b11564c8e78943be7901610e023a8606b5aeb53710871092ca20`.
API semantics: [OSV version query/pagination](https://google.github.io/osv.dev/post-v1-query/) and [PyPI release-specific vulnerabilities](https://docs.pypi.org/api/json/#get-a-release). This is public advisory metadata, not exploit testing or a deployed package inventory.
## Observed sidecar call sites
Static source inspection was made at checkout HEAD `780b9bc64f0923237c5a03f89d240bbe331f058e`; the hashes below identify the actual files read. It establishes application call sites and existing bounds only. It does not determine whether each advisory is reachable, reproduce an exploit or establish production exposure.
| Surface | Verified source evidence | Qualification |
| --- | --- | --- |
| Uploaded document entry | `tools/summarizer/app.py:1317-1336`: `/extract-text` accepts uploaded bytes, checks empty/5 MiB input, extension allowlist and signature, then calls `_run_parser_child`. File/pixel defaults are at `app.py:99-110`. | PDF and image parsers have a route from uploaded documents. No malicious upload or live request was made. |
| PyPDF | `tools/summarizer/parser_child.py:158-178`: constructs `PdfReader(io.BytesIO(data))`, obtains pages, enforces 40-page default and calls `page.extract_text(extraction_mode="layout")` with compatibility fallback. | Reader construction/page enumeration occurs before the explicit page-count check. This call site does not map or dismiss individual advisory conditions. |
| Pillow image path | `tools/summarizer/parser_child.py:150-155`: `Image.open` on uploaded image bytes, image-size check, then OCR; `_ocr_image` can call `image.convert("RGB")` at lines 142-147. Allowed image extensions are PNG/JPEG/WebP at line 26. | Opening occurs before application size validation. An allowlist/size check is not evidence that Pillow advisories are eliminated. |
| PDF OCR fallback | `tools/summarizer/parser_child.py:178-202`: short extracted text falls back to `fitz.open`, rasterizes pages, checks per-page/total pixel budgets, then opens PNG bytes through Pillow for OCR. | Pillow is also used on renderer-produced PNGs. Rasterization occurs before the explicit pixel-budget check. No claim is made about a specific advisory's applicability. |
| Child execution bounds | `tools/summarizer/app.py:1196-1208,1260-1314`: allowlisted child environment, one parser capacity slot, separate temporary directory/process group, default 25-second timeout and result-size checks; timeout termination is at lines 1232-1251 and 1292-1299. `parser_child.py:35-44,237-239` applies POSIX CPU/address-space/file/descriptor/core limits before extraction. | These are existing application controls. POSIX limits return without application on non-POSIX systems. A child process/resource bound is not proof of a security sandbox or closure of parser advisories. |
| Transformers/model loading | `tools/summarizer/app.py:4,19,96,161-169`: imports Transformers and torch; fixed repository name `sshleifer/distilbart-cnn-12-6`; `AutoTokenizer.from_pretrained(MODEL_NAME)` and `AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME)`. | Calls have no explicit revision, `trust_remote_code`, `weights_only` or `use_safetensors` argument in this source. No runtime serialization format, downloaded revision, dependency default or model safety property was verified. |
| Local inference | `tools/summarizer/app.py:172-195,512-518`: lazy/eager runtime loading, tokenizer invocation and `model.generate`; disabled-load path is explicit at lines 174-176. | No model was loaded or executed for this review. Test-time load disabling does not adjudicate the deployed inference path. |
| Direct deserialization search | A search of sidecar Python sources for `torch.load`, `weights_only`, `trust_remote_code`, `use_safetensors` and `revision=` found no explicit call/argument. | Absence in application source does not establish absence in Transformers/torch internals. Those third-party runtime paths were not traced. |
| Service access | `tools/summarizer/app.py:38-42,59-68`: optional service token middleware rejects mismatched/missing headers when the token is configured; health is exempt. | Runtime token/network configuration was not inspected. Conditional authentication is not an advisory mitigation verdict. |
| Installation boundary | `tools/summarizer/Dockerfile:14-16`: copies the Linux lock, upgrades bootstrap `pip setuptools wheel`, then installs with `--require-hashes` and the PyTorch CPU extra index. Runtime switches to user `app` at lines 18-22. | Explains the `+cpu` pin's external index. This lock-only advisory audit excludes bootstrap version drift, base image, native OS/OCR libraries and actual built/deployed identities. |
Inspected source SHA-256 values:
| File | SHA-256 |
| --- | --- |
| `tools/summarizer/app.py` | `a721a7633f7ed93b7353f2135536a168e096494629506634c96b456b7ae713c2` |
| `tools/summarizer/parser_child.py` | `0074a05e9a0d511e26a329a057f98568cdc1723b2cc37168e0b59975b5424451` |
| `tools/summarizer/Dockerfile` | `d0b8777e81a74ed7099fe1d93f608b6601da39efc43b50ec0b3b058613207443` |
## Required next decision
The owner must decide a separately scoped Python/ML/parser dependency remediation with compatible lock generation, model-loading/inference validation and representative benign document parsing/regression checks, or explicitly retain the open gate. A major ML/parser upgrade is not safe to perform automatically inside this requalification checkpoint. Existing controls and passing mocked/unit tests do not clear these advisory records; there is no current Python security PASS.
@@ -0,0 +1,20 @@
# RD21 closure map and remaining limits
This map evaluates the unchanged [six original closure clauses](../p9-04/RD21_CONCURRENCY_ISSUE.md#required-closure-evidence), rather than inventing a broader fix. Historical baseline: `549b2cba5a791862ba3f854312ea819a86dc42a0`. Source/test inspection only in this register; fresh execution must be bound to the root-owned result ledger before final acceptance.
The [SQLite remediation addendum](../rd21-remediation/ACCEPTANCE_ADDENDUM_2026-10-23.md) recorded a repaired implementation but withheld RD21 because MariaDB had not run. The later [MariaDB acceptance](../../../database/mariadb-standardisation/ACCEPTANCE.md) explicitly resolves the bounded MariaDB12.2.2 save/concurrency scope using32 real-provider cases and1 native browser journey. [Historical raw case index](../../../database/mariadb-standardisation/validation.json) retains actual reports/failures. These are historical results, not fresh P9-REQUAL-01 passes. Neither old report is edited.
| Original required closure clause | Existing concrete cases and source | Evidence boundary / remaining limit |
|---|---|---|
| Independent concurrent clients editing different and same fields cannot silently overwrite an acknowledged save | [MariaDB Career tests](../../../../JobTrackerApi.MariaDb.Tests/Rd21CareerPersistenceTests.cs): `Independent_stale_snapshot_cannot_replace_acknowledged_career_edit`; [job tests](../../../../JobTrackerApi.MariaDb.Tests/Rd21JobPersistenceTests.cs): `Stale_or_unversioned_snapshot_cannot_erase_an_acknowledged_job_edit`, `Related_text_save_cannot_silently_replace_a_newer_acknowledged_draft`, `Structured_tailored_cv_snapshot_cannot_replace_a_newer_approved_document`, `Write_that_overlaps_a_delayed_save_wins_without_partial_events_or_opportunity_changes`. Parallel SQLite names in JobTrackerApi.Tests. [Native A/B ordering](../rd21-remediation/diagnostics/persistence.spec.ts): original two-session ordering case preserves each winner through immediate independent SQL reads, navigation and a new authenticated session. | Historical qualified same-owner source snapshots and database interleavings; not exhaustive every-field/editor/order testing. Rejection is safe conflict, not silent merge. Native MariaDB journey directly covers Career, while job provider behavior is covered by real-provider controller/persistence cases. |
| Actual owner-and-revision preconditions on database write; simultaneous submissions and first saves | Career `Stale_tracked_entity_is_guarded_at_database_update_and_stale_restore_is_rejected`, `Competing_first_save_sources_have_one_acknowledged_winner`; job delayed-save overlap and `Versioned_job_save_cannot_cross_tenant_job_or_company_ownership`. [Save contract](../rd21-remediation/SAVE_CONTRACT.md) identifies Career Version and job EF token; final database predicate guards the interval after controller validation. | Real provider contexts/connections, not a client-only comparison. Narrow lifecycle/background tracked writers advance job version. This does not make every narrow command ordering a full-editor merge contract. First canonical save is distinct from create-job intent idempotency. |
| SQLite and supported MariaDB runtime, including constraints, held readers/writers and partial failure | [SQLite Career](../../../../JobTrackerApi.Tests/Rd21CareerPersistenceTests.cs) / [SQLite job](../../../../JobTrackerApi.Tests/Rd21JobPersistenceTests.cs). [MariaDB startup/lock](../../../../JobTrackerApi.MariaDb.Tests/StartupAndLockTests.cs): `Full_startup_creates_every_table_and_column_required_by_the_persisted_EF_model`, `Fresh_repeated_and_populated_startup_preserve_owned_data_and_complete_migrations`, `Rd21_additive_migration_preserves_existing_job_fields_and_backfills_revision_one`, `Open_reader_does_not_block_independent_usage_and_identity_writes`. Job `Held_writer_timeout_rolls_back_and_a_reviewed_save_recovers_after_release`; Career `Identity_failure_rolls_back_canonical_children_history_and_projection`, `Database_failure_after_deletion_restores_children_history_and_revision_and_clears_tracker`; job `Failed_job_transaction_cannot_acknowledge_or_leave_partial_data_for_a_later_save`. | Historical actual pinned Linux/amd64 MariaDB12.2.2, InnoDB and independent TCP readbacks. Identity/summarizer doubles in controller tests are disclosed. Version parity does not prove exact production image/configuration parity. Not MariaDB12.3, power-loss durability or production rollback. Hosted prerequisite failure remains separate from local provider proof; fresh hosted lane pending root results. |
| Conflicts preserve drafts and offer explicit current-data review/reload; account switch and stale session fail closed | [MariaDB browser](../../../../job-tracker-ui/mariadb-tests/save-recovery.spec.ts): native Career conflict/network-failure/reviewed-retry case; [remediation browser](../rd21-remediation/diagnostics/persistence.spec.ts): native editor retention/recovery, missing-revision rejection, cross-origin committed header and successive Answers, fresh source after letter save. [Account isolation](../../../../job-tracker-ui/e2e/account-isolation.spec.ts): retained A request with B cookies/CSRF; missing/forged contexts; two-tab account switch and Back navigation. | Controlled aborted request, actual API/SQL reads and session-boundary tests. Not physical device/reader acceptance. Unknown commit outcome requires deliberate read/review, never fetching a new token to authorize an old body. No automatic retry is claimed. |
| Imports, history restores, generic job updates, lifecycle/bulk and old callers follow agreed compatibility contract | Career `Replacement_restore_parse_and_import_fail_closed_without_valid_revision`, `Import_acceptance_requires_the_revision_shown_in_its_diff`, `Reviewed_import_commits_or_rolls_back_canonical_projection_and_application_metadata`, `Parsing_commits_or_rolls_back_its_entire_import_application`; job missing/stale snapshot and related-text/tailored tests above. [SAVE_CONTRACT](../rd21-remediation/SAVE_CONTRACT.md) inventories endpoints/statuses; [related review](../rd21-remediation/RELATED_REVIEW.md) inventories narrow lifecycle/background writers, extension/create consumers and already-guarded Dashboard/CV/email owners. [MariaDB fixture delta](../../../database/mariadb-standardisation/fixture-delta.json) retains old unversioned failure and explicit3 fixture repairs with original assertions. | Source/caller review plus contract tests, not certification of every independent editor or mixed old/new deployment. Career missing precondition400; valid stale409; job missing/invalid/stale409. Old unversioned writers intentionally fail closed. Extension/bookmarklet is a create consumer. No compatibility alias removal. |
| Complete affected regression, frozen bytes and no unrelated Identity replay | Historical addendum reports1109 backend,934 frontend/105 suites,362 ordinary,23 separate,56 sidecar and6 remediation cases, separate conforming volume3; explicit obsolete historical diagnostic failures retained. Later MariaDB acceptance reports1123 backend,32 provider,1 provider-browser, corrected diagnostics18 andvolume3. Frozen contracts and canonical/variant/null-as-inherit ownership remain independently mapped in [preservation](PRESERVATION.md); original PDF oracle16/36 pages plus8/24 pages unchanged. Career transaction/Identity failure and read-only coherent snapshot tests cover projection boundary; identity-only endpoint keeps ownership. | These are separate historical lanes, not added totals or fresh full regression. Root must obtain complete fresh successors and frozen-byte/PDF proof on final source. Do not treat database conflict pass as frozen-byte proof, print acceptance or release acceptance. |
Creation-intent idempotency is explicitly a **separate retained RD21 concern**, not one of the stale-replacement repairs. No new duplicate-create reproduction or idempotency implementation is claimed. Record owner/domain follow-up with interrupted-create/replay semantics and a separate acceptance contract before changing it. Canonical first-save uniqueness is not create-intent idempotency.
Historical acknowledged edits lost before repair are not proven recoverable; no live records were inspected. Extraction discard/accept lifecycle concurrency was considered but not reproduced. Independent settings/company/contact records were source-reviewed without a newly reproduced shared RD21 defect; full multi-session acceptance of them is not implied. [RELATED_REVIEW](../rd21-remediation/RELATED_REVIEW.md) preserves these boundaries.
Current recommendation is therefore conditional on the exact decision: retain **bounded save/concurrency closure from historical MariaDB evidence**, require fresh source-bound successors for this requalification, retain separate idempotency and release limits, and let the root acceptance addendum issue the final RD21/Phase9/Phase10/production decisions. Do not reopen repaired stale loss merely because creation debt remains, and do not close creation debt merely because stale loss is repaired.
+11 -1
View File
@@ -10,6 +10,16 @@ or proof of application reachability. No production package, service or image wa
| P9-REQUAL-SEC01 | MimeKit4.17.0 resolves Pkcs8.0.1; high and moderate denial-of-service advisories | Direct Pkcs8.0.2 security floor, preserving supported8.x API; refresh API and both test-project locks | Live API NuGet graph reports no vulnerable packages |
| P9-REQUAL-SEC02 | Next16.3.8 matches critical/high advisories | Pin Next and matching ESLint config16.4.1; matching Next compiler/environment packages change together | Live npm policy passes, zero reported vulnerabilities |
| P9-REQUAL-SEC03 | Dev-only ESLint hooks chain resolves Zod4.5.4 | Override4.6.6, within existing compatible4.x range | Live npm policy passes; no exception bypass |
| P9-REQUAL-SEC04 | Sidecar Linux hash lock contains reported advisories across eight packages, including upload parsers and ML dependencies | No broad parser/ML transition attempted; separately validated remediation required | OPEN; exact-pin advisory coverage also INCOMPLETE |
The additional [Python review](PYTHON_SECURITY.md) queries all49 exact Linux lock pins. Its
[machine-readable evidence](python-advisories.json) records98 queries,97 successful responses and
one PyPI404 for the vendor-local `torch==2.6.0+cpu` release; the exact OSV query succeeds. The249
provider records group into109 sets of published aliases across eight packages. These are not109
independent CVEs or reproduced exploits. Published records include critical/high labels. The
untrusted-upload path reaches the PDF/image libraries; existing limits do not close these findings.
Major parser/ML compatibility, model integrity and complete release validation are separate work.
The sidecar source, hash lock and configured model remain unchanged. Overall security is **OPEN**.
The current static export does not run the Next server's image optimizer, Server Actions or
`use cache`. Existing Sharp0.35.5 also satisfies the published image-optimizer workaround. Those
@@ -33,7 +43,7 @@ are retained as superseded evidence rather than added to these totals.
Existing offline supply-chain/evaluator/browser-runtime/ingress/SMTP/dependency-policy/extension
guards pass. Tracked-secret scanning and SBOM generation pass. Those checks are narrower than an
application security certification. The inherited temporary dev-advisory policy expires on
application security certification or a clean Python advisory gate. The inherited temporary dev-advisory policy expires on
1November2026UTC; it was neither broadened nor extended, and the current npm audit uses no exception.
Production still requires independent ingress, deployed identity, storage/recovery, provider,
monitoring and human release qualifications.
@@ -52,16 +52,42 @@
"addedPushBranch": "fix/p9-ci-and-requalification",
"originalPushBranchRetained": true,
"unconditionalIsolationBlockBeforeDocker": true,
"isolationBlockIsFirstStep": true,
"variableBypass": false,
"nativeServicesAdded": false,
"runnerConfigurationChanged": false,
"deploymentWorkflowsChanged": false,
"hostedRunStartedByInvestigation": false
},
"freshHostedBlockerVerification": {
"commit": "75cd04cfc2da0837b556bdcf1424846349c4c73d",
"runId": 847,
"jobId": 1737,
"runnerId": 5,
"runnerName": "Live-Runner",
"status": "completed",
"conclusion": "failure",
"blockedMessageUtc": "2026-10-24T06:56:14Z",
"firstStep": "Require verified runner isolation",
"explicitBlockedMessage": true,
"exitCode": 1,
"checkoutExecuted": false,
"sdkSetupExecuted": false,
"repositoryTestsExecuted": false,
"workflowDockerComposeCommandsExecuted": false,
"mariaDbServiceStarted": false,
"integrationArtifactUploaded": false,
"runnerPreparationCaveat": "Runner created its job container with existing host socket/network and prefetched actions before the first step; preflight cannot prevent runner preparation",
"jobContainerAndWorkspaceRemovalLogged": true,
"privateServiceLifecycleProven": false,
"qualification": "Expected fail-closed blocker verified; hosted MariaDB remains BLOCKED"
},
"futureImage": "mariadb:12.2.2@sha256:42a27baaba3e8f3f0159d16381d5a7156661aa91dcc65f663cabb6a6f9503c00",
"nextAction": "Infrastructure owner supplies dedicated-runner identity, effective private network/socket suppression and complete lifecycle proof; repository owner reviews transport/workflow before enabling execution",
"rawEvidenceSha256": {
".artifacts/mariadb/requal-ci-api.json": "3369a245165a40e20553f633ff7751f14bc8396d0523c379f658adff16f863ac",
".artifacts/mariadb/requal-ci-api.json": "1ef25fa5433e8151d3ac6c6b368ba0212410cfca8033e2fb9620d5e3c33d8bc0",
".artifacts/mariadb/requal-ci-api-847.json": "9ed3863cd897c52bcc87d09956464813d16c36e744d465a8cfc41bd6a4327db9",
".artifacts/mariadb/hosted-ci-847.log": "251b863b10d0ec3d8a94103367ae41d5d259cc7cf6e103870d219b4910545c68",
".artifacts/mariadb/hosted-ci-843.log": "47ba05d12f08eb7a16a5649f71dde18cd582176a2bed818f3e045932ec025891",
".artifacts/mariadb/hosted-ci-844.log": "e03d82a3d24329482b9acd66866bdc500244a85af89932691d09f00d881fdaf9",
".artifacts/mariadb/act-runner-v0.4.1/go.mod": "9863a6049f423c537b02caf6307c8bb6c7dcfe809fb314193b1856274a160824",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,141 @@
"""Disposable local Linux renderer; no host daemon is exposed to the API container."""
import datetime, json, os, re, subprocess, sys, time, urllib.request, uuid
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / '.artifacts/p9-requal-01/linux'
IMAGE = 'jobtracker-p9-requal-api:security-check'
DB = 'jobtracker-it-p9-requal-mariadb-test-1'
PIN = 'mariadb:12.2.2@sha256:42a27baaba3e8f3f0159d16381d5a7156661aa91dcc65f663cabb6a6f9503c00'
VERSION = 'p9-requal-linux-780b9bc'
def guard_schema(name):
if not re.fullmatch(r'jt_it_[0-9a-f]{32}', name):
raise ValueError('Only a generated qualification schema is allowed.')
return name
def command(args, **kwargs):
return subprocess.run(['rtk', 'proxy', *args], check=True, capture_output=True, text=True, encoding='utf-8', **kwargs).stdout.strip()
def sql(query):
return command(['docker', 'exec', '-e', 'MYSQL_PWD=jt-integration-only', DB,
'mariadb', '-uroot', '--batch', '--skip-column-names', '-e', query])
def safe_error(error):
# CalledProcessError includes command arguments, including development-only credentials.
return str(error).replace('jt-integration-only','[test-password]').replace('E2ePassword123!','[test-password]').replace('e2e-only-key-at-least-thirty-two-characters-long','[test-key]')
def main():
if '--self-test' in sys.argv:
assert guard_schema('jt_it_'+'a'*32)
for name in ['jobtracker', 'jt_it_abc', 'jt_it_'+'a'*32+';DROP DATABASE x']:
try: guard_schema(name)
except ValueError: continue
raise AssertionError(name)
print('Linux qualification schema guards: PASS')
return
OUT.mkdir(parents=True, exist_ok=True)
if (OUT/'process.json').exists() or (OUT.parent/'linux-renderer.json').exists():
raise ValueError('Retain prior evidence; select a fresh run directory.')
owned = json.loads(command(['docker','inspect',DB]))[0]
assert owned['Config']['Image'] == PIN
assert owned['Config']['Labels']['com.docker.compose.project'] == 'jobtracker-it-p9-requal'
assert owned['Config']['Labels']['com.docker.compose.service'] == 'mariadb-test'
assert '/var/lib/mysql' in owned['HostConfig'].get('Tmpfs',{})
assert not owned['HostConfig'].get('Privileged') and owned['HostConfig'].get('NetworkMode')!='host'
assert owned['State']['Running'] and owned['State']['Health']['Status']=='healthy'
assert owned['HostConfig']['PortBindings']['3306/tcp'] == [{'HostIp':'127.0.0.1','HostPort':'33308'}]
actual = sql('SELECT VERSION()')
assert actual.startswith('12.2.2-MariaDB'), actual
name=guard_schema('jt_it_'+uuid.uuid4().hex)
suffix=uuid.uuid4().hex[:12]
api='jobtracker-p9-requal-linux-'+suffix
network='jobtracker-p9-requal-linux-'+suffix
record={'startedUtc':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'schema':name,
'serverVersion':actual,'imageTag':IMAGE,'apiContainer':api,'network':network,'cleanup':{},'exitCode':1}
created=attached=network_created=api_created=False
try:
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='{name}'")=='0'
sql(f'CREATE DATABASE `{name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')
created=True
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='{name}'")=='0'
record['imageId']=command(['docker','image','inspect','--format','{{.Id}}',IMAGE])
record['chromium']=command(['docker','run','--rm','--network','none','--entrypoint','/usr/bin/chromium',IMAGE,'--version'])
record['runtime']=command(['docker','run','--rm','--network','none','--entrypoint','dotnet',IMAGE,'--info'])
command(['docker','network','create','--label','task=p9-requal-01',network]); network_created=True
command(['docker','network','connect','--alias','mariadb',network,DB]); attached=True
variables={
'ASPNETCORE_ENVIRONMENT':'Development','Database__Provider':'mariadb','Database__ServerVersion':'12.2.2',
'ConnectionStrings__JobTracker':f'Server=mariadb;Port=3306;Database={name};User ID=root;Password=jt-integration-only;SslMode=None',
'Auth__Require':'true','Auth__JwtKey':'e2e-only-key-at-least-thirty-two-characters-long',
'Auth__AdminEmail':'e2e@example.test','Auth__AdminPassword':'E2ePassword123!', 'Auth__AllowRegistration':'false',
'RateLimits__AuthLoginPermitLimit':'1000','Cors__Origins__0':'http://localhost:3300',
'Workers__AiOperationsEnabled':'false','Workers__SavedJobSearchAlertsEnabled':'false',
'HttpsRedirection__Enabled':'false','App__Version':VERSION,'App__CommitSha':'780b9bc',
'Data__Root':'/data','Logging__LogLevel__Default':'Warning'}
args=['docker','create','--name',api,'--label','task=p9-requal-01','--network',network,
'-p','127.0.0.1:5302:8080','--tmpfs','/data:rw,nosuid,size=512m']
for key,value in variables.items(): args+=['-e',key+'='+value]
command(args+[IMAGE]); api_created=True
assert command(['docker','inspect','--format','{{.Image}}',api])==record['imageId']
command(['docker','start',api])
deadline=time.monotonic()+120
while True:
try:
with urllib.request.urlopen('http://localhost:5302/health',timeout=3) as response:
health=json.load(response)
assert health['version']==VERSION, health
with urllib.request.urlopen('http://localhost:5302/ready',timeout=3) as response:
assert json.load(response)['status']=='ready'
break
except Exception:
if time.monotonic()>=deadline: raise
time.sleep(1)
record['health']=health
record['migrationCount']=int(sql(f'SELECT COUNT(*) FROM `{name}`.`__EFMigrationsHistory`'))
assert record['migrationCount']>0
env={**os.environ,'P9_REQUAL_LINUX_READY':VERSION,'PLAYWRIGHT_STATIC_EXPORT':'true',
'PLAYWRIGHT_PREBUILT_EXPORT':'true','NEXT_PUBLIC_API_BASE_URL':'http://localhost:5302/api',
'CV_FIDELITY_OUTPUT':str(OUT/'fidelity'),'CV_SETTINGS_FIDELITY_OUTPUT':str(OUT/'settings-fidelity'),
'PLAYWRIGHT_JSON_OUTPUT_NAME':str(OUT.parent/'linux-renderer.json')}
test=['node','node_modules/@playwright/test/cli.js','test',
'--config=../docs/redesign/phase-9/p9-requal-01/linux/playwright.config.ts',
'--retries=0','--reporter=list,json','--output='+str(OUT/'test-results')]
with (OUT/'playwright.log').open('w',encoding='utf-8') as log:
record['browserExitCode']=subprocess.run(['rtk','proxy',*test],cwd=ROOT/'job-tracker-ui',env=env,stdout=log,stderr=subprocess.STDOUT).returncode
record['exitCode']=record['browserExitCode']
record['migrationCountAfter']=int(sql(f'SELECT COUNT(*) FROM `{name}`.`__EFMigrationsHistory`'))
assert record['migrationCountAfter']==record['migrationCount']
except Exception as error:
record['exitCode']=1
record['error']=safe_error(error)
finally:
if api_created:
try:
(OUT/'api.log').write_text(safe_error(command(['docker','logs',api])),encoding='utf-8')
except Exception as error: record['cleanup']['logError']=safe_error(error)
try:
command(['docker','rm','--force',api]); record['cleanup']['apiRemoved']=True
except Exception as error: record['cleanup']['apiError']=str(error)
if attached:
try: command(['docker','network','disconnect',network,DB]); record['cleanup']['databaseDisconnected']=True
except Exception as error: record['cleanup']['disconnectError']=str(error)
if network_created:
try: command(['docker','network','rm',network]); record['cleanup']['networkRemoved']=True
except Exception as error: record['cleanup']['networkError']=str(error)
if created and (not api_created or record['cleanup'].get('apiRemoved')):
try:
guard_schema(name); sql(f'DROP DATABASE `{name}`')
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='{name}'")=='0'
record['cleanup']['schemaRemoved']=True
except Exception as error: record['cleanup']['schemaError']=str(error)
elif created:
record['cleanup']['schemaError']='Schema retained because API removal was not confirmed.'
if any(key.endswith('Error') for key in record['cleanup']): record['exitCode']=1
record['finishedUtc']=datetime.datetime.now(datetime.timezone.utc).isoformat()
(OUT/'process.json').write_text(json.dumps(record,indent=2)+'\n',encoding='utf-8')
(OUT.parent/'linux-renderer-exit.json').write_text(json.dumps(record,indent=2)+'\n',encoding='utf-8')
print(json.dumps(record))
if record['exitCode'] or any(key.endswith('Error') for key in record['cleanup']): raise SystemExit(1)
if __name__=='__main__': main()
@@ -0,0 +1,17 @@
import { defineConfig } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import base from '../../../../../job-tracker-ui/playwright.config';
import path from 'node:path';
if (process.env.P9_REQUAL_LINUX_READY !== 'p9-requal-linux-780b9bc') {
throw new Error('Use the isolated Linux qualification runner and its verified API identity.');
}
const servers = Array.isArray(base.webServer) ? base.webServer : [base.webServer!];
export default defineConfig({ ...base,
testDir: path.resolve('e2e'),
testMatch: ['cv-readiness.spec.ts', 'cv-settings-presentation.spec.ts'],
grep: /native preview\/PDF fidelity corpus|document controls preserve settings/,
workers: 1, retries: 0,
// The runner starts and identifies the existing Linux renderer; only the UI is managed here.
webServer: [{ ...servers[1], reuseExistingServer: false }],
metadata: { linuxApiIdentity: process.env.P9_REQUAL_LINUX_READY, mariaDbVersion: '12.2.2' },
});
@@ -0,0 +1,229 @@
"""Explicit isolated MariaDB browser lanes; self-check never touches a server."""
import argparse
import base64
import datetime
import hashlib
import json
import os
from pathlib import Path
import re
import socket
import subprocess
import sys
import time
import uuid
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / '.artifacts/p9-requal-01'
UI = ROOT / 'job-tracker-ui'
CLIENT = ROOT / '.artifacts/mariadb/runtime/mariadb-12.2.2-winx64/bin/mariadb.exe'
CONTAINER = 'jobtracker-it-p9-requal-mariadb-test-1'
IMAGE = 'mariadb:12.2.2@sha256:42a27baaba3e8f3f0159d16381d5a7156661aa91dcc65f663cabb6a6f9503c00'
CONFIGS = {
'mariadb-browser': ('playwright.mariadb.config.ts', 1),
'mariadb-observations': ('../docs/redesign/phase-9/p9-requal-01/observations/playwright.config.ts', 2),
'performance': ('../docs/redesign/phase-9/p9-requal-01/performance/playwright.config.ts', 1),
}
def utc():
return datetime.datetime.now(datetime.timezone.utc).isoformat()
def scrub(value):
if isinstance(value, dict):
return {k: '[REDACTED]' if re.search(r'password|connectionstring', k, re.I) else scrub(v) for k, v in value.items()}
if isinstance(value, list): return [scrub(v) for v in value]
if not isinstance(value, str): return value
value = re.sub(r'Server=127\.0\.0\.1;Port=33308;Database=jt_it_[a-f0-9]{32};User ID=root;Password=[^;\r\n]+;SslMode=None;?', '[REDACTED CONNECTION]', value)
for secret in ('jt-integration-only', 'E2ePassword123!'):
value = value.replace(secret, '[REDACTED]')
return re.sub(r'(Password\s*=\s*)[^;\s]+', r'\1[REDACTED]', value, flags=re.I)
def write(path, value):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(scrub(value), indent=2) + '\n', encoding='utf-8')
def schema_sql(schema, drop=False):
if not re.fullmatch(r'jt_it_[a-f0-9]{32}', schema): raise ValueError('Invalid generated schema')
return f'DROP DATABASE `{schema}`;' if drop else f'CREATE DATABASE `{schema}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
def ownership(data):
labels = data['Config'].get('Labels', {})
ports = data['NetworkSettings']['Ports'].get('3306/tcp', [])
if (data['Name'] != '/' + CONTAINER or not data['State']['Running'] or data['Config']['Image'] != IMAGE
or labels.get('com.docker.compose.project') != 'jobtracker-it-p9-requal'
or labels.get('com.docker.compose.service') != 'mariadb-test'
or ports != [{'HostIp': '127.0.0.1', 'HostPort': '33308'}]
or data['HostConfig'].get('Privileged') or data['HostConfig'].get('NetworkMode') == 'host'
or any(mount.get('Destination') == '/var/lib/mysql' and mount.get('Type') != 'tmpfs' for mount in data.get('Mounts', []))
or '/var/lib/mysql' not in data['HostConfig'].get('Tmpfs', {})):
raise ValueError('Exact task-owned pinned loopback disposable container proof required')
return {'name': CONTAINER, 'id': data['Id'], 'imageReference': IMAGE, 'imageId': data['Image'],
'project': labels['com.docker.compose.project'], 'service': 'mariadb-test', 'port': 33308,
'bindHost': '127.0.0.1', 'disposableData': 'tmpfs', 'privileged': False}
def cases(report):
result = []
def walk(suite):
for spec in suite.get('specs', []):
for test in spec.get('tests', []): result.append(test)
for child in suite.get('suites', []): walk(child)
for suite in report.get('suites', []): walk(suite)
return result
def extract_performance(report, process_code):
tests = cases(report)
attachments = [a for test in tests for run in test.get('results', []) for a in run.get('attachments', [])
if a.get('name') == 'p9-requal-01-mariadb-performance']
if len(attachments) != 1: raise ValueError('Expected exactly one performance evidence attachment')
attachment = attachments[0]
if attachment.get('body'):
evidence = json.loads(base64.b64decode(attachment['body']))
elif attachment.get('path'):
path = Path(attachment['path'])
if not path.is_absolute(): path = UI / path
path = path.resolve()
if not path.is_relative_to(OUT.resolve()): raise ValueError('Attachment outside task artifacts')
evidence = json.loads(path.read_text(encoding='utf-8'))
else: raise ValueError('Performance attachment has no body/path')
passed = process_code == 0 and len(tests) == 1 and not report.get('errors') and tests[0].get('status') == 'expected'
passed = passed and len(tests[0].get('results', [])) == 1 and tests[0]['results'][0].get('status') == 'passed'
passed = passed and evidence.get('phase') == 'complete' and evidence.get('actualDurationMs', 0) >= 900000
return {**evidence, 'status': 'PASS' if passed else 'FAIL', 'processExitCode': process_code,
'attachmentName': 'p9-requal-01-mariadb-performance'}
def self_check():
schema = 'jt_it_' + 'a' * 32
assert schema_sql(schema, True) == f'DROP DATABASE `{schema}`;'
for bad in ('main', 'jt_it_' + 'a' * 31, schema + ';DROP DATABASE main'):
try: schema_sql(bad, True)
except ValueError: pass
else: raise AssertionError('Invalid schema accepted')
fixture = {'Name': '/' + CONTAINER, 'Id': 'test', 'Image': 'sha256:test', 'State': {'Running': True},
'Config': {'Image': IMAGE, 'Labels': {'com.docker.compose.project': 'jobtracker-it-p9-requal', 'com.docker.compose.service': 'mariadb-test'}},
'NetworkSettings': {'Ports': {'3306/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '33308'}]}},
'HostConfig': {'Privileged': False, 'NetworkMode': 'bridge', 'Tmpfs': {'/var/lib/mysql': ''}}, 'Mounts': []}
assert ownership(fixture)['port'] == 33308
fixture['HostConfig']['NetworkMode'] = 'host'
try: ownership(fixture)
except ValueError: pass
else: raise AssertionError('Host networking accepted')
fixture['HostConfig']['NetworkMode'] = 'bridge'
fixture['Config']['Image'] = 'mariadb:latest'
try: ownership(fixture)
except ValueError: pass
else: raise AssertionError('Unpinned image accepted')
fixture['Config']['Image'] = IMAGE
fixture['NetworkSettings']['Ports']['3306/tcp'][0]['HostIp'] = '0.0.0.0'
try: ownership(fixture)
except ValueError: pass
else: raise AssertionError('Public database binding accepted')
evidence = {'phase': 'complete', 'actualDurationMs': 900000}
result = {'status': 'passed', 'attachments': [{'name': 'p9-requal-01-mariadb-performance', 'body': base64.b64encode(json.dumps(evidence).encode()).decode()}]}
report = {'suites': [{'specs': [{'tests': [{'status': 'expected', 'results': [result]}]}]}]}
assert extract_performance(report, 0)['status'] == 'PASS'
assert extract_performance(report, 1)['status'] == 'FAIL'
assert 'jt-integration-only' not in scrub('Server=127.0.0.1;Port=33308;Database=' + schema + ';User ID=root;Password=jt-integration-only;SslMode=None')
print('PASS: schema ownership/drop guards, pinned container/network guards, performance extraction/status and redaction; no services touched')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('mode', choices=list(CONFIGS) + ['self-check'])
args = parser.parse_args()
if args.mode == 'self-check': self_check(); return 0
lane = args.mode
config, expected = CONFIGS[lane]
report_path = OUT / (lane + '.json')
exit_path = OUT / (lane + '-exit.json')
results_path = OUT / (lane + '-results')
if any(path.exists() for path in (report_path, exit_path, results_path)) or (lane == 'performance' and (OUT / 'performance/result.json').exists()):
raise ValueError('Existing evidence present; archive it explicitly before a new invocation')
OUT.mkdir(parents=True, exist_ok=True)
env = {**os.environ, 'MYSQL_PWD': 'jt-integration-only', 'DOTNET_ROOT': 'C:/Users/Connor/.dotnet',
'DOTNET_HOST_PATH': 'C:/Users/Connor/.dotnet/dotnet.exe', 'PLAYWRIGHT_STATIC_EXPORT': 'true',
'PLAYWRIGHT_PREBUILT_EXPORT': 'true', 'PLAYWRIGHT_REUSE_SERVERS': 'false',
'PLAYWRIGHT_JSON_OUTPUT_NAME': str(report_path), 'JOBTRACKER_MARIADB_BROWSER_PORT': '33308',
'JOBTRACKER_MARIADB_CLIENT': str(CLIENT), 'NEXT_PUBLIC_API_BASE_URL': 'http://localhost:5302/api',
'SKIP_MODEL_LOAD': '1', 'AI_SERVICE_SKIP_MODEL_LOAD': '1', 'PYTHONIOENCODING': 'utf-8'}
schema = 'jt_it_' + uuid.uuid4().hex
created = False
process = None
record = {'lane': lane, 'startedUtc': utc(), 'generatedSchema': schema, 'commands': [], 'exitCode': 1, 'cleanup': {'attempted': False}}
started = time.monotonic()
def execute(command, name):
completed = subprocess.run(['rtk', 'proxy', *command], cwd=ROOT, env=env, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=30)
log = OUT / (lane + '-' + name + '.log')
log.write_text(scrub(completed.stdout + completed.stderr), encoding='utf-8')
record['commands'].append({'operation': name, 'exitCode': completed.returncode, 'log': str(log.relative_to(ROOT))})
if completed.returncode: raise RuntimeError(name + ' failed; inspect sanitized operation log')
return completed.stdout.strip()
def sql(query, name):
return execute([str(CLIENT), '--host=127.0.0.1', '--port=33308', '--user=root', '--batch', '--raw', '--skip-column-names', '--execute=' + query], name)
try:
for port in (5302, 3300):
for family, address in ((socket.AF_INET, '127.0.0.1'), (socket.AF_INET6, '::1')):
with socket.socket(family) as probe:
probe.settimeout(0.2)
if probe.connect_ex((address, port)) == 0: raise ValueError(f'Port {port} occupied; exclusive lane required')
# Inspect only this exact task container; full inspection stays in memory, never a report/log.
inspected = subprocess.run(['rtk', 'proxy', 'docker', 'inspect', CONTAINER], capture_output=True, text=True, timeout=20)
if inspected.returncode: raise ValueError('Task container inspection unavailable')
record['ownership'] = ownership(json.loads(inspected.stdout)[0])
version = sql('SELECT VERSION();', 'version')
record['serverVersion'] = version
if not re.fullmatch(r'12\.2\.2-MariaDB(?:-[A-Za-z0-9.]+)?', version): raise ValueError('Unexpected actual server version')
sql(schema_sql(schema), 'create-schema')
created = True
env['JOBTRACKER_MARIADB_BROWSER_CONNECTION'] = f'Server=127.0.0.1;Port=33308;Database={schema};User ID=root;Password=jt-integration-only;SslMode=None'
command = ['node', 'node_modules/@playwright/test/cli.js', 'test', '--config=' + config, '--retries=0', '--reporter=list,json', '--output=' + str(results_path)]
record['command'] = command
with (OUT / (lane + '.log')).open('w', encoding='utf-8') as log:
process = subprocess.Popen(['rtk', 'proxy', *command], cwd=UI, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace')
record['processId'] = process.pid
for line in process.stdout: log.write(scrub(line)); log.flush()
record['exitCode'] = process.wait()
report = json.loads(report_path.read_text(encoding='utf-8'))
write(report_path, report)
tests = cases(report)
record['caseCount'] = len(tests)
if len(tests) != expected or any(test.get('status') != 'expected' or len(test.get('results', [])) != 1 or test['results'][0].get('status') != 'passed' for test in tests) or report.get('errors'):
record['exitCode'] = record['exitCode'] or 1
if lane == 'performance': write(OUT / 'performance/result.json', extract_performance(report, record['exitCode']))
except Exception as error:
record['error'] = scrub(type(error).__name__ + ': ' + str(error))
record['exitCode'] = record['exitCode'] or 1
if lane == 'performance' and not (OUT / 'performance/result.json').exists():
write(OUT / 'performance/result.json', {'status': 'FAIL', 'reason': record['error'], 'processExitCode': record['exitCode']})
finally:
# Stop only this invocation's process tree before dropping its schema after an interrupted run.
if process is not None and process.poll() is None:
stopped = subprocess.run(['rtk', 'proxy', 'taskkill', '/PID', str(process.pid), '/T', '/F'], capture_output=True, text=True, timeout=20)
record['processTreeStopExitCode'] = stopped.returncode
try: process.wait(timeout=20)
except subprocess.TimeoutExpired:
created = False
record['cleanup']['error'] = 'Owned process tree still running; schema retained for operator cleanup'
record['exitCode'] = record['exitCode'] or 1
if created:
record['cleanup']['attempted'] = True
try:
sql(schema_sql(schema, True), 'drop-schema')
remaining = sql(f"SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='{schema}';", 'verify-drop')
if remaining != '0': raise ValueError('Generated schema remains after cleanup')
record['cleanup'].update(exitCode=0, schemaAbsent=True)
except Exception as error:
record['cleanup'].update(exitCode=1, error=scrub(str(error)))
record['exitCode'] = record['exitCode'] or 1
record.update(finishedUtc=utc(), durationSeconds=time.monotonic() - started,
helperSha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
if lane == 'performance' and (OUT / 'performance/result.json').exists():
result = json.loads((OUT / 'performance/result.json').read_text())
if record['exitCode']: result['status'] = 'FAIL'
result['cleanup'] = record['cleanup']
write(OUT / 'performance/result.json', result)
write(exit_path, record)
print(json.dumps({'lane': lane, 'exitCode': record['exitCode'], 'processRecord': str(exit_path)}))
return record['exitCode']
if __name__ == '__main__':
try: sys.exit(main())
except ValueError as error: raise SystemExit(scrub(str(error)))
@@ -0,0 +1,77 @@
import { test, expect, type APIRequestContext } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import { sql } from '../performance/mariadb-proof.mjs';
const api='http://localhost:5302/api';
const password='E2ePassword123!';
async function login(request: APIRequestContext, email: string) {
expect((await request.post(`${api}/auth/login`,{data:{email,password}})).status()).toBe(200);
const me=await (await request.get(`${api}/auth/me`)).json();
return {id:me.id as string, headers:{'X-CSRF-TOKEN':(await request.storageState()).cookies.find(c=>c.name==='XSRF-TOKEN')!.value,
'X-Session-Context':me.sessionContext as string}};
}
async function seed(admin: APIRequestContext, request: APIRequestContext) {
const administrator=await login(admin,'e2e@example.test');
const email=`requal-observation-${Date.now()}@example.test`;
expect((await admin.post(`${api}/users`,{headers:administrator.headers,data:{email,password,roles:[]}})).status()).toBe(200);
const owner=await login(request,email);
const company=await request.post(`${api}/companies`,{headers:owner.headers,data:{name:'Requalification synthetic company'}});
expect(company.status()).toBe(201);
return {owner,email,companyId:(await company.json()).id as number};
}
async function stored(id: number) {
expect(Number.isSafeInteger(id)&&id>0).toBe(true);
return JSON.parse(await sql(`SELECT JSON_OBJECT('id',a.Id,'version',a.Version,'title',a.JobTitle,'notes',a.Notes,
'opportunityTitle',j.JobTitle,'events',(SELECT COUNT(*) FROM JobEvents e WHERE e.JobApplicationId=a.Id))
FROM JobApplications a JOIN Jobs j ON j.Id=a.JobId WHERE a.Id=${id}`));
}
test('MariaDB real HTTP job saves reject stale and unversioned drafts; independent SQL and a new session retain the acknowledged winner',async({playwright},info)=>{
const contexts=await Promise.all(Array.from({length:4},()=>playwright.request.newContext()));
const [admin,first,second,fresh]=contexts;
try {
expect(await sql('SELECT VERSION()')).toMatch(/^12\.2\.2-MariaDB(?:-|$)/);
const {owner,email,companyId}=await seed(admin,first);
const otherSession=await login(second,email);
const created=await first.post(`${api}/jobapplications`,{headers:owner.headers,data:{companyId,jobTitle:'Original title',notes:'Original notes',status:'Prospect'}});
expect(created.status()).toBe(201);
const id=(await created.json()).id as number;
const a=await (await first.get(`${api}/jobapplications/${id}`)).json();
const b=await (await second.get(`${api}/jobapplications/${id}`)).json();
expect(b).toEqual(a);
const saved=await first.put(`${api}/jobapplications/${id}`,{headers:owner.headers,data:{...a,jobTitle:'Acknowledged winner',expectedVersion:a.version}});
expect(saved.status()).toBe(204);
const version=Number(saved.headers()['x-job-version']);
expect(version).toBe(a.version+1);
const acknowledged=await stored(id);
expect(acknowledged).toMatchObject({title:'Acknowledged winner',opportunityTitle:'Acknowledged winner',notes:'Original notes',version});
expect((await second.put(`${api}/jobapplications/${id}`,{headers:otherSession.headers,data:{...b,notes:'Rejected stale notes',expectedVersion:b.version}})).status()).toBe(409);
expect(await stored(id)).toEqual(acknowledged);
expect((await second.put(`${api}/jobapplications/${id}`,{headers:otherSession.headers,data:{...b,notes:'Legacy unversioned draft'}})).status()).toBe(409);
expect(await stored(id)).toEqual(acknowledged);
await login(fresh,email);
expect(await (await fresh.get(`${api}/jobapplications/${id}`)).json()).toMatchObject({jobTitle:'Acknowledged winner',notes:'Original notes',version});
const foreign=await seed(admin,admin);
expect((await admin.get(`${api}/jobapplications/${id}`)).status()).toBe(404);
expect((await admin.put(`${api}/jobapplications/${id}`,{headers:foreign.owner.headers,data:{...a,expectedVersion:version}})).status()).toBe(404);
expect(await stored(id)).toEqual(acknowledged);
await info.attach('durable-job-acknowledgement',{body:JSON.stringify({id,acknowledged,stale:409,missing:409,foreign:404,newSession:true}),contentType:'application/json'});
} finally {await Promise.all(contexts.map(context=>context.dispose()));}
});
test('Characterization only: repeated creation bodies create separate persisted records; no creation-intent idempotency guarantee is claimed',async({playwright},info)=>{
const admin=await playwright.request.newContext(); const request=await playwright.request.newContext();
try {
expect(await sql('SELECT VERSION()')).toMatch(/^12\.2\.2-MariaDB(?:-|$)/);
const {owner,companyId}=await seed(admin,request);
const body={companyId,jobTitle:'Identical creation body',notes:'Synthetic intent ambiguity',status:'Prospect'};
const first=await request.post(`${api}/jobapplications`,{headers:owner.headers,data:body});
const second=await request.post(`${api}/jobapplications`,{headers:owner.headers,data:body});
expect(first.status()).toBe(201); expect(second.status()).toBe(201);
const ids=[(await first.json()).id as number,(await second.json()).id as number];
expect(ids[0]).not.toBe(ids[1]);
expect(await stored(ids[0])).toMatchObject({id:ids[0],title:body.jobTitle,notes:body.notes});
expect(await stored(ids[1])).toMatchObject({id:ids[1],title:body.jobTitle,notes:body.notes});
await info.attach('creation-intent-limit',{body:JSON.stringify({ids,statuses:[201,201],classification:'Characterization, not safety acceptance',
limit:'Identical JSON cannot distinguish two intentional creations from a retry after an unknown response. A separately specified creation-intent contract remains required; no silent deduplication was added.'}),contentType:'application/json'});
} finally {await admin.dispose();await request.dispose();}
});
@@ -0,0 +1,4 @@
import { defineConfig } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import base from '../../../../../job-tracker-ui/playwright.mariadb.config';
export default defineConfig({ ...base, testDir: '.', testMatch: 'persistence.spec.ts', workers: 1, retries: 0 });
@@ -0,0 +1,91 @@
"""Stage fresh PDF evidence and run the unchanged oracle after producers stop."""
import argparse
import hashlib
import json
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
ORACLE = ROOT / 'scripts/verify-cv-fidelity.py'
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def merged_measurements(paths, expected):
records = [json.loads(path.read_text(encoding='utf-8-sig')) for path in paths]
versions = {record['browserVersion'] for record in records}
fixtures = [fixture for record in records for fixture in record['fixtures']]
names = [fixture['fixture'] for fixture in fixtures]
if len(versions) != 1 or len(fixtures) != expected or len(set(names)) != expected:
raise ValueError('Unexpected browser identity, fixture count or duplicate fixture names.')
if any(Path(name).name != name or name in ('.', '..') for name in names):
raise ValueError('Fixture name must identify one child directory.')
return {'browserVersion': versions.pop(), 'fixtures': fixtures}
def stage(source, destination, measurements, hashes):
destination.mkdir()
for fixture in measurements['fixtures']:
name = fixture['fixture']
files = [source / name / 'export.pdf', source / name / 'render.html']
files += [source / name / f'preview-{page}.png' for page in range(1, fixture['previewPages'] + 1)]
(destination / name).mkdir()
for path in files:
hashes[str(path.resolve())] = digest(path)
shutil.copy2(path, destination / name / path.name)
(destination / 'measurements.json').write_text(json.dumps(measurements, ensure_ascii=False, indent=2), encoding='utf-8')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--platform', choices=['windows', 'linux'], required=True)
parser.add_argument('--fidelity-root', type=Path, required=True)
parser.add_argument('--settings-root', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True, help='New directory; never overwrites an earlier run.')
parser.add_argument('--record', type=Path, required=True, help='New native-pdf.json or linux-pdf.json record.')
args = parser.parse_args()
if args.output.exists() or args.record.exists():
parser.error('Output directory and result record must both be new.')
inputs = [args.fidelity_root / 'measurements.json']
settings = [args.settings_root / f'{theme}-{language}-measurements.json'
for theme in ['light', 'dark'] for language in ['en', 'nb']]
corpus = merged_measurements(inputs, 16)
setting_corpus = merged_measurements(settings, 8)
expected_settings = {f'{theme}-{language}-{phase}' for theme in ['light', 'dark']
for language in ['en', 'nb'] for phase in ['before', 'after']}
if {fixture['fixture'] for fixture in setting_corpus['fixtures']} != expected_settings:
raise ValueError('Settings fixture membership differs from all eight required states.')
hashes = {str(path.resolve()): digest(path) for path in [ORACLE, *inputs, *settings]}
args.output.mkdir(parents=True)
record = {'status': 'FAIL', 'platform': args.platform, 'oracle': str(ORACLE), 'inputs': hashes, 'lanes': []}
try:
for name, source, measurements, count, pages in [
('fidelity', args.fidelity_root, corpus, 16, 36),
('settings-fidelity', args.settings_root, setting_corpus, 8, 24)]:
destination = args.output / name
stage(source, destination, measurements, hashes)
command = [sys.executable, '-B', str(ORACLE), str(destination)]
process = subprocess.run(command, capture_output=True, text=True, encoding='utf-8', errors='replace')
(destination / 'oracle.log').write_text(process.stdout + process.stderr, encoding='utf-8')
report_path = destination / 'fidelity-results.json'
report = json.loads(report_path.read_text(encoding='utf-8')) if report_path.exists() else {}
native = report.get('fixtures', [])
passed = (process.returncode == 0 and report.get('passed') == count and report.get('total') == count
and len(native) == count and {row['fixture'] for row in native} == {row['fixture'] for row in measurements['fixtures']}
and all(row['pass'] and not row['failures'] for row in native)
and sum(row['pdfPages'] for row in native) == pages)
record['lanes'].append({'lane': name, 'status': 'PASS' if passed else 'FAIL', 'command': command,
'exitCode': process.returncode, 'report': str(report_path),
'reportSha256': digest(report_path) if report_path.exists() else None,
'fixtures': len(native), 'pdfPages': sum(row['pdfPages'] for row in native)})
record['inputDrift'] = [path for path, expected in hashes.items() if digest(Path(path)) != expected]
if not record['inputDrift'] and all(lane['status'] == 'PASS' for lane in record['lanes']):
record['status'] = 'PASS'
except Exception as error:
record['error'] = f'{type(error).__name__}: {error}'
args.record.write_text(json.dumps(record, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'status': record['status'], 'record': str(args.record)}))
raise SystemExit(0 if record['status'] == 'PASS' else 1)
if __name__ == '__main__':
main()
@@ -0,0 +1,29 @@
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { validateMariaDbBrowserConnection } from '../../../../../job-tracker-ui/scripts/mariadb-browser-connection.mjs';
const execute = promisify(execFile);
export async function sql(query) {
const port = process.env.JOBTRACKER_MARIADB_BROWSER_PORT;
const database = validateMariaDbBrowserConnection(process.env.JOBTRACKER_MARIADB_BROWSER_CONNECTION || '', port || '');
if (port !== '33308' || !process.env.JOBTRACKER_MARIADB_CLIENT) throw new Error('Pinned MariaDB port and native client are required.');
const { stdout } = await execute(process.env.JOBTRACKER_MARIADB_CLIENT,
['--host=127.0.0.1', `--port=${port}`, '--user=root', `--database=${database}`, '--batch', '--raw', '--skip-column-names',
`--execute=SET TRANSACTION READ ONLY; START TRANSACTION; ${query}; COMMIT;`],
{ encoding: 'utf8', timeout: 15000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, MYSQL_PWD: 'jt-integration-only' } });
return stdout.trim();
}
export async function snapshot(owner) {
if (!/^[a-zA-Z0-9-]{1,64}$/.test(owner)) throw new Error('Invalid synthetic owner ID.');
return JSON.parse(await sql(`SELECT JSON_OBJECT(
'ids',(SELECT JSON_ARRAYAGG(Id ORDER BY Id) FROM JobApplications WHERE OwnerUserId='${owner}'),
'ownerCount',(SELECT COUNT(*) FROM JobApplications WHERE OwnerUserId='${owner}'),
'deletedCount',(SELECT COUNT(*) FROM JobApplications WHERE OwnerUserId='${owner}' AND IsDeleted=1),
'tenantCounts',(SELECT JSON_ARRAYAGG(JSON_OBJECT('owner',OwnerUserId,'count',n) ORDER BY OwnerUserId)
FROM (SELECT OwnerUserId,COUNT(*) n FROM JobApplications GROUP BY OwnerUserId) counts),
'profile',(SELECT ProfileJson FROM CareerProfiles WHERE OwnerUserId='${owner}'),
'revision',(SELECT Version FROM CareerProfiles WHERE OwnerUserId='${owner}'),
'projection',(SELECT ProfileCvStructureJson FROM AspNetUsers WHERE Id='${owner}'),
'historyCount',(SELECT COUNT(*) FROM CareerProfileVersions WHERE OwnerUserId='${owner}'))`));
}
@@ -0,0 +1,38 @@
import assert from 'node:assert/strict';
import { performance } from 'node:perf_hooks';
import { setTimeout as sleep } from 'node:timers/promises';
export const durationMs = 15 * 60 * 1000;
export const paceMs = 500;
export function summarize(rows) {
const values = rows.filter(row => !row.error && row.status === 200).map(row => row.ms).sort((a, b) => a - b);
const percentile = p => values.length ? values[Math.ceil(values.length * p) - 1] : null;
return { attempts: rows.length, failures: rows.filter(row => row.error || row.status !== 200).length,
p50Ms: percentile(.5), p95Ms: percentile(.95), p99Ms: percentile(.99), maxMs: values.at(-1) ?? null };
}
export async function waitUntil(target) {
while (performance.now() < target) await sleep(Math.min(1000, target - performance.now()));
}
export function assertMembership(actual, expected) {
assert.equal(actual.length, 250);
assert.equal(new Set(actual).size, 250);
assert.deepEqual([...actual].sort((a, b) => a - b), [...expected].sort((a, b) => a - b));
}
if (process.argv.includes('--self-test')) {
assert.equal(durationMs, 900000);
assert.equal(paceMs, 500);
assert.deepEqual(summarize([]), { attempts: 0, failures: 0, p50Ms: null, p95Ms: null, p99Ms: null, maxMs: null });
const rows = Array.from({ length: 100 }, (_, i) => ({ ms: i + 1, status: 200 }));
assert.deepEqual(summarize([...rows, { ms: 999, status: 500 }, { ms: 999, error: 'timeout' }]),
{ attempts: 102, failures: 2, p50Ms: 50, p95Ms: 95, p99Ms: 99, maxMs: 100 });
const ids = Array.from({ length: 250 }, (_, i) => i + 1);
assertMembership(ids.toReversed(), ids);
assert.throws(() => assertMembership([...ids.slice(0, 249), 1], ids));
assert.throws(() => assertMembership([...ids.slice(0, 249), 999], ids));
assert.throws(() => assertMembership(ids.slice(1), ids));
console.log('performance metrics self-check passed');
}
@@ -0,0 +1,14 @@
import { defineConfig } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import base from '../../../../../job-tracker-ui/playwright.mariadb.config';
import path from 'node:path';
if (process.env.JOBTRACKER_MARIADB_BROWSER_PORT !== '33308') throw new Error('Requalification requires pinned loopback port 33308.');
if (process.env.PLAYWRIGHT_STATIC_EXPORT !== 'true' || process.env.PLAYWRIGHT_PREBUILT_EXPORT !== 'true') {
throw new Error('Requalification requires a freshly built production static export.');
}
const artifacts = path.resolve('../.artifacts/p9-requal-01/performance');
export default defineConfig({ ...base, testDir: '.', testMatch: 'sustained.spec.ts', workers: 1, retries: 0,
timeout: 18 * 60 * 1000, outputDir: path.join(artifacts, 'test-results'),
reporter: [['list'], ['json', { outputFile: path.join(artifacts, 'playwright-results.json') }]],
});
@@ -0,0 +1,191 @@
import { test, expect, type APIRequestContext } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import os from 'node:os';
import { performance } from 'node:perf_hooks';
import { durationMs, paceMs, waitUntil, summarize, assertMembership } from './metrics.mjs';
import { snapshot, sql } from './mariadb-proof.mjs';
const api = 'http://localhost:5302/api';
const password = 'E2ePassword123!';
async function login(request: APIRequestContext, email: string) {
expect((await request.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await request.get(`${api}/auth/me`)).json();
return { id: me.id as string, headers: { 'X-CSRF-TOKEN': (await request.storageState()).cookies.find(c => c.name === 'XSRF-TOKEN')!.value,
'X-Session-Context': me.sessionContext as string } };
}
test('P9-REQUAL-01 real MariaDB: 15 minutes, 250 applications, two paced readers and native workspaces', async ({ page, context, playwright }, info) => {
const admin = await playwright.request.newContext();
const readers = await Promise.all([playwright.request.newContext(), playwright.request.newContext()]);
const cdp = await context.newCDPSession(page);
const latency: { worker: number; endpoint: string; ms: number; atMs: number; bytes?: number; status?: number; error?: string }[] = [];
const navigation: { index: number; route: string; atMs: number; readyMs: number; error?: string }[] = [];
const preflightNavigation: typeof navigation = [];
const telemetry: object[] = [];
const ids: number[] = [];
const evidence: Record<string, unknown> = { fixtureJobs: 250, fixtureCareerRoles: 8, readConcurrency: 2, paceMs, requiredDurationMs: durationMs,
database: info.config.metadata.mariaDbDatabase, latency, navigation, preflightNavigation, telemetry, ids, phase: 'setup',
environment: { platform: os.platform(), cpus: os.cpus().length, totalMemoryBytes: os.totalmem(), browser: context.browser()?.version(),
databaseProvider: 'MariaDB', frontend: 'prebuilt production static export' },
limits: 'Bounded local synthetic workload; no SLA, capacity, production, physical-device, model-inference or server CPU attribution claim. Host/Node/CDP telemetry is descriptive.' };
try {
const server = await sql('SELECT VERSION()');
expect(server).toMatch(/^12\.2\.2-MariaDB(?:-|$)/);
evidence.server = server;
const adminOwner = await login(admin, 'e2e@example.test');
const email = `p9-requal-perf-${Date.now()}@example.test`;
// A free synthetic account excludes creation-time model inference from this database/UI workload.
expect((await admin.post(`${api}/users`, { headers: adminOwner.headers, data: { email, password, roles: [] } })).status()).toBe(200);
const owner = await login(page.request, email);
evidence.owner = owner.id;
const company = await page.request.post(`${api}/companies`, { headers: owner.headers, data: { name: 'P9 requalification synthetic workload' } });
expect(company.status()).toBe(201);
const companyId = (await company.json()).id;
for (let index = 0; index < 250; index++) {
const response = await page.request.post(`${api}/jobapplications`, { headers: owner.headers,
data: { companyId, jobTitle: `P9 requalification role ${index}`, status: 'Applied', dateApplied: new Date().toISOString() } });
expect(response.status(), `seed application ${index}`).toBe(201);
ids.push((await response.json()).id);
}
const profile = { version: '1', contact: { fullName: 'Åse Synthetic', email: 'synthetic@example.test' }, summary: ['Documented accessible workflows.'],
jobs: Array.from({ length: 8 }, (_, index) => ({ id: `p9-role-${index}`, title: `Platform engineer ${index}`, company: 'Nordlys synthetic',
start: '2017-01', end: '2025-12', bullets: Array.from({ length: 6 }, (_, bullet) =>
`Evidence ${index}.${bullet}: ${'Documented reliable workflows. Dokumenterte pålitelige arbeidsflyter. '.repeat(3).trim()}`) })) };
const initialProfile = await (await page.request.get(`${api}/career/profile`)).json();
expect((await page.request.put(`${api}/career/profile`, { headers: owner.headers,
data: { expectedVersion: initialProfile.revision, profile } })).status()).toBe(200);
const savedProfile = (await (await page.request.get(`${api}/career/profile`)).json()).profile;
expect(savedProfile.jobs).toHaveLength(8);
expect(savedProfile.jobs.map((job: { start: string; end: string; bullets: string[] }) => ({ start: job.start, end: job.end, bullets: job.bullets })))
.toEqual(profile.jobs.map(job => ({ start: job.start, end: job.end, bullets: job.bullets })));
const cv = await page.request.post(`${api}/cv/variants`, { headers: owner.headers,
data: { name: 'P9 requalification long CV', settings: { themeId: 'minimal', language: 'nb' } } });
expect(cv.status()).toBe(200);
const cvId = (await cv.json()).id;
const before = await snapshot(owner.id);
assertMembership(before.ids, ids);
expect(before.ownerCount).toBe(250); expect(before.deletedCount).toBe(0);
expect(JSON.parse(before.profile)).toEqual(savedProfile);
evidence.before = before;
await Promise.all(readers.map(reader => login(reader, email)));
await page.addInitScript(() => localStorage.setItem('uiLanguage', 'en'));
await context.route('**/*', route => {
const url = new URL(route.request().url());
return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort();
});
await cdp.send('Performance.enable');
const endpoints = ['/jobapplications/stats', '/jobapplications/board', '/career/profile', '/documents'];
const routes = ['/dashboard', '/jobs?view=table', '/jobs?view=board', `/career/builder/${cvId}`];
evidence.phase = 'preflight';
for (const endpoint of endpoints) {
const response = await page.request.get(`${api}${endpoint}`, { timeout: 10000 });
try {
expect(response.status(), `preflight ${endpoint}`).toBe(200);
const body = await response.json();
if (endpoint.endsWith('/board')) assertMembership(body.map((job: { id: number }) => job.id), ids);
else if (endpoint.endsWith('/profile')) expect(body.profile).toEqual(savedProfile);
else if (endpoint.endsWith('/stats')) expect(body.active).toBe(250);
else expect(Array.isArray(body.items)).toBe(true);
} finally { await response.dispose(); }
}
let started = performance.now();
let deadline = 0;
const readWorker = async (worker: number) => {
let index = 0;
while (performance.now() < deadline) {
const endpoint = endpoints[index++ % endpoints.length];
const begin = performance.now();
const row: typeof latency[number] = { worker, endpoint, atMs: begin - started, ms: 0 };
try {
const response = await readers[worker].get(`${api}${endpoint}`, { timeout: 10000 });
try {
row.status = response.status(); row.bytes = (await response.body()).length;
expect(row.status, endpoint).toBe(200);
if (endpoint.endsWith('/board')) assertMembership((await response.json()).map((job: { id: number }) => job.id), ids);
if (endpoint.endsWith('/profile')) expect((await response.json()).profile).toEqual(savedProfile);
} finally { await response.dispose(); }
} catch (error) { row.error = String(error); }
row.ms = performance.now() - begin;
latency.push(row);
// Completion-to-start pacing avoids catch-up bursts after a slow response.
await waitUntil(Math.min(deadline, performance.now() + paceMs));
}
};
const interact = async (preflight = false) => {
for (let index = 0; index < (preflight ? 4 : 20); index++) {
if (!preflight) await waitUntil(started + index * durationMs / 20);
const route = routes[index % routes.length];
const begin = performance.now();
const row: typeof navigation[number] = { index, route, atMs: begin - started, readyMs: 0 };
try {
await page.goto(route, { timeout: 20000 });
if (route === '/dashboard') await expect(page.getByRole('link', { name: 'Tracked jobs: 250', exact: true })).toBeVisible();
else if (route.endsWith('board')) {
await expect(page.locator('[data-kanban-card-id]')).toHaveCount(250);
assertMembership(await page.locator('[data-kanban-card-id]').evaluateAll(elements => elements.map(element => Number(element.getAttribute('data-kanban-card-id')))), ids);
} else if (route.endsWith('table')) await expect(page.locator('[data-job-row-id]').first()).toBeVisible();
else {
await expect(page.getByRole('region', { name: 'Live preview', exact: true })).toHaveAttribute('aria-busy', 'false');
const measured = await page.getByTitle('CV preview', { exact: true }).evaluate(element => {
const doc = (element as HTMLIFrameElement).contentDocument!;
const pages = Number(doc.documentElement.dataset.cvPreviewPages);
const width = Number(doc.documentElement.dataset.cvPreviewWidth);
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
let node; let finalMarkerPage = 0;
while ((node = walker.nextNode())) {
const index = node.textContent?.indexOf('Evidence 7.5:') ?? -1;
if (index < 0) continue;
const range = doc.createRange(); range.setStart(node, index); range.setEnd(node, index + 'Evidence 7.5:'.length);
finalMarkerPage = Math.floor(range.getClientRects()[0].x / width) + 1;
}
return { pages, finalMarkerPage, ready: doc.documentElement.dataset.cvPreviewReady };
});
expect(measured.ready).toBe('true'); expect(measured.pages).toBeGreaterThan(1);
expect(measured.finalMarkerPage).toBe(measured.pages);
await expect(page.locator('[data-cv-page]')).toHaveCount(measured.pages);
await expect(page.getByTitle('CV preview page 1', { exact: true })).toBeVisible();
}
telemetry.push({ phase: preflight ? 'preflight' : 'workload', atMs: performance.now() - started, metrics: (await cdp.send('Performance.getMetrics')).metrics,
domElements: await page.locator('*').count(), hostFreeMemoryBytes: os.freemem(), hostLoadAverage: os.loadavg(),
nodeMemoryBytes: process.memoryUsage(), nodeCpuMicroseconds: process.cpuUsage() });
} catch (error) { row.error = String(error); }
row.readyMs = performance.now() - begin;
(preflight ? preflightNavigation : navigation).push(row);
if (preflight) expect(row.error, `preflight ${route}`).toBeUndefined();
}
};
await interact(true);
evidence.preflightPassed = true;
evidence.phase = 'workload';
started = performance.now();
deadline = started + durationMs;
evidence.startedUtc = new Date().toISOString();
await Promise.all([readWorker(0), readWorker(1), interact()]);
evidence.actualDurationMs = performance.now() - started;
evidence.finishedUtc = new Date().toISOString();
evidence.phase = 'preservation';
expect(evidence.actualDurationMs).toBeGreaterThanOrEqual(durationMs);
const after = await snapshot(owner.id);
evidence.after = after;
assertMembership(after.ids, ids);
expect(after).toEqual(before);
const finalBoard = await page.request.get(`${api}/jobapplications/board`);
expect(finalBoard.status()).toBe(200);
assertMembership((await finalBoard.json()).map((job: { id: number }) => job.id), ids);
expect((await (await page.request.get(`${api}/career/profile`)).json()).profile).toEqual(savedProfile);
expect(summarize(latency).failures).toBe(0);
expect(navigation).toHaveLength(20); expect(navigation.filter(row => row.error)).toEqual([]);
for (const worker of [0, 1]) {
const rows = latency.filter(row => row.worker === worker);
expect(rows.length).toBeGreaterThan(0);
for (let index = 1; index < rows.length; index++) expect(rows[index].atMs - rows[index - 1].atMs).toBeGreaterThanOrEqual(paceMs);
expect(rows.at(-1)!.atMs).toBeGreaterThan(durationMs - 15000);
}
evidence.phase = 'complete';
} finally {
evidence.summary = summarize(latency);
evidence.byEndpoint = Object.fromEntries([...new Set(latency.map(row => row.endpoint))].map(endpoint => [endpoint, summarize(latency.filter(row => row.endpoint === endpoint))]));
evidence.navigationFailures = navigation.filter(row => row.error).length;
await info.attach('p9-requal-01-mariadb-performance', { body: JSON.stringify(evidence, null, 2), contentType: 'application/json' });
await cdp.detach(); await admin.dispose(); await Promise.all(readers.map(reader => reader.dispose()));
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,365 @@
{
"schemaVersion": 1,
"gate": "OPEN",
"coverage": "INCOMPLETE",
"startedUtc": "2026-10-24T07:27:05.547836+00:00",
"finishedUtc": "2026-10-24T07:27:13.317497+00:00",
"lockPath": "tools/summarizer/requirements-linux.lock",
"lockSha256": "36f2912965d183e59f2d8320f3817ca8dd0fee6d76e05cfd63ac92defe6c6031",
"rawDirectory": ".artifacts/p9-requal-01/security/python-locked",
"rawManifestSha256": "3dac31a50d803e4c4a27c43e683dcf7ee7f2587a122cfb8c38fc5276fb7233ec",
"rawSummarySha256": "31fe495d72a9b11564c8e78943be7901610e023a8606b5aeb53710871092ca20",
"exactPins": [
{
"name": "annotated-doc",
"version": "0.0.5"
},
{
"name": "annotated-types",
"version": "0.8.0"
},
{
"name": "anyio",
"version": "4.14.2"
},
{
"name": "cachetools",
"version": "5.5.2"
},
{
"name": "certifi",
"version": "2026.7.22"
},
{
"name": "charset-normalizer",
"version": "3.5.1"
},
{
"name": "click",
"version": "8.5.0"
},
{
"name": "fastapi",
"version": "0.141.1"
},
{
"name": "filelock",
"version": "3.32.4"
},
{
"name": "fsspec",
"version": "2026.7.0"
},
{
"name": "h11",
"version": "0.16.0"
},
{
"name": "hf-xet",
"version": "1.6.0"
},
{
"name": "httptools",
"version": "0.8.0"
},
{
"name": "huggingface-hub",
"version": "0.36.2"
},
{
"name": "idna",
"version": "3.19"
},
{
"name": "jinja2",
"version": "3.1.6"
},
{
"name": "lxml",
"version": "6.1.2"
},
{
"name": "markupsafe",
"version": "3.0.3"
},
{
"name": "mpmath",
"version": "1.3.0"
},
{
"name": "networkx",
"version": "3.6.1"
},
{
"name": "numpy",
"version": "2.4.6"
},
{
"name": "packaging",
"version": "26.3"
},
{
"name": "pillow",
"version": "12.3.0"
},
{
"name": "pydantic",
"version": "2.10.6"
},
{
"name": "pydantic-core",
"version": "2.27.2"
},
{
"name": "pymupdf",
"version": "1.25.5"
},
{
"name": "pypdf",
"version": "6.16.2"
},
{
"name": "pytesseract",
"version": "0.3.13"
},
{
"name": "python-docx",
"version": "1.1.2"
},
{
"name": "python-dotenv",
"version": "1.2.3"
},
{
"name": "python-multipart",
"version": "0.0.32"
},
{
"name": "pyyaml",
"version": "6.0.3"
},
{
"name": "regex",
"version": "2026.7.19"
},
{
"name": "requests",
"version": "2.34.2"
},
{
"name": "safetensors",
"version": "0.8.0"
},
{
"name": "setuptools",
"version": "84.0.0"
},
{
"name": "starlette",
"version": "1.6.0"
},
{
"name": "sympy",
"version": "1.13.1"
},
{
"name": "tokenizers",
"version": "0.21.4"
},
{
"name": "torch",
"version": "2.6.0+cpu"
},
{
"name": "tqdm",
"version": "4.70.0"
},
{
"name": "transformers",
"version": "4.48.3"
},
{
"name": "typing-extensions",
"version": "4.16.0"
},
{
"name": "typing-inspection",
"version": "0.4.4"
},
{
"name": "urllib3",
"version": "2.7.0"
},
{
"name": "uvicorn",
"version": "0.34.0"
},
{
"name": "uvloop",
"version": "0.22.1"
},
{
"name": "watchfiles",
"version": "1.2.0"
},
{
"name": "websockets",
"version": "17.1"
}
],
"counts": {
"exactPins": 49,
"queries": 98,
"successfulQueries": 97,
"queryErrors": 1,
"providerRecords": 249,
"osvRecords": 137,
"pypiRecords": 112,
"aliasGroups": 109,
"packagesWithAdvisories": 8
},
"errors": [
{
"package": "torch",
"version": "2.6.0+cpu",
"provider": "pypi",
"httpStatus": 404,
"error": "HTTPError"
}
],
"groupsPerPackage": {
"certifi": 1,
"filelock": 3,
"pillow": 41,
"pypdf": 19,
"setuptools": 1,
"torch": 19,
"transformers": 22,
"urllib3": 3
},
"limits": [
"Public exact locked-version advisory metadata only; no installation, resolution or package changes.",
"OSV performs upstream version matching; query results are not independently reproduced vulnerabilities.",
"PyPI exact torch 2.6.0+cpu release returns 404; no substitute version queried; OSV exact query succeeds.",
"249 provider records overlap; 109 groups use published alias equivalence only and are not 109 independent CVEs.",
"All returned groups retained; none had withdrawn metadata in this snapshot.",
"Severity labels reproduce provider database_specific.severity only; no score inferred or normalized.",
"Fixed-version events reproduce upstream data and can differ by branch; they are not compatible upgrade recommendations.",
"No exploitability, production package identity, deployed state or provider security acceptance established.",
"Static sidecar call sites are documented separately; individual advisory reachability is not adjudicated.",
"Audit covers 49 Python lock pins only, excluding OS/native/image packages and unpinned Docker bootstrap upgrades.",
"No model load/inference, malicious documents, deployment inspection or runtime dependency changes performed.",
"NuGet/npm remediation is separate; Python advisories remain OPEN and coverage incomplete."
],
"advisoryGroups": [
{"package":"certifi","lockedVersion":"2026.7.22","identifiers":["GHSA-6q48-6c7w-9gw2","PYSEC-2026-4320"],"primaryLinks":["https://github.com/advisories/GHSA-6q48-6c7w-9gw2"],"publishedFixedVersionEvents":["2026.10.19"],"providerRecordCount":4,"publishedSeverityLabels":["CRITICAL"]},
{"package":"filelock","lockedVersion":"3.32.4","identifiers":["CVE-2026-112940","GHSA-v4x8-3wr5-3wq8"],"primaryLinks":["https://github.com/advisories/GHSA-v4x8-3wr5-3wq8"],"publishedFixedVersionEvents":["4.2.1"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"filelock","lockedVersion":"3.32.4","identifiers":["CVE-2026-112913","GHSA-9vhv-jxww-xfm5"],"primaryLinks":["https://github.com/advisories/GHSA-9vhv-jxww-xfm5"],"publishedFixedVersionEvents":["4.2.1"],"providerRecordCount":2,"publishedSeverityLabels":["LOW"]},
{"package":"filelock","lockedVersion":"3.32.4","identifiers":["CVE-2026-112907","GHSA-567q-5qvw-pf92"],"primaryLinks":["https://github.com/advisories/GHSA-567q-5qvw-pf92"],"publishedFixedVersionEvents":["4.2.1"],"providerRecordCount":2,"publishedSeverityLabels":["LOW"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112394","GHSA-4558-3h8r-v8xp"],"primaryLinks":["https://github.com/advisories/GHSA-4558-3h8r-v8xp"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":1,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112403","GHSA-8g5g-8vmr-9782"],"primaryLinks":["https://github.com/advisories/GHSA-8g5g-8vmr-9782"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":1,"publishedSeverityLabels":["CRITICAL"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112898","GHSA-rfgq-q3w2-pmv2"],"primaryLinks":["https://github.com/advisories/GHSA-rfgq-q3w2-pmv2"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":1,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112902","GHSA-xvh5-g6gv-559v"],"primaryLinks":["https://github.com/advisories/GHSA-xvh5-g6gv-559v"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":1,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112895","GHSA-p4wg-p73j-c528"],"primaryLinks":["https://github.com/advisories/GHSA-p4wg-p73j-c528"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112400","GHSA-65qm-jvmh-6pqg"],"primaryLinks":["https://github.com/advisories/GHSA-65qm-jvmh-6pqg"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112397","GHSA-58r4-6rv3-gfh8"],"primaryLinks":["https://github.com/advisories/GHSA-58r4-6rv3-gfh8"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112896","GHSA-pchr-8j38-3x4p"],"primaryLinks":["https://github.com/advisories/GHSA-pchr-8j38-3x4p"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112897","GHSA-q45j-58j2-45rg"],"primaryLinks":["https://github.com/advisories/GHSA-q45j-58j2-45rg"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112395","GHSA-462x-pgg6-55fp"],"primaryLinks":["https://github.com/advisories/GHSA-462x-pgg6-55fp"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112404","GHSA-c6hw-5grf-ff96"],"primaryLinks":["https://github.com/advisories/GHSA-c6hw-5grf-ff96"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112899","GHSA-vx6w-vv9f-6gvg"],"primaryLinks":["https://github.com/advisories/GHSA-vx6w-vv9f-6gvg"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112402","GHSA-7mwm-ghpf-9vrm"],"primaryLinks":["https://github.com/advisories/GHSA-7mwm-ghpf-9vrm"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112900","GHSA-w889-685w-c8fh"],"primaryLinks":["https://github.com/advisories/GHSA-w889-685w-c8fh"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["CRITICAL"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112893","GHSA-j4m9-6w36-pw47"],"primaryLinks":["https://github.com/advisories/GHSA-j4m9-6w36-pw47"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112392","GHSA-27h8-gf7j-v5hc"],"primaryLinks":["https://github.com/advisories/GHSA-27h8-gf7j-v5hc"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112891","GHSA-fc9g-5qg7-mf45"],"primaryLinks":["https://github.com/advisories/GHSA-fc9g-5qg7-mf45"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112405","GHSA-cp9v-662q-vvx9"],"primaryLinks":["https://github.com/advisories/GHSA-cp9v-662q-vvx9"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112390","GHSA-22hg-r758-rrw9"],"primaryLinks":["https://github.com/advisories/GHSA-22hg-r758-rrw9"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112401","GHSA-74r2-7ff9-mjj2"],"primaryLinks":["https://github.com/advisories/GHSA-74r2-7ff9-mjj2"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112901","GHSA-xgh8-jp6q-p5x8"],"primaryLinks":["https://github.com/advisories/GHSA-xgh8-jp6q-p5x8"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112391","GHSA-254f-3936-288h"],"primaryLinks":["https://github.com/advisories/GHSA-254f-3936-288h"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112894","GHSA-jg86-v3jv-hrpm"],"primaryLinks":["https://github.com/advisories/GHSA-jg86-v3jv-hrpm"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112406","GHSA-f2g6-542v-9j58"],"primaryLinks":["https://github.com/advisories/GHSA-f2g6-542v-9j58"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112399","GHSA-5xm2-6676-qcp9"],"primaryLinks":["https://github.com/advisories/GHSA-5xm2-6676-qcp9"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112892","GHSA-gg2r-wvfm-q9rx"],"primaryLinks":["https://github.com/advisories/GHSA-gg2r-wvfm-q9rx"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112393","GHSA-332p-7hrc-92wf"],"primaryLinks":["https://github.com/advisories/GHSA-332p-7hrc-92wf"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["CRITICAL"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112396","GHSA-4rr5-89j8-mwwm"],"primaryLinks":["https://github.com/advisories/GHSA-4rr5-89j8-mwwm"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96638","GHSA-24rc-8hj2-v98c"],"primaryLinks":["https://github.com/advisories/GHSA-24rc-8hj2-v98c"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96641","GHSA-4rm8-3hmm-39fg"],"primaryLinks":["https://github.com/advisories/GHSA-4rm8-3hmm-39fg"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96640","GHSA-mrfp-p49p-3hf2"],"primaryLinks":["https://github.com/advisories/GHSA-mrfp-p49p-3hf2"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96642","GHSA-3j72-p6qg-6xp7"],"primaryLinks":["https://github.com/advisories/GHSA-3j72-p6qg-6xp7"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96639","GHSA-v9pv-5gc8-w2m6"],"primaryLinks":["https://github.com/advisories/GHSA-v9pv-5gc8-w2m6"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["CRITICAL"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96643","GHSA-g4qv-2jxx-769h"],"primaryLinks":["https://github.com/advisories/GHSA-g4qv-2jxx-769h"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96637","GHSA-g842-9rc4-69pw"],"primaryLinks":["https://github.com/advisories/GHSA-g842-9rc4-69pw"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96644","GHSA-9v9v-9qvf-h2hj"],"primaryLinks":["https://github.com/advisories/GHSA-9v9v-9qvf-h2hj"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96778","GHSA-g7cr-3p24-7wf7"],"primaryLinks":["https://github.com/advisories/GHSA-g7cr-3p24-7wf7"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-96636","GHSA-p8hx-v2p6-rp84"],"primaryLinks":["https://github.com/advisories/GHSA-p8hx-v2p6-rp84"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["GHSA-78g4-xqwv-q3g8"],"primaryLinks":["https://github.com/advisories/GHSA-78g4-xqwv-q3g8"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-114501","GHSA-654x-cwwv-5j9c"],"primaryLinks":["https://github.com/advisories/GHSA-654x-cwwv-5j9c"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pillow","lockedVersion":"12.3.0","identifiers":["CVE-2026-112398","GHSA-5x8w-8jj5-q52h"],"primaryLinks":["https://github.com/advisories/GHSA-5x8w-8jj5-q52h"],"publishedFixedVersionEvents":["13.0.0"],"providerRecordCount":2,"publishedSeverityLabels":["CRITICAL"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-102994","GHSA-5jq2-8x83-x246","PYSEC-2026-4153"],"primaryLinks":["https://github.com/advisories/GHSA-5jq2-8x83-x246"],"publishedFixedVersionEvents":["6.18.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-102995","GHSA-fp3h-c4fm-7vvf","PYSEC-2026-4154"],"primaryLinks":["https://github.com/advisories/GHSA-fp3h-c4fm-7vvf"],"publishedFixedVersionEvents":["6.18.1"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-103000","GHSA-w23x-9jrw-r45c","PYSEC-2026-4160"],"primaryLinks":["https://github.com/advisories/GHSA-w23x-9jrw-r45c"],"publishedFixedVersionEvents":["6.19.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-102999","GHSA-v247-6f48-mgcj","PYSEC-2026-4159"],"primaryLinks":["https://github.com/advisories/GHSA-v247-6f48-mgcj"],"publishedFixedVersionEvents":["6.19.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-102997","GHSA-jw7q-gvrg-4vj3","PYSEC-2026-4156"],"primaryLinks":["https://github.com/advisories/GHSA-jw7q-gvrg-4vj3"],"publishedFixedVersionEvents":["6.18.1"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-102996","GHSA-g9cg-prrw-2r8q","PYSEC-2026-4155"],"primaryLinks":["https://github.com/advisories/GHSA-g9cg-prrw-2r8q"],"publishedFixedVersionEvents":["6.18.1"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-102993","GHSA-qv6h-rv94-w285","PYSEC-2026-4158"],"primaryLinks":["https://github.com/advisories/GHSA-qv6h-rv94-w285"],"publishedFixedVersionEvents":["6.17.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-102998","GHSA-php9-fj8v-98fj","PYSEC-2026-4157"],"primaryLinks":["https://github.com/advisories/GHSA-php9-fj8v-98fj"],"publishedFixedVersionEvents":["6.19.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113792","GHSA-qfv9-64jg-mh7p"],"primaryLinks":["https://github.com/advisories/GHSA-qfv9-64jg-mh7p"],"publishedFixedVersionEvents":["6.20.1"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113795","GHSA-7mh2-4gg2-979f"],"primaryLinks":["https://github.com/advisories/GHSA-7mh2-4gg2-979f"],"publishedFixedVersionEvents":["6.20.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113793","GHSA-9mx8-4j7g-633p"],"primaryLinks":["https://github.com/advisories/GHSA-9mx8-4j7g-633p"],"publishedFixedVersionEvents":["6.20.1"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113794","GHSA-39mj-jf6j-2qj3"],"primaryLinks":["https://github.com/advisories/GHSA-39mj-jf6j-2qj3"],"publishedFixedVersionEvents":["6.20.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113786","GHSA-5hx5-vpf9-5wcv"],"primaryLinks":["https://github.com/advisories/GHSA-5hx5-vpf9-5wcv"],"publishedFixedVersionEvents":["6.21.1"],"providerRecordCount":2,"publishedSeverityLabels":["LOW"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113791","GHSA-cgf9-327m-v77c"],"primaryLinks":["https://github.com/advisories/GHSA-cgf9-327m-v77c"],"publishedFixedVersionEvents":["6.21.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113790","GHSA-f54x-cmq8-qg57"],"primaryLinks":["https://github.com/advisories/GHSA-f54x-cmq8-qg57"],"publishedFixedVersionEvents":["6.21.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-113789","GHSA-6vvc-x485-93v7"],"primaryLinks":["https://github.com/advisories/GHSA-6vvc-x485-93v7"],"publishedFixedVersionEvents":["6.21.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-115572","GHSA-6c7f-9265-g8fw"],"primaryLinks":["https://github.com/advisories/GHSA-6c7f-9265-g8fw"],"publishedFixedVersionEvents":["6.21.2"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-115585","GHSA-jr96-gvgh-j3r4"],"primaryLinks":["https://github.com/advisories/GHSA-jr96-gvgh-j3r4"],"publishedFixedVersionEvents":["6.21.2"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"pypdf","lockedVersion":"6.16.2","identifiers":["CVE-2026-115580","GHSA-gmh7-3g9r-mjh5"],"primaryLinks":["https://github.com/advisories/GHSA-gmh7-3g9r-mjh5"],"publishedFixedVersionEvents":["6.21.2"],"providerRecordCount":2,"publishedSeverityLabels":["HIGH"]},
{"package":"setuptools","lockedVersion":"84.0.0","identifiers":["GHSA-xmj3-9gf7-h52w"],"primaryLinks":["https://github.com/advisories/GHSA-xmj3-9gf7-h52w"],"publishedFixedVersionEvents":["84.1.0"],"providerRecordCount":2,"publishedSeverityLabels":["LOW"]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-3001","CVE-2025-3001","GHSA-qfhq-4f3w-5fph","PYSEC-2025-195"],"primaryLinks":["https://github.com/advisories/GHSA-qfhq-4f3w-5fph"],"publishedFixedVersionEvents":["2.10.0"],"providerRecordCount":1,"publishedSeverityLabels":["LOW"]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-3000","CVE-2025-3000","GHSA-rrmf-rvhw-rf47","PYSEC-2025-194"],"primaryLinks":["https://github.com/advisories/GHSA-rrmf-rvhw-rf47"],"publishedFixedVersionEvents":["2.13.0"],"providerRecordCount":1,"publishedSeverityLabels":["LOW"]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-2999","CVE-2025-2999","GHSA-vgrw-7cvw-pwgx","PYSEC-2025-193"],"primaryLinks":["https://github.com/advisories/GHSA-vgrw-7cvw-pwgx"],"publishedFixedVersionEvents":["2.9.1"],"providerRecordCount":1,"publishedSeverityLabels":["MODERATE"]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-2953","CVE-2025-2953","GHSA-3749-ghw9-m3mg","PYSEC-2025-191"],"primaryLinks":["https://github.com/advisories/GHSA-3749-ghw9-m3mg"],"publishedFixedVersionEvents":["2.7.1-rc1"],"providerRecordCount":2,"publishedSeverityLabels":["LOW"]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-46148","CVE-2025-46148","PYSEC-2025-198"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/151198"],"publishedFixedVersionEvents":["2.7.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-46149","CVE-2025-46149","PYSEC-2025-199"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/147848"],"publishedFixedVersionEvents":["2.7.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-46150","CVE-2025-46150","PYSEC-2025-200"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/141538"],"publishedFixedVersionEvents":["2.7.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-46152","CVE-2025-46152","PYSEC-2025-201"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/143555"],"publishedFixedVersionEvents":["2.7.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-46153","CVE-2025-46153","PYSEC-2025-202"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/142853"],"publishedFixedVersionEvents":["2.7.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-55551","CVE-2025-55551","PYSEC-2025-203"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/151401"],"publishedFixedVersionEvents":["2.9.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-55552","CVE-2025-55552","PYSEC-2025-204"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/147847"],"publishedFixedVersionEvents":["2.9.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-55553","CVE-2025-55553","PYSEC-2025-205"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/151432"],"publishedFixedVersionEvents":["2.7.1"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-55554","CVE-2025-55554","PYSEC-2025-206"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/151510"],"publishedFixedVersionEvents":["2.9.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-55557","CVE-2025-55557","PYSEC-2025-207"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/151738"],"publishedFixedVersionEvents":["2.7.1"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-55558","CVE-2025-55558","PYSEC-2025-208"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/151523"],"publishedFixedVersionEvents":["2.7.1"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-55560","CVE-2025-55560","PYSEC-2025-209"],"primaryLinks":["https://github.com/pytorch/pytorch/issues/151522"],"publishedFixedVersionEvents":["2.7.1"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2026-4538","CVE-2026-4538","PYSEC-2026-139"],"primaryLinks":["https://github.com/pytorch/pytorch/pull/176791"],"publishedFixedVersionEvents":[],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2025-3730","CVE-2025-3730","GHSA-887c-mr87-cxwp","PYSEC-2026-1970"],"primaryLinks":["https://github.com/advisories/GHSA-887c-mr87-cxwp"],"publishedFixedVersionEvents":["2.8.0"],"providerRecordCount":2,"publishedSeverityLabels":["MODERATE"]},
{"package":"torch","lockedVersion":"2.6.0+cpu","identifiers":["BIT-pytorch-2026-24747","CVE-2026-24747","GHSA-63cw-57p8-fm3p","PYSEC-2026-1856","PYSEC-2026-2286"],"primaryLinks":["https://github.com/advisories/GHSA-63cw-57p8-fm3p"],"publishedFixedVersionEvents":["2.10.0"],"providerRecordCount":1,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14929","PYSEC-2025-217"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-217"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-2099","GHSA-qq3j-4f4f-9583","PYSEC-2025-40"],"primaryLinks":["https://github.com/advisories/GHSA-qq3j-4f4f-9583"],"publishedFixedVersionEvents":["4.49.0","4.50.0","8cb522b4190bd556ce51be04942720650b1a3e57"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14926","PYSEC-2025-214"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-214"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14930","PYSEC-2025-218"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-218"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14920","PYSEC-2025-211"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-211"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14921","PYSEC-2025-212"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-212"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14924","PYSEC-2025-213"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-213"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14927","PYSEC-2025-215"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-215"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-14928","PYSEC-2025-216"],"primaryLinks":["https://osv.dev/vulnerability/PYSEC-2025-216"],"publishedFixedVersionEvents":[],"providerRecordCount":2,"publishedSeverityLabels":[]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-3933","GHSA-37mw-44qp-f5jm","PYSEC-2026-1977"],"primaryLinks":["https://github.com/advisories/GHSA-37mw-44qp-f5jm"],"publishedFixedVersionEvents":["4.52.1"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-6638","GHSA-59p9-h35m-wg4g","PYSEC-2026-1981"],"primaryLinks":["https://github.com/advisories/GHSA-59p9-h35m-wg4g"],"publishedFixedVersionEvents":["4.53.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-3264","GHSA-jjph-296x-mrcr","PYSEC-2026-1985"],"primaryLinks":["https://github.com/advisories/GHSA-jjph-296x-mrcr"],"publishedFixedVersionEvents":["4.51.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-1194","GHSA-fpwr-67px-3qhx","PYSEC-2026-1984"],"primaryLinks":["https://github.com/advisories/GHSA-fpwr-67px-3qhx"],"publishedFixedVersionEvents":["4.50.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-3777","GHSA-phhr-52qp-3mj4","PYSEC-2026-1986"],"primaryLinks":["https://github.com/advisories/GHSA-phhr-52qp-3mj4"],"publishedFixedVersionEvents":["4.52.1"],"providerRecordCount":4,"publishedSeverityLabels":["LOW"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-6051","GHSA-rcv9-qm8p-9p6j","PYSEC-2026-1988"],"primaryLinks":["https://github.com/advisories/GHSA-rcv9-qm8p-9p6j"],"publishedFixedVersionEvents":["4.53.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-6921","GHSA-4w7r-h757-3r74","PYSEC-2026-1980"],"primaryLinks":["https://github.com/advisories/GHSA-4w7r-h757-3r74"],"publishedFixedVersionEvents":["4.53.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-3263","GHSA-q2wp-rjmx-x6x9","PYSEC-2026-1987"],"primaryLinks":["https://github.com/advisories/GHSA-q2wp-rjmx-x6x9"],"publishedFixedVersionEvents":["4.51.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2025-5197","GHSA-9356-575x-2w9m","PYSEC-2026-1983"],"primaryLinks":["https://github.com/advisories/GHSA-9356-575x-2w9m"],"publishedFixedVersionEvents":["4.53.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2026-1839","GHSA-69w3-r845-3855","PYSEC-2026-2288"],"primaryLinks":["https://github.com/advisories/GHSA-69w3-r845-3855"],"publishedFixedVersionEvents":["5.0.0","5.0.0rc3"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2026-4372","GHSA-29pf-2h5f-8g72","PYSEC-2026-2289"],"primaryLinks":["https://github.com/advisories/GHSA-29pf-2h5f-8g72"],"publishedFixedVersionEvents":["5.3.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2026-5241","GHSA-fgcw-684q-jj6r","PYSEC-2026-2290"],"primaryLinks":["https://github.com/advisories/GHSA-fgcw-684q-jj6r"],"publishedFixedVersionEvents":["5.5.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"transformers","lockedVersion":"4.48.3","identifiers":["CVE-2026-9856","GHSA-xrqw-3rrv-vx5w","PYSEC-2026-3929"],"primaryLinks":["https://github.com/advisories/GHSA-xrqw-3rrv-vx5w"],"publishedFixedVersionEvents":["5.10.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"urllib3","lockedVersion":"2.7.0","identifiers":["CVE-2026-97689","GHSA-vxq7-64xx-v4gw","PYSEC-2026-4177"],"primaryLinks":["https://github.com/advisories/GHSA-vxq7-64xx-v4gw"],"publishedFixedVersionEvents":["2.8.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]},
{"package":"urllib3","lockedVersion":"2.7.0","identifiers":["CVE-2026-97688","GHSA-gh4c-6fx4-qh6g","PYSEC-2026-4176"],"primaryLinks":["https://github.com/advisories/GHSA-gh4c-6fx4-qh6g"],"publishedFixedVersionEvents":["2.8.0"],"providerRecordCount":4,"publishedSeverityLabels":["MODERATE"]},
{"package":"urllib3","lockedVersion":"2.7.0","identifiers":["CVE-2026-97687","GHSA-8988-9cw3-xx77","PYSEC-2026-4175"],"primaryLinks":["https://github.com/advisories/GHSA-8988-9cw3-xx77"],"publishedFixedVersionEvents":["2.8.0"],"providerRecordCount":4,"publishedSeverityLabels":["HIGH"]}
]
}
@@ -0,0 +1,187 @@
"""Record actual requalification outcomes; reject incomplete or overlapping browser proof."""
import hashlib
import json
import sys
from pathlib import Path
import xml.etree.ElementTree as ET
from collections import Counter
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[3]
RAW = ROOT / '.artifacts/p9-requal-01'
def load(path):
data = path.read_bytes()
return json.loads(data.decode('utf-16' if data.startswith(b'\xff\xfe') else 'utf-8-sig'))
def identity(path):
return {'path': path.relative_to(ROOT).as_posix(), 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
def specs(report):
for suite in report.get('suites', []):
yield from suite.get('specs', [])
yield from specs(suite)
def browser_results(report, exit_code, expected_count=None):
if exit_code != 0 or report.get('errors'): raise ValueError('Browser process or run-level failure.')
cases = []
for spec in specs(report):
for test in spec['tests']:
results = test.get('results', [])
if test.get('expectedStatus') != 'passed' or len(results) != 1:
raise ValueError('Missing, repeated or expected-failure execution.')
result = results[0]
if result.get('status') != 'passed' or result.get('retry', 0) != 0:
raise ValueError('Failed, skipped or retried browser execution.')
cases.append({'id': spec['id'] + ':' + test['projectName'], 'title': spec['title'],
'outcome': result['status'], 'durationMs': result['duration'] if 'duration' in result else None})
if len({c['id'] for c in cases}) != len(cases): raise ValueError('Duplicate browser case IDs.')
if expected_count is not None and len(cases) != expected_count: raise ValueError('Browser membership count mismatch.')
return cases
def reconcile_ordinary(discovery, shards):
expected = [s['id'] + ':' + t['projectName'] for s in specs(discovery) for t in s['tests']]
actual = [case for shard in shards for case in shard]
ids = [case['id'] for case in actual]
if discovery.get('errors') or len(expected) != len(set(expected)) or len(ids) != len(set(ids)) or set(ids) != set(expected):
raise ValueError('Ordinary discovery and actual shard membership differ or overlap.')
return actual
def require_complete(results):
incomplete=[name for name,result in results.items() if result.get('status')!='PASS']
if incomplete: raise ValueError('Required automation incomplete or failing: '+', '.join(incomplete))
def sidecar_results(root, expected):
cases=list(root.iter('testcase'))
identities=[(case.get('classname'),case.get('name')) for case in cases]
if len(cases)!=expected or len(set(identities))!=expected or any(not name for _,name in identities):
raise ValueError('Missing or duplicate sidecar cases.')
if any(case.find(tag) is not None for case in cases for tag in ['failure','error','skipped']):
raise ValueError('Failed or skipped sidecar cases.')
return [{'classname':group,'name':name,'outcome':'passed'} for group,name in identities]
def jest_results(report, baseline, exit_code, expected):
def rows(value):
return [(suite['name'].replace('\\','/'),case) for suite in value['testResults'] for case in suite['assertionResults']]
actual, reference=rows(report), rows(baseline)
# test.each can legitimately generate identical labels; preserve their exact multiplicity.
membership=lambda items: Counter((file,case['fullName']) for file,case in items)
if exit_code!=0 or len(actual)!=expected or membership(actual)!=membership(reference):
raise ValueError('Frontend native case membership differs from unchanged-source reference.')
if any(case['status']!='passed' or case.get('invocations',1)!=1 or case.get('retryReasons') for _,case in actual):
raise ValueError('Failed, skipped or retried frontend case.')
return [{'file':file,'nativeOrdinal':index,'name':case['fullName'],'outcome':case['status']} for index,(file,case) in enumerate(actual)]
def security_results(commands, npm, nuget):
expected={'supply-chain','evaluator','browser-runtime','ingress','secrets','sbom','node-guards','extension',
'database-runner','browser-database-boundary','nuget-advisories','npm-advisories'}
if len(commands)!=len(expected) or {command['name'] for command in commands}!=expected:
raise ValueError('Missing or duplicate security command evidence.')
projects=nuget.get('projects',[])
if len(projects)!=1 or not projects[0].get('path','').replace('\\','/').endswith('/JobTrackerApi/JobTrackerApi.csproj'):
raise ValueError('Missing or unexpected NuGet project evidence.')
if nuget.get('problems') or nuget.get('errors') or any(project.get('errors') for project in projects):
raise ValueError('NuGet audit errors.')
vulnerabilities=[package for project in projects for framework in project.get('frameworks',[]) for key in ['topLevelPackages','transitivePackages'] for package in framework.get(key,[]) if package.get('vulnerabilities')]
passed=all(command['exitCode']==0 for command in commands) and npm['metadata']['vulnerabilities']['total']==0 and not vulnerabilities
return passed,vulnerabilities
def python_security_status(record):
if record['advisoryGroups']: return 'FINDINGS'
return 'INCOMPLETE' if record['errors'] else 'PASS'
def collect_browser(name, count=None):
report_path, process_path = RAW / (name + '.json'), RAW / (name + '-exit.json')
result = {'lane': name}
try:
report, process = load(report_path), load(process_path)
result.update(report=identity(report_path), processEvidence=identity(process_path), process=process)
result['cases'] = browser_results(report, process['exitCode'], count)
result.update(status='PASS', passed=len(result['cases']), stats=report.get('stats'))
except FileNotFoundError as error: result.update(status='NOT_RUN', reason=str(error))
except (ValueError, KeyError) as error: result.update(status='FAIL', reason=str(error))
return result
def collect_trx(name, file, expected):
result = {'lane': name}
try:
path = RAW / file
root = ET.parse(path).getroot()
counters = next(e.attrib for e in root.iter() if e.tag.endswith('Counters'))
cases = [e.attrib for e in root.iter() if e.tag.endswith('UnitTestResult')]
process = load(RAW / (name + '-exit.json'))
passed = process['exitCode'] == 0 and int(counters['passed']) == expected and int(counters['total']) == expected and len(cases) == expected
passed = passed and len({(c['testId'],c['testName']) for c in cases}) == expected and len({c['executionId'] for c in cases}) == expected
passed = passed and all(c['outcome'] == 'Passed' for c in cases)
result.update(status='PASS' if passed else 'FAIL', report=identity(path), counters=counters, process=process,
cases=[{'testId':c['testId'],'executionId':c['executionId'],'name':c['testName'], 'outcome':c['outcome']} for c in cases])
except FileNotFoundError as error: result.update(status='NOT_RUN', reason=str(error))
return result
def main():
results = {'backend':collect_trx('backend','backend/backend.trx',1123),
'mariadb':collect_trx('mariadb','mariadb/mariadb.trx',32)}
for index in range(1,5): results[f'ordinary-{index}'] = collect_browser(f'ordinary-{index}')
try:
if not all(results[f'ordinary-{i}']['status']=='PASS' for i in range(1,5)): raise ValueError('Incomplete ordinary shards.')
actual=reconcile_ordinary(load(RAW/'discovery.json'),[results[f'ordinary-{i}']['cases'] for i in range(1,5)])
if len(actual)!=362: raise ValueError('Unexpected ordinary discovery count.')
results['ordinary']={'status':'PASS','passed':len(actual),'discovery':identity(RAW/'discovery.json')}
except (FileNotFoundError,ValueError) as error: results['ordinary']={'status':'NOT_COMPLETE','reason':str(error)}
for name,count in {'ai':2,'auth':2,'rate':9,'ingress':6,'pdf-admission':1,'preferences':3,
'rd21':6,'diagnostics':18,'volume':3,'rd21-volume':3,'mariadb-browser':1,'mariadb-observations':2,'linux-renderer':5}.items():
results[name]=collect_browser(name,count)
try:
path=RAW/'frontend.json'; data=load(path); process=load(RAW/'frontend-exit.json')
cases=jest_results(data,load(RAW/'frontend-before-security.json'),process['exitCode'],934)
passed=process['exitCode']==0 and data['numPassedTests']==934 and data['numTotalTests']==934 and data['numPendingTests']==0 and data['numTodoTests']==0 and data['numFailedTests']==0
results['frontend']={'status':'PASS' if passed else 'FAIL','passed':data['numPassedTests'],
'suites':data['numPassedTestSuites'],'report':identity(path),'process':process,
'cases':cases,'membershipReference':identity(RAW/'frontend-before-security.json')}
except FileNotFoundError as error: results['frontend']={'status':'NOT_RUN','reason':str(error)}
except ValueError as error: results['frontend']={'status':'FAIL','reason':str(error)}
for name in ['types','lint','build','sidecar']:
try:
process=load(RAW/(name+'-exit.json'))
results[name]={'status':'PASS' if process['exitCode']==0 else 'FAIL','process':process,'log':identity(RAW/(name+'.log'))}
if name=='sidecar':
try: results[name]['cases']=sidecar_results(ET.parse(RAW/'sidecar.xml').getroot(),56)
except ValueError as error: results[name].update(status='FAIL',reason=str(error))
results[name]['report']=identity(RAW/'sidecar.xml')
except FileNotFoundError as error: results[name]={'status':'NOT_RUN','reason':str(error)}
for name,file in [('performance','performance/result.json'),('native-pdf','native-pdf.json'),('linux-pdf','linux-pdf.json')]:
try:
data=load(RAW/file)
results[name]={'status':data['status'],'record':data,'evidence':identity(RAW/file)}
except FileNotFoundError: results[name]={'status':'NOT_RUN'}
try:
path=RAW/'security/commands.json'; commands=load(path)
npm=load(RAW/'security/frontend-audit.json')
nuget=load(RAW/'security/nuget-advisories.log')
passed,vulnerabilities=security_results(commands,npm,nuget)
results['security']={'status':'PASS' if passed else 'FAIL','commands':commands,'evidence':identity(path),'npmAudit':identity(RAW/'security/frontend-audit.json'),'nugetAudit':identity(RAW/'security/nuget-advisories.log'),'nugetVulnerabilities':vulnerabilities,'npmVulnerabilities':npm['metadata']['vulnerabilities']}
except FileNotFoundError: results['security']={'status':'NOT_RUN'}
except ValueError as error: results['security']={'status':'FAIL','reason':str(error)}
try:
path=HERE/'python-advisories.json'; data=load(path)
if data['lockSha256']!=identity(ROOT/data['lockPath'])['sha256']:
raise ValueError('Python lock changed after advisory queries.')
results['python-advisories']={'status':python_security_status(data),'coverage':data['coverage'],
'counts':data['counts'],'errors':data['errors'],'evidence':identity(path)}
except FileNotFoundError: results['python-advisories']={'status':'NOT_RUN'}
except ValueError as error: results['python-advisories']={'status':'FAIL','reason':str(error)}
manifest=RAW/'source-freeze.json'
source=load(manifest)
drift=[name for name,expected in source['inputs'].items() if not (ROOT/name).is_file() or hashlib.sha256((ROOT/name).read_bytes()).hexdigest()!=expected]
record={'schemaVersion':1,'baselineCommit':'549b2cba5a791862ba3f854312ea819a86dc42a0',
'branch':'fix/p9-ci-and-requalification','countPolicy':'Separate lanes; discovery-only skips and overlapping reruns never enter executed totals.',
'sourceIdentity':identity(manifest),'frozenInputCount':len(source['inputs']),'runtimeDrift':drift,'results':results}
record['automationComplete']=not drift and all(value.get('status')=='PASS' for value in results.values())
(HERE/'validation.json').write_text(json.dumps(record,indent=2)+'\n',encoding='utf-8')
print(json.dumps({name:data.get('status','RECORDED') for name,data in results.items()}))
if drift: raise SystemExit('Runtime source changed: '+str(drift))
if '--collect-only' not in sys.argv:
try: require_complete(results)
except ValueError as error: raise SystemExit(str(error))
if __name__=='__main__': main()
@@ -0,0 +1,59 @@
"""Offline failure-path checks; these do not qualify a database or renderer."""
import contextlib
import importlib.util
import io
import json
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('linux_run', Path(__file__).with_name('linux-run.py'))
runner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runner)
class CleanupTests(unittest.TestCase):
def exercise(self, *, sql_failure=False, log_failure=False):
calls=[]
migration_reads=0
def command(args, **kwargs):
calls.append(args)
if args==['docker','inspect',runner.DB]:
return json.dumps([{'Config':{'Image':runner.PIN,'Labels':{'com.docker.compose.project':'jobtracker-it-p9-requal','com.docker.compose.service':'mariadb-test'}},
'State':{'Running':True,'Health':{'Status':'healthy'}},
'HostConfig':{'Tmpfs':{'/var/lib/mysql':''},'PortBindings':{'3306/tcp':[{'HostIp':'127.0.0.1','HostPort':'33308'}]}}}])
if args[:2]==['docker','logs'] and log_failure: raise OSError('Synthetic log capture failure')
return 'synthetic identity'
def sql(query):
nonlocal migration_reads
if query=='SELECT VERSION()': return '12.2.2-MariaDB-test'
if '__EFMigrationsHistory' in query:
migration_reads+=1
if migration_reads==2 and sql_failure: raise OSError('Synthetic post-run SQL failure')
return '1'
return '0'
def urlopen(url, **kwargs):
return io.BytesIO(json.dumps({'version':runner.VERSION} if url.endswith('/health') else {'status':'ready'}).encode())
with tempfile.TemporaryDirectory(prefix='p9-linux-guard-') as directory:
out=Path(directory)/'linux'
with patch.object(runner,'OUT',out), patch.object(runner,'command',command), patch.object(runner,'sql',sql), \
patch.object(runner.urllib.request,'urlopen',urlopen), patch.object(runner.subprocess,'run',return_value=SimpleNamespace(returncode=0)), \
patch.object(runner.sys,'argv',['linux-run.py']), contextlib.redirect_stdout(io.StringIO()):
with self.assertRaises(SystemExit) as failure: runner.main()
self.assertEqual(failure.exception.code,1)
record=json.loads((out/'process.json').read_text(encoding='utf-8'))
self.assertEqual(record['browserExitCode'],0)
self.assertEqual(record['exitCode'],1)
self.assertTrue(record['cleanup']['apiRemoved'])
self.assertTrue(record['cleanup']['schemaRemoved'])
self.assertTrue(any(args[:3]==['docker','rm','--force'] for args in calls))
self.assertEqual(json.loads((out.parent/'linux-renderer-exit.json').read_text())['exitCode'],1)
def test_post_run_sql_failure_cannot_pass(self):
self.exercise(sql_failure=True)
def test_log_failure_does_not_suppress_removal(self):
self.exercise(log_failure=True)
if __name__=='__main__': unittest.main()
@@ -0,0 +1,20 @@
{
"observedUtc": "2026-10-24T11:08:20.616748+00:00",
"status": "PASS",
"projects": [
{
"project": "JobTrackerApi.Tests",
"exitCode": 0,
"report": ".artifacts/p9-requal-01/security/JobTrackerApi.Tests-audit.json",
"sha256": "7083e801175539d0f1a00f0c24fe5577275485445f06e6daadb87c528f278b00",
"vulnerabilities": []
},
{
"project": "JobTrackerApi.MariaDb.Tests",
"exitCode": 0,
"report": ".artifacts/p9-requal-01/security/JobTrackerApi.MariaDb.Tests-audit.json",
"sha256": "14505f7f80a7d470bbc75806784e5dbfea1a89c6f605f47eaa5551eb9c6461a6",
"vulnerabilities": []
}
]
}
@@ -0,0 +1,82 @@
"""Offline fail-closed checks for the requalification result ledger."""
import copy
import importlib.util
from pathlib import Path
import unittest
import xml.etree.ElementTree as ET
spec = importlib.util.spec_from_file_location('requalification', Path(__file__).with_name('reconcile.py'))
ledger = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ledger)
def report(identifier='one'):
return {'errors': [], 'suites': [{'specs': [{'id': identifier, 'title': identifier,
'tests': [{'projectName': 'chromium', 'expectedStatus': 'passed',
'results': [{'status': 'passed', 'retry': 0}]}]}]}]}
class EvidenceGuards(unittest.TestCase):
def test_accepts_completed_native_case(self):
self.assertEqual(len(ledger.browser_results(report(), 0, 1)), 1)
def test_failed_process_cannot_be_qualified_by_passing_cases(self):
with self.assertRaises(ValueError): ledger.browser_results(report(), 1, 1)
def test_rejects_skips_retries_and_missing_results(self):
for results in [[{'status':'skipped','retry':0}], [{'status':'passed','retry':1}], []]:
value=report(); value['suites'][0]['specs'][0]['tests'][0]['results']=results
with self.assertRaises(ValueError): ledger.browser_results(value, 0, 1)
def test_rejects_expected_failure_and_run_errors(self):
value=report(); value['suites'][0]['specs'][0]['tests'][0]['expectedStatus']='failed'
with self.assertRaises(ValueError): ledger.browser_results(value, 0, 1)
value=report(); value['errors']=[{'message':'web server failed'}]
with self.assertRaises(ValueError): ledger.browser_results(value, 0, 1)
def test_rejects_count_mismatch_and_duplicate_cases(self):
with self.assertRaises(ValueError): ledger.browser_results(report(), 0, 2)
value=report(); value['suites']*=2
with self.assertRaises(ValueError): ledger.browser_results(value, 0)
def test_discovery_only_skip_status_is_not_runtime_skip_evidence(self):
discovery=report(); discovery['suites'][0]['specs'][0]['tests'][0]['results']=[]
discovery['stats']={'skipped':1,'expected':0}
self.assertEqual(len(ledger.reconcile_ordinary(discovery, [ledger.browser_results(report(),0)])),1)
def test_shards_cannot_overlap_or_omit_discovery_members(self):
cases=ledger.browser_results(report(),0)
with self.assertRaises(ValueError): ledger.reconcile_ordinary(report(), [cases,cases])
with self.assertRaises(ValueError): ledger.reconcile_ordinary(report('different'), [cases])
def test_final_gate_cannot_succeed_with_incomplete_or_failed_lanes(self):
ledger.require_complete({'one': {'status': 'PASS'}})
for status in ['FAIL', 'NOT_RUN', 'NOT_COMPLETE', 'BLOCKED', 'FINDINGS', 'INCOMPLETE']:
with self.assertRaises(ValueError): ledger.require_complete({'one': {'status':'PASS'}, 'two': {'status':status}})
def test_sidecar_requires_unique_successful_native_cases(self):
value=ET.fromstring('<testsuites><testsuite><testcase classname="a" name="one"/><testcase classname="a" name="two"/></testsuite></testsuites>')
self.assertEqual(len(ledger.sidecar_results(value,2)),2)
duplicate=ET.fromstring('<testsuites><testsuite><testcase classname="a" name="one"/><testcase classname="a" name="one"/></testsuite></testsuites>')
with self.assertRaises(ValueError): ledger.sidecar_results(duplicate,2)
failed=ET.fromstring('<testsuites><testsuite><testcase classname="a" name="one"><skipped/></testcase></testsuite></testsuites>')
with self.assertRaises(ValueError): ledger.sidecar_results(failed,1)
def test_jest_parameterized_labels_require_exact_baseline_multiset(self):
value={'testResults':[{'name':'suite.ts','assertionResults':[
{'fullName':'same label','status':'passed','invocations':1},
{'fullName':'same label','status':'passed','invocations':1}]}]}
self.assertEqual(len(ledger.jest_results(value,value,0,2)),2)
extra=copy.deepcopy(value); extra['testResults'][0]['assertionResults'].append(extra['testResults'][0]['assertionResults'][0])
with self.assertRaises(ValueError): ledger.jest_results(extra,value,0,3)
retried=copy.deepcopy(value); retried['testResults'][0]['assertionResults'][0]['invocations']=2
with self.assertRaises(ValueError): ledger.jest_results(retried,value,0,2)
def test_security_rejects_missing_commands_and_empty_project_evidence(self):
with self.assertRaises(ValueError): ledger.security_results([],{'metadata':{'vulnerabilities':{'total':0}}},{'projects':[]})
def test_known_python_advisories_cannot_be_a_clean_security_gate(self):
value={'advisoryGroups':[{'identifiers':['one']}],'errors':[{'httpStatus':404}]}
self.assertEqual(ledger.python_security_status(value),'FINDINGS')
self.assertEqual(ledger.python_security_status({'advisoryGroups':[],'errors':[{}]}),'INCOMPLETE')
self.assertEqual(ledger.python_security_status({'advisoryGroups':[],'errors':[]}),'PASS')
if __name__ == '__main__': unittest.main()