feat(cv): qualify templates and reliable local import processing
This commit is contained in:
@@ -91,7 +91,7 @@ def sources(commands):
|
||||
'scripts/verify-cv-fidelity.py', 'docs/redesign/phase-9/p9-requal-01/pdf-proof.py'}
|
||||
selected = [name for name in paths if name.startswith(prefixes) or name in exact
|
||||
or name.startswith(('Directory.', 'job-tracker-ui/playwright', 'job-tracker-ui/scripts/'))
|
||||
or name.startswith('docs/cv-redesign/') and name.endswith(('.py', '.ts', '.json'))]
|
||||
or name.startswith(('docs/cv-redesign/', 'docs/pre-phase10-ux/')) and name.endswith(('.py', '.ts', '.json'))]
|
||||
return {name: digest((ROOT / name).read_bytes()) for name in sorted(set(selected)) if (ROOT / name).is_file()}
|
||||
|
||||
|
||||
@@ -240,6 +240,8 @@ def run(args):
|
||||
record['frontend'] = tree(static)
|
||||
if not record['frontend']:
|
||||
raise ValueError('Prebuilt frontend output is required.')
|
||||
if not any(b'http://localhost:5302/api' in file.read_bytes() for file in static.rglob('*.js')):
|
||||
raise ValueError('Build the static frontend with NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api; serving-time environment cannot change compiled client routing.')
|
||||
record.update({'buildRecord': str(Path(args.build_record).resolve()), 'buildRecordSha256': digest(Path(args.build_record).read_bytes()),
|
||||
'sourceSha256': build_record['sourceSha256'], 'imageId': args.image, 'commit': build_record['commit'],
|
||||
'schema': schema, 'dbContainer': db, 'dbProject': project, 'apiContainer': api, 'network': network})
|
||||
@@ -316,12 +318,18 @@ def run(args):
|
||||
'NEXT_PUBLIC_API_BASE_URL': 'http://localhost:5302/api', 'CV_FIDELITY_OUTPUT': str(out / 'fidelity'),
|
||||
'CV_SETTINGS_FIDELITY_OUTPUT': str(out / 'settings-fidelity'), 'LETTER_FIDELITY_OUTPUT': str(out / 'letter-fidelity'),
|
||||
'PLAYWRIGHT_JSON_OUTPUT_NAME': str(out / 'linux-renderer.json')}
|
||||
commands.run(['node', 'node_modules/@playwright/test/cli.js', 'test', '--config=' + CONFIG, '--retries=0',
|
||||
config = CONFIG
|
||||
if args.include_template_library:
|
||||
config = '../docs/pre-phase10-ux/linux-template.config.ts'
|
||||
env.update({'PRE_P10_DESIGN_VERSION': '2', 'CV_TEMPLATE_OUTPUT': str(out / 'template-library'),
|
||||
'LETTER_LIBRARY_OUTPUT': str(out / 'letter-library')})
|
||||
commands.run(['node', 'node_modules/@playwright/test/cli.js', 'test', '--config=' + config, '--retries=0',
|
||||
'--reporter=list,json', '--output=' + str(out / 'test-results')], cwd=ROOT / 'job-tracker-ui', env=env, timeout=1800)
|
||||
native = json.loads((out / 'linux-renderer.json').read_text(encoding='utf-8'))
|
||||
stats = native.get('stats', {})
|
||||
if stats.get('expected') != 6 or any(stats.get(key) != 0 for key in ('unexpected', 'flaky', 'skipped')) or native.get('errors'):
|
||||
raise ValueError('Expected six passing browser cases with no skips, retries or errors.')
|
||||
expected_cases = 8 if args.include_template_library else 6
|
||||
if stats.get('expected') != expected_cases or any(stats.get(key) != 0 for key in ('unexpected', 'flaky', 'skipped')) or native.get('errors'):
|
||||
raise ValueError(f'Expected {expected_cases} passing browser cases with no skips, retries or errors.')
|
||||
record['browserStats'] = stats
|
||||
if sql(migration_sql).splitlines() != record['migrations'] or sources(commands) != current_sources or tree(static) != record['frontend']:
|
||||
raise ValueError('Source/runtime/migration drift during rendering.')
|
||||
@@ -335,6 +343,10 @@ def run(args):
|
||||
raise ValueError('The original CV16 + settings8 document membership is incomplete.')
|
||||
if {fixture.get('fixture') for fixture in letter.get('fixtures', [])} != {'light-en-a4', 'dark-nb-a4', 'light-nb-letter', 'dark-en-letter'}:
|
||||
raise ValueError('The independent letter4 document membership is incomplete.')
|
||||
if args.include_template_library:
|
||||
for folder, count in (('template-library', 36), ('letter-library', 18)):
|
||||
if len(json.loads((out / folder / 'measurements.json').read_text(encoding='utf-8')).get('fixtures', [])) != count:
|
||||
raise ValueError('Incomplete revised design corpus: ' + folder)
|
||||
record['exitCode'] = 0
|
||||
except Exception as error:
|
||||
record['error'] = scrub(error)
|
||||
@@ -373,6 +385,7 @@ def main():
|
||||
renderer.add_argument('--output', required=True)
|
||||
renderer.add_argument('--build-record', required=True)
|
||||
renderer.add_argument('--image', required=True)
|
||||
renderer.add_argument('--include-template-library', action='store_true', help='Also render the separate PRE-P10 revised CV and matching-letter corpora.')
|
||||
args = parser.parse_args()
|
||||
return build(args) if args.mode == 'build' else run(args)
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# B and owner additions: development acceptance
|
||||
|
||||
28 October 2026. Branch `feature/cv-template-library-review`, cumulatively based on accepted A commit `663023a` and local-runtime checkpoint `73ea888`. The source-bearing commit immediately following this report is the qualified checkpoint; source/image hashes are retained in the build records. No main merge or production deployment is authorised.
|
||||
|
||||
**Decision: CONDITIONAL GO for continued development and workstream C. Production: NO-GO.** The application changes and finite supported-document checks pass. The unchanged large-name coordinate oracle remains partial, human/device/print qualifications remain open, and the consolidated PRE-P10 regression is still required after C/D.
|
||||
|
||||
## Delivered
|
||||
|
||||
- Version-two designs for the existing nine compatible CV IDs: title/header separation in Modern/Classic/Code, full-height physical sidebar backgrounds, actual sidebar palettes, wider identities and measured name typography. Existing absent/version-one designs retain their original rendering.
|
||||
- Actual server-rendered synthetic template cards, lazy loading and sandboxing; explicit candidate Apply preserves CV content and canonical profile data.
|
||||
- Nine matching cover-letter designs and chooser, current-authored-text preview, explicit snapshot application and existing shared revision/history persistence. Server-side premium entitlement enforcement covers save/preview/restore, while preserving legacy and existing same-theme designs after downgrade.
|
||||
- Optional CV portrait controls using the existing shared profile/crop/storage owner; keyboard zoom/pan, bounded decoded-image validation, metadata stripping, durable save acknowledgements, staged-file reconciliation and owner-only retention/account inventory.
|
||||
- Document-only durable queue execution while generative work remains disabled. Disabled workers reject new PDF/DOCX imports with actionable503 rather than acknowledging an unprocessable queue. English/Bokmål recovery guidance is covered.
|
||||
- Isolated verified local Ollama, approved read-only trained summariser checkpoint and parser-capable sidecar. Development workers are enabled, external AI is off and email is routed to Mailpit. The configurable backend timeout supports slower local CPU inference. Source image activation is a separate operational step, recorded in the local-runtime documentation.
|
||||
|
||||
## Exact evidence
|
||||
|
||||
Counts below are separate lanes. Focused reruns are not added to full-suite totals.
|
||||
|
||||
| Lane | Result | Raw evidence under `.artifacts/` |
|
||||
|---|---|---|
|
||||
| Full .NET backend | 1,394 passed,0 failed,0 skipped | `pre-phase10-ux/b-backend-04/b-backend.trx` |
|
||||
| Full frontend | 1,014 passed across110 suites,0 failed | `pre-phase10-ux/b-frontend-full-02.json` |
|
||||
| Real MariaDB12.2.2 | 84 passed,0 skipped; generated schemas cleaned | `pre-phase10-ux/b-mariadb-03/` |
|
||||
| B interactive browser | 6 passed: actual gallery, actual letter preview, keyboard crop/optional photo; EN1440/NB390 | `pre-phase10-ux/b-selection-browser-03.json` |
|
||||
| Real Docling import | 2 passed: upload→actual extraction→refresh→review/correct/apply→profile→CV→PDF; EN/light and NB/dark | `pre-phase10-ux/queue-real-docling-02.json` |
|
||||
| Windows revised CV producer | 36 documents/122 pages; marker/page/bounds36/36; whole-name36/36; sidebar43/43 pages | `pre-phase10-ux/templates-revised-02/` |
|
||||
| Windows matching letters | 18 documents/38 pages; full source retention/count/bounds18/18 | `pre-phase10-ux/matching-letters-02/` |
|
||||
| Current v1 HTML preservation | 36/36 exact CSS/fonts, body/order and image-normalized HTML; raw12/36 exact | `pre-phase10-ux/templates-revised-02/legacy-preservation.json` |
|
||||
| Linux producer/browser | 8 passed,0 failed/skipped/flaky; all owned resources cleaned | `cv-ux-01/pre-p10-b-linux-run-02/process.json` |
|
||||
| Linux original CV fidelity | 24 documents/60 pages; unchanged oracle PASS | `cv-ux-01/pre-p10-b-linux-run-02/linux-pdf.json` |
|
||||
| Linux original letter fidelity | 4 documents/14 pages; unchanged oracle4/4 PASS | `cv-ux-01/pre-p10-b-linux-run-02/letter-fidelity/fidelity-results.json` |
|
||||
| Linux revised CVs | 36 documents/122 pages; content/bounds36/36, whole-name36/36, sidebar43/43 pages | `cv-ux-01/pre-p10-b-linux-run-02/template-library/` |
|
||||
| Linux matching letters | 18 documents/38 pages; full source retention/count/bounds18/18 | `cv-ux-01/pre-p10-b-linux-run-02/letter-library/pdf-proof.json` |
|
||||
| TypeScript/lint/static build | Native exits0; corrected static API target explicitly verified | `pre-phase10-ux/b-{typecheck-final,lint-final,build-02}.log` |
|
||||
| NuGet vulnerability query | Exit0; no vulnerable packages reported, including transitives | `pre-phase10-ux/b-dependency-audit.json` |
|
||||
| Local model integrity/runtime | All pinned blobs reverified28 October; earlier actual JSON/summary/rewrite smokes pass | `pre-phase10-ux/local-ai/` and `dev/README.md` |
|
||||
|
||||
The original Windows24-CV/four-letter corpus and the complete ordinary/contract/security/Python/preservation lanes must be rerun in consolidated integration. Prior accepted results are historical and are not relabelled as fresh B checks. Backend/frontend totals include their existing regression cases; they are not a substitute for browser or provider lanes.
|
||||
|
||||
## Retained failures and limits
|
||||
|
||||
The revised-name unchanged oracle remains **Windows8/36 PASS with28 S01-only failures; Linux16/36 PASS with20 S01-only failures**. No other marker/font/colour/membership failures occur. Separate captured-ink diagnostics show a maximum3px top difference on each final platform. The original2mm character-cell/DOM Range tolerance remains unchanged; the diagnostic is not a replacement acceptance criterion. Source completeness, physical bounds and full-height sidebar pixel checks independently pass. See TEMPLATE_AUDIT.md.
|
||||
|
||||
Earlier failures remain available: signature-only avatar RED;27-case legacy/removal RED;9-case entitlement RED;8 surname-fragment benchmark failures; a worker test stopped before the durable operation acknowledgement (fixed to await both review and completed operation); an initial static build pointed at the wrong test API (six UI failures, corrected by rebuilding with the explicit test endpoint and adding a preflight guard). Docker being stopped prevented one new MariaDB run from starting; no tests ran in that attempt. The subsequent84-test run passed after local IPC recovery. No failed attempt is counted as a skip or a pass.
|
||||
|
||||
The checked browser journeys emit pre-existing disabled-tooltip warnings and a handled checklist seed uniqueness race in the isolated SQLite host; their assertions pass. No general checklist rewrite is included. The MariaDB lane validates real persistence separately.
|
||||
|
||||
Automated JPEG/WebP/EXIF decoder cases ran on Windows; the actual Linux import/gallery corpus exercises the normal cropped-PNG upload path. Human assistive technology, physical devices, native language review and manual printing remain unperformed. Sparse final pages can remain for sparse source content; arbitrary names and custom font sizes cannot be guaranteed to fit without user adjustment.
|
||||
|
||||
Local `qwen2.5:7b` matches the repository's recorded production model name. Exact deployed artifact parity is unverified. Runtime smoke success does not qualify factual CV extraction accuracy or all queued generative workflows. Generative CV-import assistance remains disabled pending its separate grounded-output/model-quality qualification. No production cache, service, database or filesystem concern is treated as resolved.
|
||||
|
||||
## Independent decisions
|
||||
|
||||
| Area | Decision |
|
||||
|---|---|
|
||||
| Template implementation, finite content retention and v1 compatibility | PASS |
|
||||
| Original Linux CV/letter fidelity | PASS |
|
||||
| Revised full-height sidebars and benchmark name wrapping | PASS |
|
||||
| Revised large-name coordinate oracle | PARTIAL; original failures retained |
|
||||
| Actual gallery and explicit matching-letter selection | PASS, finite automation |
|
||||
| Optional photo workflow/security | CONDITIONAL GO; stated platform/human limits |
|
||||
| Upload queue recovery / actual deterministic Docling journey | PASS, finite supported fixtures |
|
||||
| MariaDB / RD21 preservation | PASS in84-test provider lane |
|
||||
| Local AI runtime / worker configuration | PASS for integrity, connectivity and stated smokes; source activation separately recorded |
|
||||
| Real generative CV extraction accuracy / deployed model parity | NOT QUALIFIED |
|
||||
| Workstream C entry | CONDITIONAL GO; continue safe development |
|
||||
| Consolidated PRE-P10 acceptance / Phase10 cleanup | NOT YET READY |
|
||||
| Production | NO-GO |
|
||||
|
||||
Next: activate qualified source images only in the existing local development stack; then create `feature/dashboard-interactive-grid` from this accepted cumulative checkpoint. Keep all C/D and consolidated integration requirements in PLAN.md. No additional owner approval is needed for these already-authorised safe development steps.
|
||||
@@ -24,8 +24,8 @@ Local runtime target: official Ollama0.40.11 linux/amd64 image digest `sha256:45
|
||||
|
||||
## Checkpoint additions
|
||||
|
||||
- [ ] B-owner: qualify title spacing/full-page sidebar backgrounds and real template thumbnails; match letter designs through versioned settings and existing revision owners.
|
||||
- [ ] Import-recovery: document-only worker can consume prior document imports while model jobs remain disabled; disabled workers return an actionable503; real Docling journey verifies completion.
|
||||
- [x] B-owner: qualified title spacing/full-page sidebar backgrounds and real template thumbnails; matching letter designs use versioned settings and existing revision owners. Finite evidence and partial oracle/human qualifications are explicit in B_ACCEPTANCE.md.
|
||||
- [x] Import-recovery: document-only worker consumes prior document imports while model jobs remain disabled; disabled workers return an actionable503; two real Docling journeys verify completion.
|
||||
- [x] Local-options configuration: guarded standalone dev stack identity/loopback/network/storage boundaries; authorised workers enabled; email forced to Mailpit. Account entitlements, schedules and missing provider credentials remain applicable. This is configuration activation, not full feature acceptance.
|
||||
- [x] Local-Ollama: independent pinned container/cache; all official manifest blobs verified; actual constrained JSON CPU inference and authenticated sidecar summary/rewrite checks passed. Deployed digest parity and real CV extraction accuracy remain unqualified.
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ No main merge, production service/database/storage/model/cache/deployment operat
|
||||
- [x] P10-UX-00: read owner specification, verify latest CV-QA ancestry/remote/entry, isolate managed worktree, audit four owners and choose compatibility-first design. Create feedback register and branch plan.
|
||||
- [x] A-01: reproduce blank hydration/header/export and Strict Mode history failures in focused tests plus actual local development browser. Save RED evidence; no application fix before reproduction.
|
||||
- [x] A-02: nullable explicit inclusion flags across resolver/editor/AI/matcher, deliberate creation/profile selection, empty export validation; pure immutable grouped session history/shortcuts/epoch guards; focused unit/provider/browser and RD21 checks, review, docs, commit/push/verify. Evidence: A_ACCEPTANCE.md, backend108/frontend98/browser4/MariaDB82, TypeScript/lint0; fresh review fixes have RED/GREEN tests. Commit is the source-bearing checkpoint immediately following5d4b8f2.
|
||||
- [ ] B-00: branch from accepted A; inventory9 IDs, render equivalent short/medium/long/dense samples, establish measured spacing/pagination defects and legacy source/PDF proof.
|
||||
- [ ] B-01: opt-in revised design semantics and template-specific spacing/palette/identity fixes; portrait controls reuse secure existing storage/crop with keyboard alternatives; tests and per-template Windows/Linux visual evidence; commit/push/verify.
|
||||
- [x] B-00: branch from accepted A; inventory9 IDs, render equivalent short/medium/long/dense samples, establish measured spacing/pagination defects and legacy source/PDF proof. See TEMPLATE_AUDIT.md.
|
||||
- [x] B-01 implementation/qualification: versioned designs, actual samples, matching letters, secure optional photographs, queue repair and local runtime. Backend1394/frontend1014/MariaDB84/interactive browser6/real Docling2; revised CV36+letter18 per platform and original Linux24+4. Conditional development acceptance and retained large-name oracle failures: B_ACCEPTANCE.md. Commit/push/remote verification closes this checkpoint.
|
||||
- [ ] C-00: branch from accepted B; verify official grid API/licence/compatibility/bundle, define v2 JSON and v1 migration tests.
|
||||
- [ ] C-01: interactive real widget grid/edit/catalogue with geometry, min sizes, keyboard/touch alternatives, conflict-safe persistence; unit/provider/drag/resize/reflow/browser coverage; commit/push/verify.
|
||||
- [ ] D-00: branch from accepted C; define exact safe diagnostic/event schema and admin identity/association policies; tests first.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Template audit and versioned design library
|
||||
|
||||
27 October 2026. Source baseline: accepted A checkpoint `663023a`; local runtime checkpoint `73ea888` is additive. The nine existing theme IDs are retained for saved-document compatibility. New documents and explicit template Apply opt into `designVersion: 2`. Missing/version-one settings retain the original catalogue and rendering. Duplicate/restore preserve their stored design version.
|
||||
|
||||
## Inventory and changes
|
||||
|
||||
Body typography, line height, paper size and default page margins remain theme-specific and unchanged. Existing font embedding and licensing remain authoritative. The revised scale below is in points for the name and millimetres for section/entry gaps.
|
||||
|
||||
| Theme | Family | Name / section / entry | Revised treatment |
|
||||
|---|---|---|---|
|
||||
| Modern | Single column, blue header band | 25 / 5.8 / 4 | 3mm name gap and 5mm header-to-main separation |
|
||||
| Minimal | Plain single column | 25 / 7.5 / 4.8 | Open section/entry rhythm, restrained rules |
|
||||
| Executive | Centred editorial serif | 28 / 6.5 / 4.6 | Centred identity and measured entry spacing |
|
||||
| Technical | Left technical sidebar | 24 / 4.8 / 3.2 | 68mm sidebar, 8mm internal padding, readable teal/white palette |
|
||||
| ATS Classic | Plain text-oriented column | 23 / 5 / 3.4 | 3mm name gap and 5mm header-to-main separation |
|
||||
| Nordic | Right pale sidebar | 23 / 6 / 4.2 | Actual pale catalogue palette; wider identity column |
|
||||
| Elegant | Serif heading, editorial kicker | 27 / 7.5 / 5 | Distinct editorial hierarchy and entry rhythm |
|
||||
| Creative | Left magenta sidebar | 20 / 5.5 / 3.8 | Wider identity column, actual magenta/white palette |
|
||||
| Code | Numbered developer sections | 25.5 / 5.2 / 3.6 | 3mm name gap and 5mm header-to-main separation; mono details |
|
||||
|
||||
Original sidebar usable width was approximately30mm; revised usable width is52mm. Version-two sidebars use fixed page backgrounds plus native print margin boxes, covering every physical page without consuming text-safe margins. Existing pagination/content rules are preserved. No source facts or filler are inserted to fill sparse pages.
|
||||
|
||||
## Real visual specimens
|
||||
|
||||
The same synthetic short English, medium Bokmål, long English and dense Bokmål histories were rendered across all nine designs. They include long names, Norwegian characters, missing optional fields, long bullets, many skills and a deliberately nonhuman rectangle image. The reference profile is identical across themes; all outputs retain source markers and profile/document revision checks.
|
||||
|
||||
| Corpus | Documents | Physical pages | Content/count/bounds |
|
||||
|---|---:|---:|---|
|
||||
| Version-one baseline | 36 | 122 | 36/36 passed |
|
||||
| Revised version two | 36 | 122 | 36/36 passed |
|
||||
| Matching letters, short EN / long NB | 18 | 38 | 18/18 passed |
|
||||
|
||||
Final Windows roots: `.artifacts/pre-phase10-ux/templates-legacy`, `templates-revised-02`, `matching-letters-02`. Each includes source measurements and native PDF bytes. CV contact sheets cover first and last pages for all four histories; letter sheets cover both histories. CV Poppler review includes63 first/last images; letters render all38 pages. Earlier `*-01` roots remain immutable intermediate evidence. Linux qualification is recorded separately when executed.
|
||||
|
||||
Physical page changes: short Creative2→1; long Technical4→5; dense Technical5→6; dense Creative7→6. Other cases retain their page counts. More readable spacing can require additional pages. The total remains122; no content was dropped to reduce page count.
|
||||
|
||||
All43 revised CV sidebar pages pass an independent raster check at two physical edge columns, every pixel row including top/bottom, tolerance±2RGB. Initial version-two specimens still split an unhyphenated surname in Nordic/Creative: the added benchmark reported28/36 passes and8 failures. Their name sizes were adjusted to the measured52mm column; fresh specimens pass36/36 with no unhyphenated name fragments. Name wrapping now improves Technical7→4 lines, Nordic7→4 and Creative8→4. Natural hyphen breaks remain allowed. Arbitrary longer names/custom font sizes cannot be guaranteed; existing typography controls remain available. Minimal can retain a sparse final page.
|
||||
|
||||
## Fidelity and preservation accounting
|
||||
|
||||
The unchanged marker oracle reports8/36 passes and28 S01-only failures in **both** baseline and revised CV sets. The large name marker's DOM Range top differs from the PDF character-cell top by2.05–3.16mm, exceeding the original2mm limit. Other marker position, font, colour and membership checks pass. The tolerance was not relaxed and these failures remain in `fidelity-results.json`.
|
||||
|
||||
Separate actual-ink diagnostics compare the captured preview/PDF foreground pixels: maximum top difference2px baseline and3px revised. Viewed crops support a coordinate metric mismatch for this large marker, rather than demonstrated visible displacement. This diagnostic does not replace the original oracle or human print acceptance.
|
||||
|
||||
Current-source version-one comparison passes36/36 exact CSS (including embedded fonts), normalized HTML, body and marker-order checks. Raw HTML is byte-identical12/36; the other24 differ only in the synthetic avatar data-URI bytes after decoded-pixel normalization. Only image payload bytes are excluded in that explicitly labelled comparison. Original goldens and historical acceptance records are preserved.
|
||||
|
||||
The first letter proof used whole-page line extraction, interleaving sidebar and body text in six documents. Its12/18 result is retained in `matching-letters-01/pdf-proof-linear-reading.json`. Physical column-aware extraction then verifies all18 complete body/recipient/sender/subject texts without changing PDF bytes, marker counts, page dimensions or word-bound checks. Fresh final letter specimens also pass18/18. This is a reading-order correction in the independent proof, not a content tolerance exception.
|
||||
|
||||
## Actual previews and matching letters
|
||||
|
||||
CV selection cards now lazily load the existing server-rendered synthetic sample endpoint and display a scaled sandboxed, noninteractive document. Only synthetic samples are cached; user documents are not placed in the thumbnail cache. The full candidate dialog remains read-only until explicit Apply. Skeleton/failure states are translated. The former test forbidding any thumbnail iframe was intentionally replaced because the owner explicitly requested actual rendered previews; isolation and distinct-theme checks remain covered.
|
||||
|
||||
The letter chooser uses the same nine available/locked themes. Explicit Apply changes the local style snapshot and existing actual letter preview, preserving current text, sender, recipient, date, language and page size. Match CV also copies the reviewed heading font and existing paper-size behaviour. Existing letter revision/save/history owners remain unchanged. Version-two rendering matches palette, fonts and appropriate header/sidebar details; old letter settings remain version one and serialize without added null properties. Shared server-side persistence/preview checks reject new premium-theme activation without the existing ProThemes entitlement, including restore. Legacy settings and currently saved same-theme premium designs remain usable after downgrade. The review-discovered API bypass has5 actual RED failures followed by the full1394-test backend GREEN.
|
||||
|
||||
## Optional portrait safety
|
||||
|
||||
The existing shared profile portrait remains the only image owner. CV controls link to that editor and retain per-CV Show Photo. Crop zoom and both axes have keyboard sliders, labels and existing bounded512px output.
|
||||
|
||||
Uploads now validate actual stream size, decode still PNG/JPEG/WebP, enforce2048px axes/4million pixels, orient EXIF and re-encode PNG without source metadata. Decode concurrency is bounded. Corrupt, truncated, unsupported and animated inputs are rejected. Identity failure retains the old image; ambiguous acknowledgement loss preserves the staged target for reconciliation. Cleanup failure after durable success does not report a false failed save/removal.
|
||||
|
||||
The existing hourly retention hook deletes unreferenced staged/exact legacy portrait files older than24hours, preserving current references and excluding reparse paths. Account export/erasure inventory includes owner-only staged and legacy leftovers. No second image store or master-profile owner was introduced. Decoder dependency: pinned SkiaSharp4.153.1 plus Linux native assets, locked transitive hashes; MIT upstream licence.
|
||||
|
||||
Human accessibility, physical devices, native language review and manual printing remain open. Neither synthetic images nor browser automation establish those qualifications.
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Independent PDF retention/bounds and Poppler contact-sheet proof for 18 letters.
|
||||
|
||||
Keeps the original four-letter corpus and all fidelity tolerances untouched.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import unicodedata
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import pdfplumber
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def normalize(text):
|
||||
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", text)).casefold()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("directory", type=Path)
|
||||
parser.add_argument("--poppler", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
source = json.loads((args.directory / "measurements.json").read_text(encoding="utf-8"))
|
||||
fixtures = []
|
||||
for fixture in source["fixtures"]:
|
||||
directory = args.directory / fixture["fixture"]
|
||||
filename = directory / "export.pdf"
|
||||
failures, pages, markers, texts = [], [], [], []
|
||||
columns = [[], []]
|
||||
sidebar = fixture["measured"]["pageDisplay"] == "grid"
|
||||
with pdfplumber.open(filename) as document:
|
||||
for number, page in enumerate(document.pages, 1):
|
||||
text = page.extract_text() or ""
|
||||
texts.append(text)
|
||||
if sidebar:
|
||||
# PDF line extraction otherwise interleaves separate sidebar/body columns.
|
||||
boundary = (210 - 68 if fixture["settings"]["themeId"] == "nordic" else 68) * 72 / 25.4
|
||||
for index, bounds in enumerate(((0, 0, boundary, page.height), (boundary, 0, page.width, page.height))):
|
||||
columns[index].append(page.crop(bounds).extract_text() or "")
|
||||
markers.extend(re.findall(r"\b[SEBPR]\d{2}\b", text))
|
||||
words = page.extract_words()
|
||||
outside = [word for word in words if word["x0"] < -1.42 or word["top"] < -1.42
|
||||
or word["x1"] > page.width + 1.42 or word["bottom"] > page.height + 1.42]
|
||||
if outside or not words:
|
||||
failures.append(f"page {number}: {len(outside)} outside words; {len(words)} total words")
|
||||
if abs(page.width * 25.4 / 72 - 210) > .5 or abs(page.height * 25.4 / 72 - 297) > .5:
|
||||
failures.append(f"page {number}: unexpected A4 dimensions")
|
||||
pages.append({"page": number, "widthPt": page.width, "heightPt": page.height, "words": len(words), "outsideWords": outside})
|
||||
combined = normalize("\n".join("\n".join(column) for column in columns) if sidebar else "\n".join(texts))
|
||||
for text in fixture["expectedParagraphs"] + [fixture["expectedRecipient"], fixture["expectedSender"], fixture["expectedSubject"]]:
|
||||
if normalize(text) not in combined:
|
||||
failures.append("Missing or changed source text: " + text[:90])
|
||||
if Counter(markers) != Counter(fixture["expectedMarkers"]):
|
||||
failures.append("PDF/source marker multisets differ")
|
||||
if len(pages) != fixture["previewPages"]:
|
||||
failures.append("PDF/preview page count differs")
|
||||
for page in range(1, len(pages) + 1):
|
||||
subprocess.run([str(args.poppler), "-f", str(page), "-l", str(page), "-singlefile", "-scale-to", "1400", "-png", str(filename), str(directory / f"poppler-{page}")], check=True, capture_output=True)
|
||||
fixtures.append({"fixture": fixture["fixture"], "pass": not failures, "failures": failures,
|
||||
"pdfSha256": hashlib.sha256(filename.read_bytes()).hexdigest(), "pages": pages,
|
||||
"expectedMarkers": len(fixture["expectedMarkers"]), "actualMarkers": len(markers)})
|
||||
report = {"total": len(fixtures), "passed": sum(item["pass"] for item in fixtures),
|
||||
"pages": sum(len(item["pages"]) for item in fixtures), "fixtures": fixtures,
|
||||
"qualification": "Finite local synthetic retention, page dimensions/count and physical word bounds; sidebar text extracted by physical columns to avoid line interleaving. Not human print acceptance."}
|
||||
(args.directory / "pdf-proof.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
for language in ("short-en", "long-nb"):
|
||||
for edge in ("first", "last"):
|
||||
sheet = Image.new("RGB", (900, 1365), "#e5e7eb")
|
||||
draw = ImageDraw.Draw(sheet)
|
||||
for index, fixture in enumerate([item for item in fixtures if item["fixture"].startswith(language + "-")]):
|
||||
number = 1 if edge == "first" else len(fixture["pages"])
|
||||
with Image.open(args.directory / fixture["fixture"] / f"poppler-{number}.png") as image:
|
||||
image.thumbnail((286, 414))
|
||||
x, y = (index % 3) * 300 + 7, (index // 3) * 455 + 32
|
||||
sheet.paste(image, (x, y))
|
||||
draw.text((x, y - 25), f"{fixture['fixture']} p{number}/{len(fixture['pages'])}", fill="#111827")
|
||||
sheet.save(args.directory / f"contact-{language}-{edge}.png")
|
||||
print(f"Matching letter physical proof: {report['passed']}/{report['total']} documents, {report['pages']} pages")
|
||||
for item in fixtures:
|
||||
if not item["pass"]:
|
||||
print(item["fixture"], item["failures"])
|
||||
raise SystemExit(0 if report["passed"] == report["total"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from '../../job-tracker-ui/node_modules/@playwright/test';
|
||||
import base from '../cv-redesign/linux/playwright.config';
|
||||
|
||||
// Keep the original 24 CVs/four letters and their assertions in this same native lane.
|
||||
export default defineConfig({ ...base,
|
||||
testMatch: ['cv-readiness.spec.ts', 'cv-settings-presentation.spec.ts', 'cover-letter-document-fidelity.spec.ts',
|
||||
'pre-p10-template-library.spec.ts', 'pre-p10-letter-library.spec.ts'],
|
||||
grep: /native preview\/PDF fidelity corpus|document controls preserve settings|cover letter native preview\/PDF fidelity|\[PRE-P10 B\]/,
|
||||
metadata: { ...base.metadata, revisedCvDocuments: 36, matchingLetterDocuments: 18 },
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Check that the benchmark's unhyphenated name words are not broken into fragments."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("directory", type=Path)
|
||||
args = parser.parse_args()
|
||||
source = json.loads((args.directory / "measurements.json").read_text(encoding="utf-8"))
|
||||
fixtures = [{"fixture": row["fixture"], "brokenWords": [word for word in row["measured"]["splitNameWords"] if "-" not in word]}
|
||||
for row in source["fixtures"]]
|
||||
result = {"total": len(fixtures), "passed": sum(not row["brokenWords"] for row in fixtures), "fixtures": fixtures,
|
||||
"limits": "Finite synthetic name fixture only; natural hyphen breaks permitted, arbitrary names/custom font sizes not guaranteed."}
|
||||
(args.directory / "name-wrap-proof.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
raise SystemExit(0 if result["passed"] == result["total"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Separate DOM Range/PDF character-cell metrics from visible raster ink alignment.
|
||||
|
||||
This leaves the original 2mm fidelity oracle and all its failures unchanged.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("directory", type=Path)
|
||||
args = parser.parse_args()
|
||||
source = json.loads((args.directory / "measurements.json").read_text(encoding="utf-8"))
|
||||
oracle = json.loads((args.directory / "fidelity-results.json").read_text(encoding="utf-8"))
|
||||
results = []
|
||||
for fixture in source["fixtures"]:
|
||||
directory = args.directory / fixture["fixture"]
|
||||
marker = next(item for item in fixture["measured"]["tokens"] if item["token"] == "S01")
|
||||
rect = next(item for item in fixture["measured"]["nameLines"] if item["text"] == "S01")
|
||||
foreground = tuple(map(int, re.findall(r"\d+", marker["color"])[:3]))
|
||||
bounds = (math.floor(rect["x"]), math.floor(rect["y"]), math.ceil(rect["x"] + rect["width"]), math.ceil(rect["y"] + rect["height"] + 12))
|
||||
boxes = {}
|
||||
for kind, filename in (("preview", "preview-1.png"), ("pdf", "pdf-1.png")):
|
||||
with Image.open(directory / filename) as image:
|
||||
crop = image.convert("RGB").crop(bounds)
|
||||
pixels = [(x, y) for y in range(crop.height) for x in range(crop.width)
|
||||
if max(abs(a - b) for a, b in zip(crop.getpixel((x, y)), foreground)) <= 50]
|
||||
if not pixels:
|
||||
raise ValueError(f"No foreground ink for {fixture['fixture']} {kind}")
|
||||
boxes[kind] = [min(x for x, y in pixels) + bounds[0], min(y for x, y in pixels) + bounds[1],
|
||||
max(x for x, y in pixels) + bounds[0], max(y for x, y in pixels) + bounds[1]]
|
||||
crop.resize((crop.width * 4, crop.height * 4)).save(directory / f"S01-{kind}-ink.png")
|
||||
original = next(item for item in oracle["fixtures"] if item["fixture"] == fixture["fixture"])
|
||||
comparison = next(item for item in original["comparisons"] if item["token"] == "S01")
|
||||
results.append({"fixture": fixture["fixture"], "domRange": rect, "foregroundRgb": foreground,
|
||||
"originalOracleRangeVsPdfCharacterCellDeltaMm": comparison["deltaYmm"],
|
||||
"visibleInkBoundsPx": boxes, "visibleInkTopDeltaPx": abs(boxes["preview"][1] - boxes["pdf"][1]),
|
||||
"visibleInkTopDeltaMm": abs(boxes["preview"][1] - boxes["pdf"][1]) * 25.4 / 96})
|
||||
report = {"qualification": "Separate diagnostic measurements only. Pixels within 50 per RGB channel of the recorded foreground identify visible ink in the S01 ROI. Original fidelity tolerances/failures are unchanged.",
|
||||
"fixtures": results, "maxVisibleInkTopDeltaPx": max(item["visibleInkTopDeltaPx"] for item in results)}
|
||||
(args.directory / "name-box-metrics.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"S01 raster-ink diagnostic: {len(results)} documents, max top delta {report['maxVisibleInkTopDeltaPx']}px; original oracle unchanged")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Independent byte-bound completeness/bounds report; no presentation-quality goldens.
|
||||
|
||||
Run after the producer and unchanged scripts/verify-cv-fidelity.py. All reported
|
||||
whitespace/name-wrap observations remain measurements for human review.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import pdfplumber
|
||||
|
||||
|
||||
def inspect(root, fixture):
|
||||
filename = root / fixture["fixture"] / "export.pdf"
|
||||
failures, pages, markers = [], [], []
|
||||
with pdfplumber.open(filename) as document:
|
||||
for index, page in enumerate(document.pages):
|
||||
words = page.extract_words()
|
||||
text = page.extract_text() or ""
|
||||
markers.extend(re.findall(r"\b[SEBPR]\d{2}\b", text))
|
||||
outside = [word for word in words if word["x0"] < -1.42 or word["top"] < -1.42
|
||||
or word["x1"] > page.width + 1.42 or word["bottom"] > page.height + 1.42]
|
||||
if outside:
|
||||
failures.append(f"page {index + 1}: {len(outside)} words outside physical page by more than 0.5mm")
|
||||
if not words:
|
||||
failures.append(f"page {index + 1}: blank output")
|
||||
top = min((word["top"] for word in words), default=0)
|
||||
bottom = max((word["bottom"] for word in words), default=0)
|
||||
pages.append({"page": index + 1, "wordCount": len(words), "widthPt": page.width,
|
||||
"heightPt": page.height, "textTopPt": top, "textBottomPt": bottom,
|
||||
"occupiedVerticalFraction": (bottom - top) / page.height,
|
||||
"trailingWhitespaceFraction": (page.height - bottom) / page.height,
|
||||
"outsideWords": outside, "text": text,
|
||||
"embeddedImages": len(page.images)})
|
||||
if Counter(markers) != Counter(fixture["expectedMarkers"]):
|
||||
failures.append("PDF marker multiset differs from independent source fixture")
|
||||
if len(pages) != fixture["previewPages"]:
|
||||
failures.append("PDF/preview page count differs")
|
||||
return {"fixture": fixture["fixture"], "pass": not failures, "failures": failures,
|
||||
"pdfSha256": hashlib.sha256(filename.read_bytes()).hexdigest(), "expectedMarkerCount": len(fixture["expectedMarkers"]),
|
||||
"actualMarkerCount": len(markers), "nameLineCount": fixture["measured"]["nameLineCount"],
|
||||
"splitNameWords": fixture["measured"]["splitNameWords"], "pages": pages}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("directory", type=Path)
|
||||
args = parser.parse_args()
|
||||
measurements = json.loads((args.directory / "measurements.json").read_text(encoding="utf-8"))
|
||||
fixtures = [inspect(args.directory, item) for item in measurements["fixtures"]]
|
||||
result = {"measurementSha256": hashlib.sha256((args.directory / "measurements.json").read_bytes()).hexdigest(),
|
||||
"designVersion": measurements["designVersion"], "total": len(fixtures),
|
||||
"passed": sum(item["pass"] for item in fixtures), "pages": sum(len(item["pages"]) for item in fixtures),
|
||||
"qualification": "Finite synthetic completeness and physical bounds only; whitespace and name wrapping require visual review.",
|
||||
"fixtures": fixtures}
|
||||
(args.directory / "pdf-proof.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"Source-bound PDF completeness/bounds: {result['passed']}/{result['total']} documents, {result['pages']} pages")
|
||||
for item in fixtures:
|
||||
if not item["pass"]:
|
||||
print(item["fixture"], item["failures"])
|
||||
raise SystemExit(0 if result["passed"] == result["total"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Inspect actual PDF raster pixels at both sidebar edges on every exported page."""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("directory", type=Path)
|
||||
args = parser.parse_args()
|
||||
source = json.loads((args.directory / "measurements.json").read_text(encoding="utf-8"))
|
||||
results = []
|
||||
for fixture in source["fixtures"]:
|
||||
if not fixture["fixture"].endswith(("-technical", "-creative", "-nordic")):
|
||||
continue
|
||||
html = (args.directory / fixture["fixture"] / "render.html").read_text(encoding="utf-8")
|
||||
color = re.search(r"--cv-sidebar-bg:(#[0-9a-fA-F]{6})", html)
|
||||
if not color:
|
||||
continue
|
||||
expected = tuple(int(color[1][index:index + 2], 16) for index in (1, 3, 5))
|
||||
for number in range(1, fixture["previewPages"] + 1):
|
||||
with Image.open(args.directory / fixture["fixture"] / f"pdf-{number}.png") as image:
|
||||
image = image.convert("RGB")
|
||||
# 2px and 8px lie inside the bleed and outside the 8mm text padding.
|
||||
columns = [image.width - 3, image.width - 9] if fixture["fixture"].endswith("-nordic") else [2, 8]
|
||||
samples = []
|
||||
for x in columns:
|
||||
mismatched = [y for y in range(image.height) if max(abs(a - b) for a, b in zip(image.getpixel((x, y)), expected)) > 2]
|
||||
samples.append({"x": x, "expectedRgb": expected, "height": image.height,
|
||||
"mismatchedRows": len(mismatched), "firstMismatchRows": mismatched[:8],
|
||||
"topPixel": image.getpixel((x, 0)), "bottomPixel": image.getpixel((x, image.height - 1))})
|
||||
results.append({"fixture": fixture["fixture"], "page": number, "pass": all(sample["mismatchedRows"] == 0 for sample in samples), "samples": samples})
|
||||
report = {"rasterizer": "pypdfium2 via unchanged verify-cv-fidelity.py, actual exported PDF bytes",
|
||||
"colorTolerancePerChannel": 2, "passedPages": sum(item["pass"] for item in results), "pages": len(results), "results": results}
|
||||
(args.directory / "sidebar-pixels.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"Full-height sidebar raster coverage: {report['passedPages']}/{report['pages']} pages")
|
||||
|
||||
# Contact sheets use independent Poppler first/last rasterizations.
|
||||
for sample in ("short-en", "medium-nb", "long-en", "dense-nb"):
|
||||
fixtures = [item for item in source["fixtures"] if item["fixture"].startswith(sample + "-")]
|
||||
for edge in ("first", "last"):
|
||||
sheet = Image.new("RGB", (900, 1365), "#e5e7eb")
|
||||
draw = ImageDraw.Draw(sheet)
|
||||
for index, fixture in enumerate(fixtures):
|
||||
page = 1 if edge == "first" else fixture["previewPages"]
|
||||
filename = args.directory / fixture["fixture"] / f"poppler-{page}.png"
|
||||
if not filename.exists():
|
||||
raise FileNotFoundError(filename)
|
||||
with Image.open(filename) as image:
|
||||
image = image.convert("RGB")
|
||||
image.thumbnail((286, 414))
|
||||
x, y = (index % 3) * 300 + 7, (index // 3) * 455 + 32
|
||||
sheet.paste(image, (x, y))
|
||||
draw.text((x, y - 25), f"{fixture['fixture']} p{page}/{fixture['previewPages']}", fill="#111827")
|
||||
sheet.save(args.directory / f"contact-{sample}-{edge}.png")
|
||||
raise SystemExit(0 if report["passedPages"] == report["pages"] and report["pages"] > 0 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user