diff --git a/.gitattributes b/.gitattributes index 1ce50156..7ce09dae 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,6 +14,10 @@ # client's autocrlf setting, because the 7-Zip manifest hashes their bytes. tests/7zip-vectors/*.txt text eol=lf +# Delivery Handoff parser corpus inputs are byte-exact: the probe compares findings +# by line and column, and some hostile inputs deliberately mix encodings. +tests/handoff-specs/** -text + # Denote all files that are truly binary and should not be modified. # (binary is a macro for -text -diff) *.zip binary diff --git a/.github/workflows/nightly-parser-fuzz.yml b/.github/workflows/nightly-parser-fuzz.yml index 6beb22f1..867773e3 100644 --- a/.github/workflows/nightly-parser-fuzz.yml +++ b/.github/workflows/nightly-parser-fuzz.yml @@ -33,3 +33,8 @@ jobs: - name: Replay Markdown extension-combination corpus shell: pwsh run: .\tools\test-cmark-gfm-hardening.ps1 -Iterations 250 + + - name: Replay Handoff specification hostile corpus + shell: pwsh + # Deterministic mutations of every delivery specification input, as in the bzip2 soak. + run: .\tools\test-handoff-spec-parser.ps1 -Architecture x64 -Iterations 250 diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index 6907f255..5936d75e 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -134,6 +134,11 @@ jobs: if: matrix.platform == 'x64' run: .\tools\test-cmark-gfm-hardening.ps1 + - name: Exercise Handoff specification parser corpus + if: matrix.platform == 'x64' + # Same invocation as scripts/runtests.ps1: shared specifications are untrusted input. + run: .\tools\test-handoff-spec-parser.ps1 -Architecture x64 + - name: Register MSVC problem matcher # This reviewed commit replaces the mutable master branch in PR builds. uses: ammaraskar/msvc-problem-matcher@541aa4360062d76d9620af6b8bd35cfa0c5ef19f diff --git a/README.md b/README.md index f8479f56..3803888e 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ The highlights below cover repository work from **March through September 2026** - FTP downloads keep an existing local file intact while receiving data into a unique sibling `.salftp-*.part` file. The matching `.meta` file records ownership and verified resume checkpoints. A move deletes its remote source only after local validation, flush, metadata, publication, and checked completion succeed. Cancellation or failure can leave these private files for a later retry; keep them together. Resume requires a matching remote version and unchanged local evidence. Legacy, corrupted, named-stream, reparse-point, or ambiguous publication states require manual inspection; a `.previous` sibling retains the old destination when publication cleanup cannot finish. Restart never replays remote deletion. - Bundled engines were upgraded: [7-Zip 26.02](src/plugins/7zip/doc/upgrade-26.02.md), zlib, SQLite (with defined recovery behavior), bzip2, and cmark-gfm. - UnRAR plugin loading was restored; 64-bit file-size handling was applied to the active plug-in readers. +- New **Delivery Handoff** plug-in builds and verifies professional delivery packages: a reusable `*.handoff.json` specification defines required documents, naming, formats, PDF page sizes, image dimensions, allowed content, and approval and licence evidence; the plug-in assembles candidates from the working panel, flags omissions, copies them into a hash-verified package in the other panel, and writes `manifest.json`, `manifest.csv`, and `CONTENTS.txt`. ### Build, tests, and releases diff --git a/architecture.md b/architecture.md index 0412bdac..f6442b92 100644 --- a/architecture.md +++ b/architecture.md @@ -401,7 +401,7 @@ Representative plug-in families in the solution are: - archive/compression: 7-Zip, ZIP, TAR, PAK, ARJ, CAB, CHM, FAT, ISO, LHA, MIME, OLE, and RAR readers/writers; - virtual filesystems: FTP, Network Neighborhood, Folders, Registry Editor, portable devices, and Windows Mobile; - viewers/analyzers: database, browser, multimedia, PE, image, disk-map, and file-comparison tools; -- utilities: automation, checksum, version check, renamer, and split/combine; +- utilities: automation, checksum, delivery handoff, version check, renamer, and split/combine; - SDK demonstrations: archive/menu/viewer/sample plug-ins. ### 5.4 Archive integration diff --git a/design/icon-reference/fluent-color-essentials/fluent-map.json b/design/icon-reference/fluent-color-essentials/fluent-map.json index a8776da8..2a91c83b 100644 --- a/design/icon-reference/fluent-color-essentials/fluent-map.json +++ b/design/icon-reference/fluent-color-essentials/fluent-map.json @@ -88,6 +88,7 @@ "PluginDiskRecovery": "arrow_counterclockwise", "PluginExecutable": "window_dev_tools", "PluginFolders": "app_folder", + "PluginHandoff": "box", "PluginHelp": "book", "PluginMedia": "video", "PluginNetwork": "desktop_sync", diff --git a/design/icon-reference/fluent-color-essentials/fluent-provenance.json b/design/icon-reference/fluent-color-essentials/fluent-provenance.json index df4c760d..3fafe4b7 100644 --- a/design/icon-reference/fluent-color-essentials/fluent-provenance.json +++ b/design/icon-reference/fluent-color-essentials/fluent-provenance.json @@ -1713,6 +1713,26 @@ "sha256": "8c25eaf69cc9156f0c44cb991745fd8645806fa88c4aa0a1f228f3816816d6ca" } }, + "PluginHandoff": { + "16": { + "source": "box_16_regular.svg", + "sourceSize": 16, + "color": "#0F6CBD", + "sha256": "53fe38c2788d994131408de03cf32761249d519e22d22a1fe579728c2fd3dcfa" + }, + "24": { + "source": "box_24_regular.svg", + "sourceSize": 24, + "color": "#0F6CBD", + "sha256": "0462b5ce36b275c9f6868eecc73eede4df999dc227e3fcac1edeb44b0e3a7e60" + }, + "32": { + "source": "box_32_regular.svg", + "sourceSize": 32, + "color": "#0F6CBD", + "sha256": "a0287790bd89ba57e643d9f67dca368d2a1dc67ff2e26cac54282e5d82e5f33e" + } + }, "PluginHelp": { "16": { "source": "book_16_regular.svg", diff --git a/design/icon-reference/fluent-color-essentials/svg/PluginHandoff.svg b/design/icon-reference/fluent-color-essentials/svg/PluginHandoff.svg new file mode 100644 index 00000000..c5a6ed78 --- /dev/null +++ b/design/icon-reference/fluent-color-essentials/svg/PluginHandoff.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/handoff-spec.md b/handoff-spec.md new file mode 100644 index 00000000..351bc93b --- /dev/null +++ b/handoff-spec.md @@ -0,0 +1,1839 @@ +# Delivery Handoff — Implementation Specification + +Build and verify a professional delivery package: one pane holds the working material, the other receives a staged delivery package that is assembled, checked, and listed according to a reusable delivery specification. + +| Item | Value | +| --- | --- | +| Document | `handoff-spec.md` | +| Status | Ready for implementation (v1) | +| Date | 2026-10-03 | +| Baseline | `main` at `165a2c9` (Open Salamander 6.0 line) | +| Deliverable | New menu-extension plug-in `handoff.spl` (display name **Delivery Handoff**), its language modules, help, tests, CI and documentation updates | +| SDK impact | None. Uses the existing plug-in SDK (`LAST_VERSION_OF_SALAMANDER`); no ABI change | +| Supported OS | The installer floor, Windows 10 1809 (`Installer/setup.iss:38`, `MinVersion=10.0.17763`) and later; Win32 and x64 builds | + +## Contents + +- [How to use this document](#how-to-use-this-document) +- [Decision record](#decision-record) +- [Part A — Feature overview and user guide](#part-a--feature-overview-and-user-guide) +- [Part B — Implementation task list](#part-b--implementation-task-list) +- [Part C — Normative reference](#part-c--normative-reference) + +## How to use this document + +- **Part A** explains the feature and how professionals use it. Read it first; it defines the observable behavior. +- **Part B** is the implementation task list. Every `- [ ]` item is a unit of work that an implementer checks off when its acceptance criteria are met. Phases are ordered by dependency; tasks within a phase may run in parallel unless a dependency is stated. +- **Part C** is the normative reference — formats, algorithms, catalogues, UI control maps, and file inventories. Tasks cite it as `→ C.x.y`. +- The keywords MUST, MUST NOT, SHOULD, and MAY follow RFC 2119. +- Repository rules from `AGENTS.md` apply to every task and are not repeated per task: + - every source change carries a concise nearby comment documenting its intent, invariant, or compatibility constraint; + - `scripts/runtests.ps1` and the GitHub Actions pipelines stay in parity (toolchain, prerequisites, filters, ordering, skip policy); + - completion requires a full `scripts/runtests.ps1` run under pipeline conditions, reported with its invocation and result; + - builds use the Visual Studio 2026 developer environment (MSVC `v145`). +- When an implementation deviates from this document, update the document in the same change and record the reason in [C.13](#c13-risks-and-open-items). + +## Decision record + +| ID | Decision | Rationale | +| --- | --- | --- | +| D-01 | Implement the feature as an in-process **menu-extension plug-in** `handoff` (`src/plugins/handoff`). | The SDK already exposes everything required (panel paths, selection, focus, refresh notification, configuration persistence, help, language modules, UI-test menu IDs). The feature stays optional, isolated from core coupling, and needs no ABI change. Feasibility evidence: [C.1](#c1-feasibility-and-architecture). | +| D-02 | The delivery specification is **JSON** (`*.handoff.json`), strict RFC 8259 plus ignorable `$comment` members, read by a bounded reader inside the plug-in. | Chosen by the product owner. Diff-friendly, expresses nested rules; no new third-party dependency; bounded parsing suits untrusted, shareable files. | +| D-03 | A package contains **`manifest.json` + `manifest.csv` + `CONTENTS.txt`**. | Chosen by the product owner. Machine verification, spreadsheet review, and a plain client-facing contents list. | +| D-04 | PDF checks are a **bounded structural scan plus page count and page sizes through `Windows.Data.Pdf`**. | Chosen by the product owner. No PDF library to vendor; page-size rules (A1/A3/Letter/ARCH) are central for architects and print production. Validated by spike T0.3. | +| D-05 | v1 authoring is **text + validator + embedded templates**; a GUI rule editor is deferred. | Chosen by the product owner. Keeps the UI scope and test surface bounded. | +| D-06 | Working material is **never modified, moved, or renamed**. Staging is copy-only into a **new** package folder; an existing package is never overwritten. | Delivery must not put work at risk; reproducible revisions (`_R01`, `_R02`). | +| D-07 | Packages are built in a hidden **partial sibling folder**, verified, then **published by a handle-bound directory rename** that refuses to replace an existing entry. | An interrupted or failed build never leaves a half package under the final name; mirrors the repository's durable-commit discipline. | +| D-08 | Hashes are **SHA-256 through Windows CNG** (`BCrypt*`). | No third-party cryptography code; FIPS-validated provider. | +| D-09 | Specification templates are **embedded as `RCDATA` in `handoff.spl`**. | The installer stages only build outputs (`tools/prepare_installer.ps1:118-151`); data files beside the source tree would not ship. | +| D-10 | Client-facing outputs exclude absolute paths, user names, and machine names by default; an internal **build record** is written **beside** the package, not inside it. | Privacy and professional hygiene; full audit trail stays with the producer. | +| D-11 | The processing **engine is host-independent** (Windows SDK + C++ standard library only) and is compiled by the plug-in, by a native test executable, and by a `cl.exe` parser probe. | Deterministic native tests without the host; hosted nightly soak uses plain `cl.exe` like the cmark/bzip2 probes. | +| D-12 | The plug-in UI runs as a **modeless top-level window on a plug-in-owned UI thread**; scanning, probing, and building run on **stop-aware worker threads**. | Panels remain usable during review ("Show in panel"); long work never blocks the host message loop. | + +--- + +# Part A — Feature overview and user guide + +## A.1 Summary + +Designers, architects, consultants, and production teams repeatedly hand off packages whose requirements are predictable: which documents must be present, how files are named, which formats and image sizes are acceptable, what must not be included, and which supporting assets (fonts, stock images, licences) travel with the deliverables. Today this is assembled by hand — copy, rename, open, check, write a contents list — and errors are common: a missing sheet, a superseded revision, a 72-dpi image, an unlicensed font, `Thumbs.db`, internal drafts. + +**Delivery Handoff** turns that routine into one command. The active pane holds the working material; the inactive pane is the staging location. A reusable **delivery specification** describes the package. The plug-in assembles candidates from the working material, flags omissions and violations for review, copies the approved selection into a new package folder with conventional names, verifies every byte, and writes a manifest and contents list. The outcome is concrete: **a package ready for review and delivery**. + +The motivating request: + +> "Prepare the client delivery: approved PDFs, source artwork, licensed assets, and a contents list." + +| Part of the request | Specification construct | What the plug-in checks | +| --- | --- | --- | +| approved PDFs | rule with `role: deliverable`, `formats: ["pdf"]`, `approval`, `select.latestRevision` | approval evidence and/or reviewer confirmation, valid unencrypted PDF, page count/sizes, latest revision only | +| source artwork | rule with `role: source`, formats `ai`, `psd`, `svg`, `eps`, … | content matches extension, raster dimensions, naming | +| licensed assets | rules with `role: asset` and `license.required` | licence sidecar or register entry, expiry, permitted scope; evidence copied with the asset | +| a contents list | `package.outputs` | `CONTENTS.txt`, `manifest.csv`, `manifest.json` with SHA-256 for every file | + +## A.2 Users and professional scenarios + +| Profession | Typical package | Requirements the specification encodes | +| --- | --- | --- | +| Brand / graphic design studio | Brand handoff: logos, guidelines PDF, fonts, campaign artwork | logo formats (SVG/AI/PNG at exact pixel sizes), font licences, latest guideline version, no working files | +| Architecture / engineering | Drawing issue: sheet PDFs, DWG/IFC models, drawing register, transmittal list | every sheet in the register present, latest revision only, sheet sizes (A1/A3, ARCH D), naming `PRJ-DISC-SHEET-REV` | +| Consultancy | Report delivery: final report PDF, appendices, data files | approved final only, no drafts/comments files, PDF not password-protected | +| Print and production | Print-ready files | page size with tolerance, raster images ≥ 300 dpi, CMYK colour model, no RGB in print folder | + +## A.3 Goals and non-goals + +Goals: + +- **G1** Assemble a delivery package from working material according to a reusable specification, with no manual copying or renaming. +- **G2** Flag omissions (missing required documents or expected items) before anything is staged. +- **G3** Check naming conventions, formats, PDF page constraints, image dimensions/resolution/colour, allowed content, approvals, and licences. +- **G4** Let a reviewer see, include/exclude, approve, and acknowledge before building. +- **G5** Produce a staged package that is complete, byte-verified, and atomically published. +- **G6** Produce a machine-readable manifest and a human-readable contents list. +- **G7** Re-verify a staged package later (before upload or after it was handled by others). +- **G8** Make specifications reusable across projects and shareable across a team. + +Non-goals for v1 (candidates for later versions are listed in [C.13](#c13-risks-and-open-items)): + +- A graphical specification editor (D-05). +- Sources or targets in archives or plug-in file systems (ZIP, FTP, SFTP, portable devices). Both panes MUST be Windows file-system paths. +- Compressing, uploading, e-mailing, or transmitting the package (users can still use **Files → Pack** on the result). +- Changing content: resizing or converting images, flattening/optimizing PDFs, stripping metadata, embedding fonts. +- PDF/A or PDF/X preflight, font-embedding checks, and inspection of PDF content (annotations, layers, comments). +- Digital signing; multi-user approval workflows. +- Updating an already published package in place (a rebuild creates a new revision folder). + +## A.4 Glossary + +| Term | Meaning | +| --- | --- | +| Working pane / working root | The active (source) panel and its current Windows path. Working material is read-only to the plug-in. | +| Staging pane / staging location | The inactive (target) panel and its current Windows path. The package folder is created here. | +| Delivery specification (spec) | A `*.handoff.json` file describing variables, package naming, rules, naming policy, allowed content, and outputs ([C.4](#c4-delivery-specification-format-v1)). | +| Rule | One expected kind of content (for example "Approved PDFs"); selects candidates and constrains them. | +| Candidate | A working file selected by a rule. | +| Finding | A diagnostic with severity **error**, **warning**, or **info** and a stable code ([C.6](#c6-findings-catalogue)). | +| Omission | A finding that something required is missing (rule empty, count below minimum, expected item absent). | +| Evidence | Proof of approval or licence: path/name pattern, sidecar file, or register CSV entry. | +| Package | The published folder in the staging location containing the deliverables and outputs. | +| Manifest | `manifest.json` (authoritative) and `manifest.csv` listing every file with SHA-256. | +| Contents list | `CONTENTS.txt`, the client-facing listing. | +| Build record | `.handoff-build.json` beside the package: internal audit trail (source paths, reviewer decisions, spec snapshot). | +| Partial folder | The hidden `..handoff-.partial` folder in which a package is built before publication. | + +## A.5 User guide + +### A.5.1 Quick start: "Prepare the client delivery…" + +Working material (left pane, `D:\Projects\ACME-Rebrand\`): + +```text +ACME-Rebrand\ + .handoff\client-delivery.handoff.json ← the reusable specification + Approved\Brand-Guidelines_v3.pdf + Approved\Brand-Guidelines_v4.pdf + Approved\Stationery_v2.pdf + Artwork\Final\Logo\ACME-logo-primary.ai + Artwork\Final\Logo\ACME-logo-mono.svg + Artwork\Final\Campaign\Hero.psd + Artwork\WIP\Hero-explorations.psd ← not selected by any rule + Assets\Fonts\Inter-Regular.otf + Assets\licences.csv ← licence register + Assets\Stock\city-skyline.jpg + Assets\Stock\city-skyline.licence.pdf ← licence sidecar + Thumbs.db +``` + +Staging location (right pane): `D:\Deliveries\`. + +1. Navigate the left pane to `D:\Projects\ACME-Rebrand` and the right pane to `D:\Deliveries`. Keep the left pane active. +2. Choose **Plugins → Delivery Handoff → Build Delivery Package…**. +3. In **Build Delivery Package**, the specification `client-delivery.handoff.json` is preselected because it was found in the working folder's `.handoff` folder. Enter the variables the specification asks for — Client `ACME`, Project `Rebrand`, Revision `R01`. The package name preview shows `ACME-Rebrand_Delivery_2026-10-03_R01`. Choose **Scan…**. +4. **Review Delivery Package** opens while the plug-in scans and inspects candidates: + - *Approved PDFs*: `Brand-Guidelines_v4.pdf` and `Stationery_v2.pdf` are selected; `Brand-Guidelines_v3.pdf` is shown as *superseded*. The specification requires reviewer confirmation, so both show *Awaiting approval*. Select the rule and choose **Approve all in rule**. + - *Source artwork*: `Hero.psd` reports error `HO-IMG-013` — its long edge is 1 800 px, the rule requires at least 2 000 px. Replace the file in the working folder, then choose **Re-scan**. + - *Licensed stock images*: `city-skyline.jpg` (4 000 × 2 667 px, 300 dpi) has licence evidence `city-skyline.licence.pdf`, which will be copied into the package. + - *Licensed fonts*: `Inter-Regular.otf` matches the register row in `Assets\licences.csv` (licence *OFL-1.1*, scope *Unlimited*). + - *Not included*: `Artwork\WIP\Hero-explorations.psd` and other unclaimed files are listed for information only; `Thumbs.db` is listed as forbidden content that will never be packaged. +5. When no errors remain, tick **I have reviewed all warnings** (only shown when warnings exist) and choose **Build**. A progress bar shows each file being copied and verified. +6. The result reads **Verified — ready for review**. The right pane refreshes and focuses the new folder: + +```text +D:\Deliveries\ + ACME-Rebrand_Delivery_2026-10-03_R01\ + 01_Approved_PDFs\ACME-Rebrand_Brand-Guidelines_v4.pdf + 01_Approved_PDFs\ACME-Rebrand_Stationery_v2.pdf + 02_Source_Artwork\Logo\ACME-logo-primary.ai + 02_Source_Artwork\Logo\ACME-logo-mono.svg + 02_Source_Artwork\Campaign\Hero.psd + 03_Licensed_Assets\Fonts\Inter-Regular.otf + 03_Licensed_Assets\Images\city-skyline.jpg + 03_Licensed_Assets\Licences\city-skyline.licence.pdf + CONTENTS.txt + manifest.csv + manifest.json + ACME-Rebrand_Delivery_2026-10-03_R01.handoff-build.json ← internal record, not for the client +``` + +7. Before uploading the folder (or after someone else touched it), switch to the staging pane (Tab), focus the package folder, and choose **Plugins → Delivery Handoff → Verify Delivery Package…** (verification acts on the active pane). + +### A.5.2 Commands + +All commands are in **Plugins → Delivery Handoff**. No default hot keys are assigned (users can assign them in Plugins Manager → Keyboard Shortcuts). + +| Command | Enabled when | What it does | +| --- | --- | --- | +| **Build Delivery Package…** | Both panes show Windows paths | Opens the build session for the active pane (working) and inactive pane (staging). | +| **Verify Delivery Package…** | The active pane shows a Windows path | Verifies the focused folder if it contains a manifest, otherwise the current folder; otherwise asks for a folder. | +| **Validate Specification…** | Always | Validates the focused `*.handoff.json` or a chosen file; lists errors with line and column. | +| **New Specification from Template…** | The active pane shows a Windows path | Writes a starter specification from an embedded template (default location `\.handoff\`). | +| **Configuration** (Plugins Manager) | Always | Specification library folder, recent specifications, review and staging preferences. | + +### A.5.3 Where specifications come from + +The **Specification** list in the build dialog is populated in this order (duplicates by full path removed): + +1. `*.handoff.json` in the nearest `.handoff` folder found by walking from the working root up to its volume root. +2. `*.handoff.json` directly in the configured **specification library** folder (for example a shared team folder). +3. Recently used specifications (up to 10) that still exist. +4. **Browse…** for any other file. + +A specification is plain UTF-8 JSON. Edit it in any editor (for example focus it and press F4 in Open Salamander), then use **Validate**. **New Specification from Template…** creates a starting point: *Design studio handoff*, *Architecture drawing issue*, *Consultancy report delivery*, and *Client delivery (example)* — the last one implements the quick-start request. + +### A.5.4 The review screen + +- **Rules** list: each rule with role, required flag, selected/expected counts, and status (OK, Errors, Warnings, Missing). +- **Items** list for the selected rule (or all rules): include check box, source path, target path, size, details (pages, page size, pixel size, dpi), approval, licence, status. +- **Findings** list: severity, code, item, message. Double-click or **Show in panel** focuses the source file in the working pane. +- Filters: *All items*, *Problems only*, *Omissions*, *Not included*, *Superseded*. +- Actions: **Approve**, **Approve all in rule**, **Show in panel**, **Override…** (only when the specification allows reviewer overrides), **Re-scan**, **Save report…**. +- **Build** is disabled while errors exist (unless overridden as permitted) or while warnings are unacknowledged. +- Excluding a candidate is recorded; excluding all candidates of a required rule produces an omission error. + +### A.5.5 What a build produces + +- A new package folder named by the specification's `package.folderName` template in the staging location. +- Every included file copied (never moved) with its target name and folder; last-write time preserved by default; only the default data stream is copied. +- `manifest.json`, `manifest.csv`, `CONTENTS.txt` in the package root (names configurable by the specification; `manifest.json` is mandatory). +- The build record beside the package (unless the specification sets `package.buildRecord` to `"none"`). +- A summary: **Verified — ready for review**, or **Verified with warnings** (listed), or **Not built** (reason; the working material and staging location are unchanged). + +### A.5.6 Verifying a package + +Verification recomputes SHA-256 for every listed file and reports missing, modified, and unlisted files. When the specification used for the build can be found (build record beside the package, library folder, recent list, or **Browse…**), its content, naming, format, PDF, image, and count rules are re-checked against the package. A specification whose hash differs from the one recorded in the manifest is used with a warning. **Save report…** writes a plain-text report to a location the user chooses (never into the package). + +### A.5.7 Configuration + +| Setting | Default | Purpose | +| --- | --- | --- | +| Specification library folder | empty | Folder searched for `*.handoff.json` (team library). | +| Show files not included by any rule | on | Shows the *Not included* group in review. | +| Focus the new package in the staging pane | on | After publication, focuses the package folder. | +| Keep failed staging folders for diagnosis | off | When on, a failed or cancelled build keeps its hidden partial folder. | +| Clear recent specifications | — | Empties the recent list. | + +### A.5.8 Troubleshooting + +| Symptom | Cause and remedy | +| --- | --- | +| *Build* is disabled | Errors remain or warnings are not acknowledged. Filter *Problems only*. | +| "Package folder already exists" (`HO-SES-005`) | Increase the revision variable or remove the old package. Packages are never overwritten. | +| "Source changed since review" (`HO-BUILD-002`) | A working file was edited after the scan. Choose **Re-scan**. | +| "Publication blocked" (`HO-BUILD-006`) | Another program (indexer, antivirus, Explorer preview) held a file in the partial folder. Retry; the partial folder is kept and offered for cleanup next time. | +| "Page inspection unavailable" (`HO-PDF-030/031`) | Windows could not open the PDF for page inspection (damaged file, unusual encryption, or Windows PDF component unavailable). | +| Hidden `.…handoff-….partial` folder in the staging location | A build was interrupted (for example the process was terminated). The next build in that location offers to remove it. | + +--- + +# Part B — Implementation task list + +Check each box when its acceptance criteria are met. Task IDs are stable; cite them in commits and pull requests. + +## Phase 0 — Foundations and spikes + +- [x] **T0.1 Scaffold the plug-in** (→ [C.2](#c2-change-inventory-and-source-layout), [C.3](#c3-plug-in-host-integration)) + - [x] Create `src/plugins/handoff/` from the minimal menu-extension pattern in `src/plugins/demomenu/` and the entry/connect/config pattern in `src/plugins/checksum/checksum.cpp`. + - [x] Add `vcxproj/handoff.vcxproj` (+ `.filters`, `handoff.props`) for `Debug|Win32`, `Debug|x64`, `Release|Win32`, `Release|x64`, importing `x86.props`/`x64.props`, `plugin_base.props`, `plugin_debug.props`/`plugin_release.props` exactly as `src/plugins/checksum/vcxproj/checksum.vcxproj:53-100` does. + - [x] Set `stdcpplatest` (precedent `src/plugins/ftp/vcxproj/ftp.vcxproj:105`) and add `..\..\..` to `AdditionalIncludeDirectories` so `common/relative_file_operations.h` resolves (precedent `src/plugins/ftp/vcxproj/ftp.props:8`, `src/plugins/ftp/precomp.h:23`). + - [x] Add `vcxproj/lang_handoff.vcxproj` + `lang_handoff.props` (`ShortProjectName=handoff`) and `vcxproj/lang_pl_handoff.vcxproj` mirroring `src/plugins/checksum/vcxproj/lang_*.vcxproj`; outputs are `plugins\handoff\lang\english.slg` and `polish.slg` (`src/plugins/shared/vcxproj/lang_base.props:7-18`, `lang_pl_base.props:7-19`). + - [x] `handoff.def` exports `SalamanderPluginEntry` and `SalamanderPluginGetReqVer` (copy `src/plugins/checksum/checksum.def`). + - [x] `versinfo.rh2` with plug-in version `1.0.0`, description "Delivery Handoff plugin for Open Salamander", copyright "Copyright © 2026 Taskscape Ltd". + - [x] Add the three projects to `src/vcxproj/salamand.sln` with all four solution configurations mapped, next to the other plug-in projects. + - [x] Add base addresses (Debug builds link with `/BASE:@file,key`, `src/plugins/shared/vcxproj/plugin_debug.props:19`): `handoff 0x0000010021b00000`, `lang_handoff 0x0000010031b00000`, `lang_pl_handoff 0x0000010044600000` in `baseaddr_x64.txt`; `0x21b00000`, `0x31b00000`, `0x44600000` in `baseaddr_x86.txt`. These slots are unused at `165a2c9`; confirm with `dumpbin /headers` that `handoff.spl` (Debug) does not overlap the next module and move it if it does. + - *Implementation note:* Done with a correction: the Debug x64 image (5.5 MB, C++/WinRT and debug STL) overlapped `folders` at `0x22000000`, so `handoff` moved to `0x000001002a800000` / `0x2a800000` (free up to `lang`). See C.13.1. + - Acceptance: the solution builds `Debug|Win32`, `Debug|x64`, `Release|x64` with no warnings (PR builds treat warnings as errors); the plug-in auto-installs from a fresh profile, appears in Plugins Manager, shows **About**, and unloads cleanly. + +- [ ] **T0.2 Engine/host boundary** (→ [C.2.2](#c22-source-layout)) + - [x] Create `src/plugins/handoff/engine/`. Engine files MUST NOT include `precomp.h`, SDK headers (`spl_*.h`), `dbg.h`, or WinLib; only Windows SDK, C++ standard library, and `src/common/relative_file_operations.h`. + - [x] Mark engine `.cpp` files `PrecompiledHeader=NotUsing` in the plug-in project. + - [x] Engine code MUST NOT depend on `char` signedness: plug-ins compile with `/J` (`src/plugins/shared/vcxproj/plugin_base.props:16`), the parser probe does not. + - [ ] Engine files compiled by the parser probe (C.12.2) MUST also build with the Visual Studio 2022 toolset at `/std:c++latest`, because the hosted nightly soak runs on `windows-2025` (`.github/workflows/nightly-parser-fuzz.yml`), where VS 2026 `v145` is unavailable. + - *Implementation note:* Not verified: no VS 2022 toolset on the implementation machine. The probe command line defines `_REGEX_MAX_*` for older STLs (C.13.1). + - [x] Add the host abstraction interfaces `IHandoffFileSystem`, `IHandoffClock`, `IHandoffProgress`, `IPdfPageInspector`, `IImageInspector` (→ [C.2.3](#c23-engine-interfaces)). + - *Implementation note:* Implemented as `IHandoffFileSystem`, `IProgress`/`IBuildCallbacks`, `IPdfPageInspector`, and `IImageInspector`; time is passed in `BuildInput` instead of an `IHandoffClock` (C.13.1). + - Acceptance: an empty native console project can compile every engine `.cpp` with only `/I src/plugins/handoff/engine /I src`. + +- [ ] **T0.3 Spike: page inspection with `Windows.Data.Pdf`** (→ [C.5.4.3](#c543-pdf-pages)) + - [x] Implement `engine/pdf_pages_winrt.cpp` (C++/WinRT headers from the Windows SDK, no PCH, `#undef GetCurrentTime` before WinRT headers) on an MTA worker thread. Link `shcore.lib` and the import library C++/WinRT requires (`WindowsApp.lib` per C++/WinRT guidance, or `runtimeobject.lib` if `WindowsApp.lib` re-routes Win32 imports undesirably); record the choice in C.13. + - [x] Open through `SHCreateStreamOnFileEx` (`\\?\` path, `STGM_READ | STGM_SHARE_DENY_WRITE`) → `CreateRandomAccessStreamOverStream` → `PdfDocument::LoadFromStreamAsync`, `wait_for(30 s)`, read `PageCount`, `IsPasswordProtected`, and each page's `Size` (DIPs). + - [ ] Verify on Win32 and x64, in Debug and Release, on Windows 10 1809+ and Windows 11; run `tools/audit-pe-hardening.ps1` on the Release `handoff.spl`. + - *Implementation note:* Windows 11: x64 Debug exercised by engine and UI tests; Win32/x64 Debug/Release build cleanly; `audit-pe-hardening.ps1` passes for the Release x64 `handoff.spl`. Windows 10 1809+ not verified. + - [x] Record the outcome under [C.13](#c13-risks-and-open-items). If the API is unusable, keep D-04's structural scan and report page constraints as `HO-PDF-031`; update D-04. + - Acceptance: a generated 3-page PDF (A4, A4, A3 landscape) reports 3 pages and sizes within 0.5 mm. + +- [x] **T0.4 Spike: WIC header probe** (→ [C.5.4.4](#c544-raster-images)) + - [x] On an MTA worker: `IWICImagingFactory::CreateDecoderFromFilename(..., WICDecodeMetadataCacheOnDemand)`, frame 0 `GetSize`, `GetResolution`, `GetPixelFormat`, `IWICPixelFormatInfo2`, frame count, and GPS query for JPEG/TIFF. + - [x] Confirm behaviour when HEIC/WebP/AVIF codecs are absent (decoder creation fails with `WINCODEC_ERR_COMPONENTNOTFOUND`). + - Acceptance: PNG, JPEG, TIFF (CMYK), GIF fixtures generated at runtime report the expected size, dpi, colour model, alpha, and bit depth. + +- [ ] **T0.5 Ratchet dry run**: run `tools/verify-no-new-raw-thread-creation.ps1`, `verify-no-new-terminatethread.ps1`, `verify-no-new-gettickcount.ps1`, `verify-no-new-max-path-buffers.ps1`, `verify-no-new-unsafe-string-calls.ps1` with `-BaseCommit origin/main` and `tools/test-unsafe-api-baseline.ps1` on the scaffold. Acceptance: all pass without baseline regeneration or exemptions. + - *Implementation note:* The diff ratchets compare commits; the uncommitted work was checked with their exact patterns (no findings) and `test-unsafe-api-baseline.ps1` passes. Re-run the scripts after committing. + +## Phase 1 — Engine: JSON and specification model + +- [x] **T1.1 Bounded JSON reader** `engine/json_reader.{h,cpp}` (→ [C.4.1](#c41-file-rules-and-limits)) + - [x] Strict RFC 8259: one value, no trailing content, no comments, no trailing commas, no `NaN`/`Infinity`. + - [x] Input is UTF-8; a leading BOM is accepted and skipped; invalid UTF-8 → `HO-SPEC-001`. `\u` escapes decode surrogate pairs; an unpaired surrogate and `\u0000` are errors. + - [x] Duplicate member names in one object → `HO-SPEC-007`. + - [x] Every value records line and column (1-based, columns in Unicode scalar values) and a JSON Pointer path. + - [x] Numbers are kept as lexemes; the model converts them (integer members reject fractions and values outside ±2^53). + - [x] Enforce the limits table per profile (spec / manifest); exceeding a limit → `HO-SPEC-006` with the limit named. No recursion deeper than the depth limit (iterative or depth-guarded). + - Acceptance: corpus in `tests/handoff-specs/` passes (T8.2); malformed input never crashes, hangs, or allocates beyond 4× input size. + +- [x] **T1.2 Specification model and validator** `engine/spec_model.{h,cpp}` (→ [C.4](#c4-delivery-specification-format-v1)) + - [x] Typed model for every member in C.4.2–C.4.13 with defaults applied. + - [x] Validation emits `HO-SPEC-*` findings with JSON Pointer, line, and column; validation continues after errors to report as many as possible (cap 200 findings). + - [x] Unknown members → `HO-SPEC-002` warning with a "did you mean `x`?" suggestion when a known member is within Levenshtein distance 2. `$comment` members are ignored at any level. + - [x] Cross-checks: unique rule IDs; every template token resolvable; every format ID known or defined; output names valid and distinct; `count.min ≤ count.max`; severity overrides only for overridable codes; paths confined (C.10.1). + - [x] Canonical spec hash: SHA-256 of the exact file bytes (BOM included), computed by `LoadSpecification` (C.2.3) with the CNG helper from T3.3 (implement T3.3 first or in parallel) — used in manifests and verification. `spec_model` itself stays hash-free so the parser probe needs no crypto. + - Acceptance: each `HO-SPEC-*` code has at least one invalid corpus case producing it at the documented location. + +- [x] **T1.3 Glob matcher** `engine/glob.{h,cpp}` (→ [C.4.6.1](#c461-globs)) + - Acceptance: table-driven tests for `*`, `**`, `?`, `[...]`, `[!...]`, `{a,b}`, case-insensitivity, separators, static-prefix extraction. + +- [ ] **T1.4 Regular-expression guard** `engine/regex_guard.{h,cpp}` (→ [C.4.6.2](#c462-regular-expressions)) + - [x] Wrap `std::wregex` (`ECMAScript | icase | optimize`); compile at spec validation; reject patterns over 512 UTF-16 units; catch `std::regex_error` (including `error_complexity`, `error_stack`) at compile and match time and convert to findings. + - [ ] Bound backtracking with MSVC's `_REGEX_MAX_COMPLEXITY_COUNT` and `_REGEX_MAX_STACK_COUNT`, defined **project-wide** with identical values in `handoff.props`, the engine test project, and the parser-probe command line (per-file definitions would violate the one-definition rule for the `` templates). + - *Implementation note:* Superseded: the VS 2026 STL bounds backtracking itself (no `_REGEX_MAX_*` macros); only the parser probe defines them for older hosted toolsets. See C.13.1. + - Acceptance: catastrophic patterns from the hostile corpus finish within 1 s per subject and produce `HO-SPEC-010` (at validation) or `HO-SEL-008` (at match time). + +- [x] **T1.5 Units and dates** `engine/units.{h,cpp}`: size strings (`"20 GB"`, binary multiples), `{date:fmt}` subset (`yyyy`, `MM`, `dd`, `HH`, `mm`), ISO-8601 UTC formatting and `yyyy-MM-dd` parsing. + +- [x] **T1.6 Name templates and sanitization** `engine/name_template.{h,cpp}` (→ [C.4.10](#c410-naming-policy-and-target-placement)) + - [x] Token expansion, NFC normalization (`NormalizeString`), transliteration table, replacement, trimming, case transform, truncation preserving the extension, reserved-name and trailing dot/space rejection. + - Acceptance: golden tests for Polish, German, French, Czech, Nordic, and CJK names in `portable` and `unicode` modes. + +- [x] **T1.7 Embedded templates validate cleanly** (→ [C.4.14](#c414-embedded-templates), [C.4.15](#c415-complete-example)): the four template files validate with zero findings in an engine test. + +## Phase 2 — Engine: scanning, selection, inspection, evidence + +- [ ] **T2.1 Scanner** `engine/scanner.{h,cpp}` (→ [C.5.2](#c52-scan)) + - *Implementation note:* Implementation and limit/cancellation tests done; the 100 000-entry timing budget has not been measured. + - [x] Iterative breadth-first enumeration with `FindFirstFileExW(FindExInfoBasic, FIND_FIRST_EX_LARGE_FETCH)` on `\\?\` paths; never traverse directory reparse points; record file links and cloud placeholders; implicit exclusion of `.handoff/**`. + - [x] Cooperative cancellation between directories and every 256 entries; progress every ≥ 100 ms (`GetTickCount64`). + - [x] Limits and errors → `HO-SCAN-*`. + - Acceptance: 100 000-entry synthetic tree scans in < 10 s on local NVMe; junction cycle and out-of-root junction are not traversed. + +- [x] **T2.2 Assembler** `engine/assembler.{h,cpp}` (→ [C.5.3](#c53-match-and-select)) + - [x] Rule matching in specification order with first-match ownership (unless `select.shared`), global `content.forbid`, latest-revision selection, expectation keys (inline and register CSV), counts, unassigned and superseded lists, files consumed as evidence/registers excluded from *Not included*. + - Acceptance: tests for every `HO-SEL-*` and `HO-REQ-*` code. + +- [x] **T2.3 Format detection** `engine/format_detect.{h,cpp}` (→ [C.4.7](#c47-formats-and-signatures)): built-in signature table + custom formats; reads ≤ 4 KiB from the file start. + +- [x] **T2.4 PDF structure probe** `engine/pdf_structure.{h,cpp}` (→ [C.5.4.2](#c542-pdf-structure)): header/version, `%%EOF`, `startxref`, `/Encrypt`, `/Linearized`; reads ≤ 2 × 64 KiB. + +- [x] **T2.5 PDF page inspector** `engine/pdf_pages_winrt.cpp` behind `IPdfPageInspector` (from T0.3), plus `NullPdfPageInspector` used by the `cl.exe` parser probe. + +- [x] **T2.6 Image inspector** `engine/image_probe.{h,cpp}` (→ [C.5.4.4](#c544-raster-images), [C.5.4.5](#c545-psd-and-psb)): WIC raster formats and PSD/PSB header parsing; GPS presence. + +- [x] **T2.7 CSV reader** `engine/csv_reader.{h,cpp}` (→ [C.4.1](#c41-file-rules-and-limits)): RFC 4180, UTF-8 (BOM optional), header row required, quoted fields with embedded commas/quotes/newlines, limits. + +- [x] **T2.8 Evidence** `engine/evidence.{h,cpp}` (→ [C.4.8](#c48-approval-evidence), [C.4.9](#c49-licence-evidence)): approval patterns, sidecars, registers; licence sidecars, registers, expiry, scope, evidence files to copy. + +- [x] **T2.9 Findings and gating** `engine/findings.{h,cpp}` (→ [C.6](#c6-findings-catalogue)): finding model, severity resolution (default → spec override → reviewer override), non-overridable set, gate computation (`canBuild`, `needsAcknowledgement`). + +## Phase 3 — Engine: planning, staging, outputs, publication + +- [x] **T3.1 Planner** `engine/planner.{h,cpp}` (→ [C.5.5](#c55-plan-target-paths)): target paths from templates, `{seq}` allocation, collisions (case-insensitive, NFC), relative path length, output-name collisions, licence-evidence placement, totals and limits, free-space check. +- [x] **T3.2 File-system abstraction** `engine/file_system.{h,cpp}`: Win32 implementation of `IHandoffFileSystem` and a fault-injecting test double able to fail any call by ordinal and error code. +- [x] **T3.3 SHA-256** `engine/sha256_cng.{h,cpp}`: `BCryptOpenAlgorithmProvider(BCRYPT_SHA256_ALGORITHM)` once per worker, reusable hash objects (`BCRYPT_HASH_REUSABLE_FLAG`), link `bcrypt.lib`. +- [x] **T3.4 Stager** `engine/stager.{h,cpp}` (→ [C.5.7](#c57-build-and-publish)): partial folder and marker, per-file copy with identity check, hash-while-copy, flush, re-read verification, deferred probes, outputs, final tree verification, cancellation cleanup, stale-partial detection. +- [x] **T3.5 Output writers** `engine/output_writers.{h,cpp}` (→ [C.7](#c7-output-formats)): deterministic `manifest.json`, `manifest.csv` (formula guard), `CONTENTS.txt`. +- [x] **T3.6 Build record** `engine/build_record.{h,cpp}` (→ [C.7.4](#c74-build-record)). +- [x] **T3.7 Publisher** `engine/publisher.{h,cpp}` (→ [C.5.7](#c57-build-and-publish), steps 9–10): handle-bound directory rename via `RenameRelativePublicationFile` (`src/common/relative_file_operations.h:79`), identity check, bounded retry on sharing violations, never replace. +- Acceptance for Phase 3: engine tests in [C.12.1](#c121-native-engine-tests) group *Build* pass, including every fault-injection point leaving no final package and no unowned deletions. + +## Phase 4 — Engine: verification + +- [x] **T4.1 Manifest reader** `engine/manifest_reader.{h,cpp}`: strict `handoffManifest: 1` schema (C.7.1) using the manifest limit profile. +- [x] **T4.2 Specification resolution** for verification (→ [C.5.8](#c58-verify)): build record snapshot → library → recent list → user choice; hash comparison. +- [x] **T4.3 Verifier** `engine/verifier.{h,cpp}`: integrity (missing/modified/unlisted/outputs), rule re-check on package contents, status computation, text report. +- Acceptance: engine tests for each `HO-VER-*` code; a freshly built package verifies with zero findings beyond those recorded at build time. + +## Phase 5 — Plug-in host integration + +- [x] **T5.1 Entry point** `handoff.cpp` (→ [C.3.1](#c31-identity-entry-and-registration)): version check against `LAST_VERSION_OF_SALAMANDER`, `LoadLanguageModule(parent, "Handoff")`, `SetBasicPluginData(..., FUNCTION_CONFIGURATION | FUNCTION_LOADSAVECONFIGURATION, ..., "Handoff")`, help file `handoff.chm`, `InitializeWinLib("Handoff", DLLInstance)`, `SetupWinLibHelp`. +- [x] **T5.2 Connect**: menu items with the state masks in C.3.2, translator comment block for `export_mnu.py` (as in `src/plugins/checksum/checksum.cpp:243-252`), plug-in icon (`SetBitmapWithIcons`, `SetPluginIcon`, `SetPluginMenuAndToolbarIcon`). +- [x] **T5.3 `ExecuteMenuItem`** (→ [C.3.3](#c33-threading-and-lifetime)): main-thread session capture, single-session guard, start the UI thread; internal command `CMD_INTERNAL_FOCUS` for "Show in panel" and package focus (`FocusNameInPanel` is main-thread only, `spl_gen.h:1578-1584`). +- [x] **T5.4 Release and shutdown**: close modeless windows (`CWindowQueue`), request worker stop and join (`CThreadQueue::KillAll`), refuse non-forced unload while a build is running; on `IsCriticalShutdown()` cancel immediately without UI. +- [x] **T5.5 Configuration persistence** (→ [C.9](#c9-configuration-persistence)) and the Configuration dialog. +- [x] **T5.6 String boundary helpers** `ui/sdk_strings.{h,cpp}`: UTF-8 ↔ UTF-16 conversions with `MB_ERR_INVALID_CHARS`/`WC_ERR_INVALID_CHARS`, long-path normalization, heap buffers sized from SDK limits; no fixed `MAX_PATH` arrays (ratchet, `tools/max-path-buffer-exemptions.md`). +- [x] **T5.7 `HelpForMenuItem`** mapping command IDs to `IDH_*` topics. +- Acceptance: menu enablement matches C.3.2 for disk/archive/FS pane combinations; unloading from Plugins Manager during review closes the window; unloading during a build is refused unless forced; a forced unload leaves no package under the final name, and the partial folder is either removed or retained with its marker when cleanup cannot finish. + +## Phase 6 — User interface + +- [x] **T6.1 Build session dialog** `IDD_HO_SESSION` (→ [C.8.2](#c82-build-session-dialog)): paths, swap, scope, specification list/browse/validate/show, variables editor, live package-name preview and validation. +- [x] **T6.2 Review window** `IDD_HO_REVIEW` (→ [C.8.3](#c83-review-window)): resizable modeless window; rules, items, findings lists; filters; approve; include/exclude; show in panel; override; re-scan; save report; acknowledgement; build gating; progress area; persisted placement and column widths. +- [x] **T6.3 Build progress and cancellation** inside the review window: total and per-file progress, current file, **Cancel** with confirmation; retry/cancel prompt for I/O errors marshalled from the worker. +- [x] **T6.4 Result dialog** `IDD_HO_RESULT` (→ [C.8.4](#c84-result-dialog)). +- [x] **T6.5 Verify window** `IDD_HO_VERIFY` (→ [C.8.5](#c85-verify-window)). +- [x] **T6.6 New specification dialog** `IDD_HO_NEWSPEC` (→ [C.8.6](#c86-new-specification-dialog)); writes with `CREATE_NEW` and never overwrites. +- [x] **T6.7 Validate specification dialog** `IDD_HO_VALIDATE` (→ [C.8.7](#c87-validate-specification-dialog)). +- [x] **T6.8 Override dialog** `IDD_HO_OVERRIDE` (→ [C.8.8](#c88-override-dialog)). +- [x] **T6.9 Report export**: plain-text report (UTF-8 with BOM, CRLF) through `IFileSaveDialog` on the UI thread. +- [ ] **T6.10 Accessibility, DPI, theming** (→ [C.8.1](#c81-general-ui-rules)): keyboard-only operation, UIA names, severity as text + icon, system colours, per-monitor DPI at 100/150/200 %. + - *Implementation note:* Mnemonics, label-before-control order, system colours, and severity icons with text are in place; 150 %/200 % DPI and Narrator runs are not verified. +- Acceptance: every control listed in C.8 exists with its frozen resource ID; FlaUI can discover and operate it (T8.3). + +## Phase 7 — Content, localization, help, icon + +- [x] **T7.1 Templates** `templates/*.handoff.json` embedded as `RCDATA` in `handoff.rc` (language-independent resources) (→ [C.4.14](#c414-embedded-templates)). +- [x] **T7.2 English resources** `lang/lang.rc`, `lang/lang.rc2`, `lang/lang.rh` with `#pragma code_page(65001)` (pattern `src/plugins/checksum/lang/lang.rc:3`): all dialogs, menu texts, findings messages (`IDS_HO_`), contents-list labels. + - [x] UI and worker code load text with `SalamanderGeneral->LoadStrW(HLanguage, id)` (any thread, `src/plugins/shared/spl_gen.h:3104-3121`) and copy it immediately into owned `std::wstring`s, because the returned buffer is shared and cyclic. + - [x] Messages are formatted with bounded APIs (`StringCchPrintfW`/`_snwprintf_s`); `sprintf`-family calls are rejected by `tools/verify-no-new-unsafe-string-calls.ps1`. Placeholders are positional-safe for Polish word order (format pieces separately where order differs). +- [x] **T7.3 Polish resources** `lang/pl/lang_pl.rc`, `lang/pl/lang_pl.rc2` with complete parity. Acceptance: `tools/verify-language-resource-parity.ps1 -BuildDirectory .\src -IncludePattern '*\salamander\Debug_x64\*'` passes. +- [x] **T7.4 Help** `src/plugins/handoff/help/` (`handoff.hhp`, `handoff.hhc`, `handoff.hhk`, `compile.bat`, `hh/handoff/*.htm`): introduction, quick start, commands, specification reference (C.4 in user language), findings reference (C.6), verification, configuration. Add `handoff` to `PLUGIN_LIST` in `help/src/compileall.bat`; add the merge entry next to *Checksum* in `help/src/salamand.hhc:914-918`; map `IDH_*` in `handoff.rh2` and `[MAP]`/`[ALIAS]` of `handoff.hhp` (pattern `src/plugins/checksum/help/checksum.hhp:24-25`). + - *Implementation note:* Help pages and project are generated and wired into `compileall.bat`, `salamand.hhc`, and `salamand.hhp`; CHM compilation was not run (HTML Help Workshop not installed). +- [x] **T7.5 Icon**: `src/res/toolbars/PluginHandoff.svg`, `24/PluginHandoff.svg`, `32/PluginHandoff.svg` (Fluent System Icons `box_checkmark` regular or the nearest available glyph; first line ``; one approved palette colour; exact `width/height/viewBox` per size); add `{"handoff.spl", "PluginHandoff"}` to `GetPluginSVGName` in `src/svg.cpp:200-232`; add a 16 × 16 menu bitmap `handoff.bmp`. Acceptance: `tools/verify-fluent-icon-coverage.ps1` passes. + - *Implementation note:* Uses the vendored Fluent `box` glyph (also used by DriveDropbox) because `box_checkmark` is not in the vendored set; registered in `fluent-map.json` and `fluent-provenance.json`. +- [x] **T7.6 Other languages**: no `translations/*/handoff.slt` in v1; other UI languages fall back to English. Record in C.13. + +## Phase 8 — Tests + +- [x] **T8.1 Native engine tests** `tests/HandoffEngineTests/` (→ [C.12.1](#c121-native-engine-tests)): console executable compiling engine sources directly (pattern `tests/PictViewEngineTests/PictViewEngineTests.vcxproj:29-41`), all fixtures generated at runtime below a GUID directory under `%TEMP%`, removed afterwards; non-zero exit on any failure. +- [x] **T8.2 Parser probe and corpus** (→ [C.12.2](#c122-specification-parser-probe-and-corpus)): `tools/test-handoff-spec-parser.ps1`, `tools/handoff_spec_parser_probe.cpp`, `tests/handoff-specs/{valid,invalid,hostile}/` with expected-findings files; `-Architecture x64|x86` and `-Iterations N`. +- [x] **T8.3 UI tests** `tests/FileManager.UiTests/HandoffUiTests.cs` (category `UI`) (→ [C.12.3](#c123-ui-tests)). + - [x] Generalize the private `WaitForFtpPluginCommand(int pluginCommand, string commandName)` (`Infrastructure/FileManagerUiTestBase.cs:506-541`) into `WaitForPluginCommand(string dllLeaf, int pluginCommand, string commandName)` reading `UiTestSettings.PluginCommandMapPath` for the launched `Application.ProcessId`; keep `WaitForFtpPluginCommand` as a thin wrapper (the root runner contract asserts its name, `tests/FileManager.UiTests/NativeSafetyRegressionTests.cs:53-55`). + - [x] Add `RequireHandoffPluginRuntime()` that **fails** (not ignores) when `plugins\handoff\handoff.spl` or `plugins\handoff\lang\english.slg` is missing beside the executable under test — the runner always builds the plug-in, so absence is a defect, not a capability skip. + - [x] Extend the focused runner `run-ui-tests.ps1` (`Test-FtpUiRuntime`/`New-FtpUiTestRuntime`, lines 55-125) so it stages `handoff.spl` and `lang\english.slg` from `src\plugins\handoff\vcxproj\salamander\\plugins\handoff` into the disposable runtime when the filter can select Handoff tests. + - [x] Reference dialog/control IDs with comments naming their `lang.rh` symbols (pattern `Infrastructure/NativeCommands.cs:493`). +- [x] **T8.4 Source-contract tests** `tests/FileManager.UiTests/HandoffSourceContractTests.cs` (non-UI): publication uses `RenameRelativePublicationFile`; the engine contains no `MOVEFILE_REPLACE_EXISTING` or `CopyFile*`; `DeleteFileW`/`RemoveDirectoryW` appear only in `engine/file_system.cpp` behind `DeleteOwned`; every template in `templates/` is embedded in `handoff.rc`; every `HO-*` code in C.6 has a string in both language files. + +## Phase 9 — Runner/CI parity, packaging, documentation + +- [x] **T9.1 `scripts/runtests.ps1`** + - [x] Add `$handoffEngineProject` and `Invoke-HandoffEngineTests` mirroring `Invoke-PictViewEngineTests` (`scripts/runtests.ps1:444-475`), and `Invoke-AutomatedCheck -Name 'HandoffEngineTests (Debug x64)'` after the PictView check (`:893`). + - [x] Run `tools\test-handoff-spec-parser.ps1` for `x64` and `x86` inside the existing `foreach ($architecture in @('x64', 'x86'))` probe loop used for zlib/bzip2. + - [x] Keep skip policy unchanged: both checks are blocking and never skip when VS 2026 is present. +- [x] **T9.2 GitHub workflows** + - [x] `.github/workflows/pr-msbuild.yml`: step "Exercise Handoff specification parser corpus" next to the cmark step, on the x64 matrix leg (`if: matrix.platform == 'x64'`, as `pr-msbuild.yml:125-135`), same invocation as the runner. + - [x] `.github/workflows/build-installer.yml`: no new step — the release gate runs `runtests.ps1` (`:104`); confirm the new checks appear in its output. + - [x] `.github/workflows/nightly-parser-fuzz.yml`: step "Replay Handoff specification hostile corpus" with `-Iterations 250`. +- [x] **T9.3 Runner contract**: extend `Root_test_runner_collects_every_documented_automated_test_layer` (`tests/FileManager.UiTests/NativeSafetyRegressionTests.cs:16`) with `Does.Contain("test-handoff-spec-parser.ps1")` and `Does.Contain("HandoffEngineTests")`. +- [x] **T9.4 Packaging**: add `'handoff\handoff.spl'` and `'handoff\lang\english.slg'` to `$requiredPluginPayloads` in `tools/prepare_installer.ps1:155-160` so a regression that drops the plug-in fails staging. No `setup.iss` change (it copies `plugins\*` recursively, `Installer/setup.iss:91`). +- [x] **T9.5 Documentation** + - [x] `README.md`: "What's new in 6.0 → Plugins and network": one bullet for Delivery Handoff. + - [x] `architecture.md` §5.3 "Representative plug-in families → utilities": add "delivery handoff". + - [x] `testing.md`: catalog entries for `HandoffEngineTests`, `test-handoff-spec-parser.ps1`, `HandoffUiTests`, `HandoffSourceContractTests`; quick-start commands. + - [x] `tests/FileManager.UiTests/README.md`: Handoff cases and their fixtures. + - [x] No `doc/third_party.txt` change (no third-party code is added). + +## Phase 10 — Final validation + +- [x] **T10.1 Build matrix** in the VS 2026 developer environment: `Debug|Win32`, `Debug|x64`, `Release|x64` — zero warnings; report the exact commands and results. + - *Implementation note:* VS 2026 (MSVC 14.51.36231, SDK 10.0.26100.0): the complete solution builds Debug|x64 and Debug|Win32 (the latter with `/warnaserror`, as the PR workflow) without warnings; the plug-in and both language projects build Debug|Win32, Release|x64, and Release|Win32 with `/warnaserror`. +- [ ] **T10.2 Regression run**: `.\scripts\runtests.ps1 -PrerequisiteOnly`, then `.\scripts\runtests.ps1` (release-pipeline default). Report invocation, passed/failed/skipped lists, and TRX path. Unexpected skips or failures block completion. +- [ ] **T10.3 Manual acceptance** ([C.12.4](#c124-manual-acceptance-script)) on Windows 10 and Windows 11, English and Polish UI. +- [ ] **T10.4 Close-out**: tick this document, record spike outcomes and deviations in C.13, attach a sample package (built from the example template fixture) to the pull request. + +## B.11 Definition of done + +- [x] The quick-start scenario in A.5.1 works end to end with the *Client delivery (example)* template. +- [x] Omissions (empty required rule, missing expected key, count below minimum) block the build and are listed. +- [x] Naming, format, PDF, image, allowed-content, approval, and licence rules produce the documented findings. +- [x] A built package contains exactly the planned files plus outputs; every hash re-verifies; the build record is beside the package. +- [x] Cancel, I/O failure, source change, name collision, and process termination never leave a package under the final name and never modify working material. +- [x] Verify detects missing, modified, and unlisted files and re-checks rules. +- [x] English and Polish resources are complete; help topics exist for every command. +- [ ] All new automated checks run in `runtests.ps1` and GitHub Actions with identical inputs; the full regression run passes. + +--- + +# Part C — Normative reference + +## C.1 Feasibility and architecture + +### C.1.1 Verdict + +The plug-in architecture is **practical and technically feasible** for the whole v1 scope. Evidence from the SDK and existing plug-ins: + +| Need | SDK facility | Reference | +| --- | --- | --- | +| Command in Plugins menu, enable by pane kinds | `CSalamanderConnectAbstract::AddMenuItem` with `MENU_EVENT_DISK`, `MENU_EVENT_TARGET_DISK` | `src/plugins/shared/spl_base.h:211-233`, `:298-320` | +| Execute command, get operations interface | `CPluginInterfaceForMenuExtAbstract::ExecuteMenuItem` | `src/plugins/shared/spl_menu.h` | +| Read pane paths and kinds | `GetPanelPath` (`PATH_TYPE_WINDOWS`), main thread only | `src/plugins/shared/spl_gen.h:246-254`, `:1290-1297` | +| Read selection and focus | `GetPanelSelection`, `GetPanelSelectedItem`, `GetPanelFocusedItem`, `GetSourcePanel` | `spl_gen.h:1330-1356`, `:1411` | +| Focus a file in a pane | `FocusNameInPanel` (main thread) via `PostMenuExtCommand(id, TRUE)` from other threads | `spl_gen.h:1578-1584`, `:1868-1882`; precedent `src/plugins/checksum/dialogs.cpp:2079-2081` | +| Refresh panes after staging | `PostChangeOnPathNotification` (any thread) | `spl_gen.h:1940-1949` | +| Record working path in history | `SetUserWorkedOnPanelPath` | `spl_gen.h:2321` | +| Configuration persistence | `LoadConfiguration`/`SaveConfiguration` with `CSalamanderRegistryAbstract` | `spl_base.h:496-509`; precedent `src/plugins/checksum/checksum.cpp:170-213` | +| Localized UI and help | `LoadLanguageModule`, `LoadStr`, `OpenHtmlHelp`, `.slg` modules | `src/plugins/checksum/checksum.cpp:100-117` | +| Plug-in threads with cooperative stop | `CThreadQueue::StartThread(CThreadQueueStopBody, …)`, `CThread`, `CPluginThreadOwner` | `src/plugins/shared/auxtools.h:16`, `:38-110`; `src/plugins/shared/plugin_thread_owner.h` | +| Durable, non-replacing publication | Header-only `RenameRelativePublicationFile` (already reused by the FTP plug-in through `common/`) | `src/common/relative_file_operations.h:79-98`; `src/plugins/ftp/precomp.h:23` | +| UI-test access to plug-in commands | Runtime menu IDs published to `ui-test-plugin-commands.log` | `src/plugins_loading.cpp:3706-3709`, `src/path_checking.cpp:2372-2409`, `tests/FileManager.UiTests/Infrastructure/UiTestSettings.cs:43-44` | +| Shipping | Installer stages all plug-in build outputs recursively | `tools/prepare_installer.ps1:118-151`, `Installer/setup.iss:91` | + +Constraints the design accommodates: + +- SDK strings are UTF-8 `char*` with bounded buffers (panel paths up to `2 * MAX_PATH` bytes, `CFileData::NameLen` is 9 bits — `spl_com.h:230`). The plug-in converts at the boundary and works internally in UTF-16 with `\\?\` long paths, re-enumerating the file system itself rather than relying on panel listings. +- The SDK offers no copy engine to plug-ins (`CSalamanderForOperationsAbstract` exposes only progress and `MoveFiles`, `spl_com.h:848-897`). The plug-in implements its own copy with hash verification and atomic publication (C.5.7). Startup journal recovery of the core does not cover plug-in copies; the partial-folder design makes recovery unnecessary (an interrupted build is simply discarded). +- Plug-ins run in-process; the engine is defensive (bounded parsers, no recursion on input, cooperative cancellation, no unbounded waits on the UI thread). + +### C.1.2 Alternatives considered + +| Alternative | Assessment | Outcome | +| --- | --- | --- | +| Core feature in `salamand.exe` (new `CM_*` command, dialogs in `dialogs_*.cpp`, reuse of `COperations` copy scripts) | Reuses the durable copy engine and journaling, but adds a large, specialized feature to `CMainWindow`/`HandleWmCommand`, `CConfiguration`, migration, and core language resources; cannot be disabled or unloaded. | Rejected for v1. | +| SDK extension: new `CSalamanderGeneralAbstract` method to submit a copy plan to the core engine | Gives plug-ins the core copy engine but changes the ABI (`spl_vers.h` procedure, `doc\how_to_change.txt`), version negotiation, and every SDK consumer. Benefit is small because packages are new folders built privately. | Deferred; revisit only if more plug-ins need core copying. | +| Automation plug-in script (`src/plugins/automation`) | Fast to prototype, but no WIC/WinRT access from script hosts without COM shims, weak UI, no deterministic tests, scripting engines may be disabled. | Rejected. | +| Plug-in file system showing a virtual "package view" | Elegant browsing of planned contents, but large surface (`spl_fs.h`) and confusing semantics before publication. | Deferred (C.13). | + +### C.1.3 Component view + +```mermaid +flowchart LR + subgraph Host["salamand.exe (main thread)"] + Menu["Plugins menu"] --> Exec["ExecuteMenuItem"] + Exec --> Capture["Session capture\npanel paths, selection"] + Focus["CMD_INTERNAL_FOCUS\nFocusNameInPanel"] + end + subgraph UI["Plug-in UI thread (STA)"] + Session["Build session dialog"] --> Review["Review window"] + Review --> Result["Result dialog"] + Verify["Verify window"] + end + subgraph Workers["Plug-in workers (MTA, stop-aware)"] + Scan["Scanner"] --> Assemble["Assembler"] --> Probe["Format / PDF / image probes"] --> Evidence["Evidence"] --> Plan["Planner"] + Stage["Stager"] --> Outputs["Output writers"] --> Publish["Publisher"] + Verifier["Verifier"] + end + Capture --> Session + Review -->|start| Scan + Plan -->|findings, plan| Review + Review -->|build| Stage + Publish -->|PostChangeOnPathNotification| Host + Review -->|PostMenuExtCommand| Focus + Verify -->|start| Verifier +``` + +## C.2 Change inventory and source layout + +### C.2.1 Files to add or modify + +| Path | Change | Purpose | +| --- | --- | --- | +| `src/plugins/handoff/**` | Add | Plug-in sources, engine, resources, templates, help, projects (C.2.2) | +| `src/vcxproj/salamand.sln` | Modify | Add `handoff`, `lang_handoff`, `lang_pl_handoff` | +| `src/plugins/shared/baseaddr_x64.txt`, `baseaddr_x86.txt` | Modify | Debug base addresses (T0.1) | +| `src/svg.cpp` | Modify | Plug-in icon mapping in `GetPluginSVGName` | +| `src/res/toolbars/PluginHandoff.svg`, `24/…`, `32/…` | Add | Plug-in icon | +| `help/src/compileall.bat`, `help/src/salamand.hhc` | Modify | Compile and merge plug-in help | +| `tests/HandoffEngineTests/**` | Add | Native engine tests | +| `tests/handoff-specs/**` | Add | Parser corpus and expected findings | +| `tools/test-handoff-spec-parser.ps1`, `tools/handoff_spec_parser_probe.cpp` | Add | `cl.exe` parser probe | +| `tests/FileManager.UiTests/HandoffUiTests.cs`, `HandoffSourceContractTests.cs` | Add | UI and source-contract tests | +| `tests/FileManager.UiTests/Infrastructure/FileManagerUiTestBase.cs` | Modify | Generic `WaitForPluginCommand`, `RequireHandoffPluginRuntime` | +| `run-ui-tests.ps1` | Modify | Stage the Handoff plug-in into the focused runner's disposable runtime | +| `tests/FileManager.UiTests/NativeSafetyRegressionTests.cs` | Modify | Runner contract assertions | +| `scripts/runtests.ps1` | Modify | Engine tests and parser probe | +| `.github/workflows/pr-msbuild.yml`, `nightly-parser-fuzz.yml` | Modify | Parser probe steps | +| `tools/prepare_installer.ps1` | Modify | Required payload guard | +| `README.md`, `architecture.md`, `testing.md`, `tests/FileManager.UiTests/README.md` | Modify | Documentation | + +### C.2.2 Source layout + +```text +src/plugins/handoff/ + handoff.cpp, handoff.h entry point, CPluginInterface, CPluginInterfaceForMenuExt, config + handoff.def exports + handoff.rc, handoff.rc2 language-independent resources: version, menu bitmap, RCDATA templates + handoff.rh, handoff.rh2 command IDs, IDH_* help IDs, RCDATA IDs + handoff.bmp 16×16 menu icon strip + versinfo.rh2 + precomp.h, precomp.cpp PCH for host-facing files only + ui/ + session.cpp/.h session capture (main thread) and session object + ui_thread.cpp/.h plug-in UI thread, message loop, single-session guard + dlg_session.cpp/.h IDD_HO_SESSION + dlg_review.cpp/.h IDD_HO_REVIEW (+ progress area) + dlg_result.cpp/.h IDD_HO_RESULT + dlg_verify.cpp/.h IDD_HO_VERIFY + dlg_newspec.cpp/.h IDD_HO_NEWSPEC + dlg_validate.cpp/.h IDD_HO_VALIDATE + dlg_override.cpp/.h IDD_HO_OVERRIDE + dlg_config.cpp/.h IDD_HO_CONFIG + worker_bridge.cpp/.h worker start/stop, posted progress/finding messages, retry prompts + listview_util.cpp/.h list-view columns, persistence, sorting + sdk_strings.cpp/.h UTF-8/UTF-16 boundary, long paths + report_text.cpp/.h Save report… + engine/ host-independent (D-11) + json_reader, spec_model, glob, regex_guard, units, name_template, + scanner, assembler, format_detect, pdf_structure, pdf_pages_winrt (isolated TU), + image_probe, csv_reader, evidence, findings, planner, file_system, sha256_cng, + stager, output_writers, build_record, publisher, manifest_reader, verifier, + handoff_engine.h umbrella header with interfaces (C.2.3) + templates/ + design-studio.handoff.json + architecture-issue.handoff.json + consultancy-report.handoff.json + client-delivery-example.handoff.json + lang/lang.rc, lang.rc2, lang.rh + lang/pl/lang_pl.rc, lang_pl.rc2 + help/handoff.hhp, handoff.hhc, handoff.hhk, compile.bat, hh/handoff/*.htm + vcxproj/handoff.vcxproj(.filters), handoff.props, lang_handoff.vcxproj, lang_handoff.props, lang_pl_handoff.vcxproj +``` + +### C.2.3 Engine interfaces + +```cpp +// Host-independent seams; the plug-in supplies Win32 implementations, tests supply doubles. +struct IHandoffProgress { + virtual void Phase(HandoffPhase phase, uint64_t total) = 0; // scan, probe, copy, verify, publish + virtual void Advance(uint64_t done, const std::wstring& currentItem) = 0; + virtual bool StopRequested() = 0; // backed by the worker stop event + virtual HandoffRetryChoice AskRetry(const HandoffIoError& error) = 0; // marshalled to the UI thread +}; +struct IHandoffClock { virtual FILETIME NowUtc() = 0; virtual SYSTEMTIME TodayLocal() = 0; }; +struct IPdfPageInspector { virtual PdfPagesResult Inspect(const std::wstring& path, DWORD timeoutMs) = 0; }; +struct IImageInspector { virtual ImageFacts Inspect(const std::wstring& path, FormatId format) = 0; }; +struct IHandoffFileSystem; // CreateFile/Read/Write/Flush/Close/GetInformationByHandle/CreateDirectory/ + // SetAttributes/Enumerate/RenameRelative/DeleteOwned/GetFreeSpace +``` + +Engine entry points (each runs on a worker thread and is cancellable): + +```cpp +SpecLoadResult LoadSpecification(const std::wstring& path); // parse + validate +ScanResult ScanWorkingMaterial(const SessionInput&, const Spec&, IHandoffProgress&); +ReviewModel AssembleAndInspect(const ScanResult&, const Spec&, const Variables&, Inspectors&, IHandoffProgress&); +PlanResult PlanPackage(const ReviewModel&, const ReviewerDecisions&, const Spec&); +BuildResult BuildPackage(const PlanResult&, IHandoffFileSystem&, IHandoffProgress&, IHandoffClock&); +VerifyResult VerifyPackage(const std::wstring& packageRoot, const SpecResolver&, IHandoffProgress&); +``` + +## C.3 Plug-in host integration + +### C.3.1 Identity, entry, and registration + +| Item | Value | +| --- | --- | +| DLL | `plugins\handoff\handoff.spl` | +| Display name (`IDS_PLUGINNAME`) | "Delivery Handoff" (localized) | +| Internal name (language module, registry key, `/* do not translate! */`) | `Handoff` | +| Functions | `FUNCTION_CONFIGURATION \| FUNCTION_LOADSAVECONFIGURATION` (static menu; no dynamic menu) | +| Help file | `handoff.chm` | +| Home page | `www.taskscape.com` (as other first-party plug-ins) | +| Required version | `SalamanderPluginGetReqVer` returns `LAST_VERSION_OF_SALAMANDER`; entry rejects older hosts with `REQUIRE_LAST_VERSION_OF_SALAMANDER` | + +### C.3.2 Menu commands + +| ID (frozen) | Constant | Menu text (EN) | `callGetState` | `state_or` | `state_and` | +| --- | --- | --- | --- | --- | --- | +| 1 | `CMD_BUILD` | `&Build Delivery Package...` | FALSE | `MENU_EVENT_DISK` | `MENU_EVENT_DISK \| MENU_EVENT_TARGET_DISK` | +| 2 | `CMD_VERIFY` | `&Verify Delivery Package...` | FALSE | `MENU_EVENT_DISK` | `MENU_EVENT_DISK` | +| — | separator | | | | | +| 3 | `CMD_VALIDATE_SPEC` | `Validate &Specification...` | FALSE | `MENU_EVENT_TRUE` | `0` | +| 4 | `CMD_NEW_SPEC` | `&New Specification from Template...` | FALSE | `MENU_EVENT_DISK` | `MENU_EVENT_DISK` | +| 100 | `CMD_INTERNAL_FOCUS` | not in menu; run via `PostMenuExtCommand(CMD_INTERNAL_FOCUS, TRUE)` | — | — | — | + +All items use `MENU_SKILLLEVEL_ALL` and hot key `0`. `ExecuteMenuItem` returns `FALSE` (never clears the panel selection) and calls `SetUserWorkedOnPanelPath(PANEL_SOURCE)` for `CMD_BUILD`, `CMD_VERIFY`, `CMD_NEW_SPEC`. + +`CMD_INTERNAL_FOCUS` reads a request queued under a critical section: `{panel: PANEL_LEFT|PANEL_RIGHT, directory (UTF-8), name (UTF-8)}`. It calls `SkipOneActivateRefresh()` then `FocusNameInPanel`. Requests whose UTF-8 path does not fit the SDK limits are dropped with a status-line message in the plug-in window, never truncated (precedent `src/plugins/checksum/dialogs.cpp:2079-2087`). + +### C.3.3 Threading and lifetime + +1. **Main thread** (`ExecuteMenuItem`): read both pane paths with `GetPanelPath` (reject non-`PATH_TYPE_WINDOWS` → `HO-SES-001/002`); determine which physical pane is source with `GetSourcePanel()`; read the selection (names, is-dir) or focused item; read `SALCFG_ALWAYSONTOP` with `GetConfigParameter` (main thread only). Build an immutable `SessionInput` (UTF-16). No disk I/O on the main thread. +2. **Single session**: if a Handoff window exists, bring it to the foreground and return. One build session and one verify window MAY coexist; two build sessions MUST NOT. +3. **UI thread**: started through `CThread::Create(ThreadQueue)` (pattern `src/plugins/checksum/dialogs.cpp:2221-2310`); calls `CoInitializeEx(COINIT_APARTMENTTHREADED)`; creates modeless top-level windows with `CDialog::Create()` and **no owner** so the main window stays usable; honours always-on-top; registers windows in a `CWindowQueue`; runs its own `GetMessage`/`IsDialogMessage` loop; ends when its last window closes. +4. **Workers**: started from the UI thread through `CThreadQueue::StartThread(CThreadQueueStopBody, …)`; each calls `CoInitializeEx(COINIT_MULTITHREADED)` (WIC, WinRT). Workers never touch HWNDs except `PostMessage` to the owning window; results are transferred as heap objects owned by the message (receiver deletes). Retry prompts use a cross-thread `SendMessage` to the UI thread while no lock is held (`architecture.md` §7.1). +5. **Release**: `Release(parent, force)` closes windows (`CWindowQueue::CloseAllWindows(force)`); if a build is running and `force == FALSE`, return `FALSE` after telling the user; otherwise signal stop, wait (`CThreadQueue::KillAll(force, 5000, 2000)`), and the stager deletes owned files or leaves a marked partial folder. Under `IsCriticalShutdown()` no UI is shown. +6. The plug-in creates no threads except through `CThreadQueue`/`CPluginThreadOwner` (ratchet `verify-no-new-raw-thread-creation.ps1`) and never calls `TerminateThread`. + +## C.4 Delivery specification format v1 + +### C.4.1 File rules and limits + +- File name `*.handoff.json`; UTF-8 with optional BOM; strict RFC 8259 JSON; `$comment` members are ignored anywhere. +- Member names are case-sensitive. Unknown members produce `HO-SPEC-002` warnings (forward-compatible authoring) — an unsupported `handoffSpec` version is an error. +- All path matching is case-insensitive; separators in specifications are `/` (a `\` is accepted and normalized). +- Paths in a specification are relative. Absolute paths, drive letters, UNC prefixes, `..` segments, `:` (alternate data streams), and empty segments are rejected (`HO-SPEC-013`). + +| Limit | Specification | Manifest (verify) | Register CSV | +| --- | --- | --- | --- | +| File size | 1 MiB | 32 MiB | 4 MiB | +| Nesting depth | 32 | 16 | — | +| String length (UTF-16 units) | 4 096 | 32 768 | 4 096 per cell | +| Members per object | 256 | 64 | — | +| Array elements | 1 024 (rules ≤ 256, patterns per list ≤ 64) | 200 000 | rows ≤ 50 000, columns ≤ 64 | +| Number lexeme | ≤ 32 characters | ≤ 32 characters | — | +| Regular expression pattern | ≤ 512 UTF-16 units | — | — | +| Findings reported per validation | 200 | 10 000 | — | + +### C.4.2 Top-level object + +| Member | Type | Required | Description | +| --- | --- | --- | --- | +| `handoffSpec` | integer | yes | Format version; MUST be `1`. | +| `id` | string | yes | Stable identifier, `^[a-z0-9][a-z0-9._-]{0,63}$`. Recorded in manifests; used to find the specification at verification. | +| `name` | string | yes | Display name, ≤ 128 characters. | +| `revision` | string | no | Author's revision label (for example `2026.1`). | +| `description` | string | no | Shown in the session dialog. | +| `variables` | object | no | Package variables (C.4.3). | +| `package` | object | yes | Package naming, outputs, limits (C.4.4). | +| `naming` | object | no | Naming policy for target names (C.4.10). | +| `content` | object | no | Allowed-content policy (C.4.11). | +| `formats` | object | no | Custom format definitions (C.4.7). | +| `rules` | array | yes | 1–256 rule objects, evaluated in order (C.4.5). | +| `severity` | object | no | Finding code → `"error"`, `"warning"`, `"info"`, or `"off"` (C.4.12). | +| `policy` | object | no | Build gating policy (C.4.12). | + +### C.4.3 Variables + +```json +"variables": { + "client": { "label": "Client code", "required": true, "pattern": "^[A-Z0-9]{2,12}$" }, + "project": { "label": "Project code", "required": true }, + "revision": { "label": "Delivery revision", "default": "R01", "pattern": "^R\\d{2}$" }, + "purpose": { "label": "Issue purpose", "default": "For review", "choices": ["For review", "For construction", "Final"] } +} +``` + +| Member | Type | Default | Notes | +| --- | --- | --- | --- | +| `label` | string | variable name | Shown in the session dialog. | +| `required` | bool | false | Empty value → `HO-SES-004`. | +| `default` | string | `""` | Initial value. | +| `pattern` | regex | none | Full-match validation of the value. | +| `choices` | array of strings | none | Value must be one of them; the editor shows a drop-down. | + +Variable names match `^[A-Za-z][A-Za-z0-9]{0,31}$`; values are ≤ 128 characters. Built-in variables: `date` (local build date, `yyyy-MM-dd`, editable in the session dialog), `specId`, `specName`, `specRevision`. Reserved names that cannot be declared: the built-ins and the per-file tokens `stem`, `ext`, `name`, `key`, `rev`, `seq`, `rule`, `relDir`, `folder`, `dir`. + +### C.4.4 Package + +```json +"package": { + "folderName": "{client}-{project}_Delivery_{date}_{revision}", + "outputs": { "manifestJson": "manifest.json", "manifestCsv": "manifest.csv", "contents": "CONTENTS.txt" }, + "contents": { "title": "{client} {project} — delivery contents", "groupBy": "folder", "notes": ["Issued for: {purpose}"] }, + "limits": { "maxFiles": 5000, "maxTotalBytes": "20 GB", "maxRelativePathLength": 180 }, + "preserveModifiedTime": true, + "buildRecord": "beside", + "manifest": { "includeSourcePaths": false } +} +``` + +| Member | Type | Default | Notes | +| --- | --- | --- | --- | +| `folderName` | template | required | Package-level tokens only (variables, built-ins, `{date:fmt}`). Sanitized with the naming policy. | +| `outputs.manifestJson` | file name | `"manifest.json"` | Required; cannot be `null`. | +| `outputs.manifestCsv` | file name or `null` | `"manifest.csv"` | `null` disables. | +| `outputs.contents` | file name or `null` | `"CONTENTS.txt"` | `null` disables. | +| `contents.title` | template | `"{specName}"` | First line of `CONTENTS.txt`. | +| `contents.groupBy` | `"folder"` or `"rule"` | `"folder"` | Grouping in `CONTENTS.txt`. | +| `contents.notes` | array of templates | `[]` | Free lines after the header. | +| `limits.maxFiles` | integer | 10 000 | `HO-PKG-001`. | +| `limits.maxTotalBytes` | size | none | `HO-PKG-002`. | +| `limits.maxRelativePathLength` | integer | 200 | UTF-16 units of `folder/name` below the package root; `HO-NAME-013`. | +| `preserveModifiedTime` | bool | true | Copy last-write time to staged files. | +| `buildRecord` | `"beside"` or `"none"` | `"beside"` | C.7.4. | +| `manifest.includeSourcePaths` | bool | false | Adds working-root-relative `source` to manifest entries. | + +Output names are plain file names (no folders), must satisfy the naming policy, must differ from each other, and must not collide with any planned file (`HO-SPEC-015` at validation, `HO-NAME-012` at planning). + +### C.4.5 Rules + +```json +{ + "id": "approved-pdfs", + "title": "Approved PDFs", + "role": "deliverable", + "required": true, + "count": { "min": 1, "max": 200 }, + "select": { + "include": ["Approved/**/*.pdf"], + "exclude": ["**/*draft*"], + "nameRegex": "^[^ ]+\\.pdf$", + "latestRevision": { "regex": "_v(\\d+)$", "scope": "name" } + }, + "expect": { "keyRegex": "^(A\\d{3})_", "keys": ["A101", "A102"], "allowUnexpected": false }, + "formats": ["pdf"], + "fileSize": { "min": "1 KB", "max": "200 MB" }, + "pdf": { "pages": { "min": 1 }, "allowPasswordProtected": false }, + "approval": { "mode": "evidenceAndConfirm", "evidence": { "pathRegex": "^Approved/" } }, + "target": { "folder": "01_Approved_PDFs", "name": "{client}-{project}_{stem}{ext}" }, + "severity": { "HO-NAME-010": "off" } +} +``` + +| Member | Type | Default | Notes | +| --- | --- | --- | --- | +| `id` | string | required | `^[a-z0-9][a-z0-9_-]{0,47}$`, unique. | +| `title` | string | `id` | Shown in review and `CONTENTS.txt`. | +| `description` | string | none | Shown as a tooltip/details. | +| `role` | `"deliverable"`, `"source"`, `"asset"`, `"document"`, `"supporting"` | `"deliverable"` | Recorded in outputs; no behavioural difference except default `CONTENTS.txt` ordering (in that order). | +| `required` | bool | false | Empty selection → `HO-REQ-001`. | +| `count.min` / `count.max` | integer | `required ? 1 : 0` / unlimited | `HO-REQ-002/003`. | +| `select` | object | required | C.4.6. | +| `expect` | object | none | C.4.6.4. | +| `formats` | array of format IDs | any | Detected format must be listed (`HO-FMT-001`). | +| `fileSize.min` / `.max` | size | none | `HO-CONT-005`. | +| `pdf` | object | none | C.4.7.1; applies to PDF candidates. When `formats` is present it must include `pdf` (else `HO-SPEC-016`). | +| `image` | object | none | C.4.7.2; applies to raster candidates only. When `formats` is present it must include at least one raster format (else `HO-SPEC-016`). | +| `approval` | object | `{ "mode": "none" }` | C.4.8. | +| `license` | object | none | C.4.9. | +| `target` | object | `{ "folder": "", "name": "{name}" }` | C.4.10. | +| `severity` | object | none | Rule-scoped overrides, same semantics as top level. | + +### C.4.6 Selection + +#### C.4.6.1 Globs + +- Matched against the working-root-relative path with `/` separators, case-insensitively (ordinal comparison after simple case folding). +- `*` matches any run of characters except `/`; `**` as a whole segment matches zero or more segments; `?` matches one character except `/`; `[abc]`, `[a-z]`, `[!abc]` match one character; `{a,b,c}` expands alternatives (no nesting, ≤ 16 alternatives). +- The **static prefix** of a glob is the sequence of leading segments that contain none of `*?[{`; it defines `{relDir}` (C.4.10). +- `select.include` (1–64 globs, required), `select.exclude` (0–64 globs). +- `select.nameRegex` (on the file name) and `select.pathRegex` (on the relative path) further filter candidates. +- `select.shared` (bool, default false): when true, files already claimed by an earlier rule may also be claimed by this rule (copied twice under different targets); otherwise the first matching rule owns the file (`HO-SEL-002` informs about later matches). + +#### C.4.6.2 Regular expressions + +ECMAScript syntax of `std::wregex`, always case-insensitive. Patterns are compiled during specification validation. + +| Full match (the whole subject) | Search (anywhere in the subject) | +| --- | --- | +| `select.nameRegex`, `select.pathRegex`, `naming.sourceNameRegex`, `variables.*.pattern` | `select.latestRevision.regex`, `expect.keyRegex`, `approval.evidence.nameRegex`, `approval.evidence.pathRegex` | + +Subjects longer than 1 024 UTF-16 units are not matched and produce `HO-SEL-007`. Backtracking is bounded by MSVC's `_REGEX_MAX_COMPLEXITY_COUNT`/`_REGEX_MAX_STACK_COUNT` (T1.4); a match that exceeds them is treated as "no match" and produces `HO-SEL-008`. + +#### C.4.6.3 Latest revision + +`select.latestRevision` keeps only the newest revision of each document: + +| Member | Default | Notes | +| --- | --- | --- | +| `regex` | required | Searched in the file **stem**; exactly one capturing group with the revision value (for example `_v(\d+)$`, `_R(\d{2})$`, `-rev([A-Z]+)$`). | +| `scope` | `"name"` | `"name"`: documents are grouped across folders by stem-without-revision; `"folder"`: grouped per folder. | +| `prefixOrder` | none | Ordered list of revision prefixes for status-coded schemes, for example `["P", "C"]` so that `C01` (construction) supersedes `P07` (preliminary) as in ISO 19650 practice. | + +Comparison: values are split into a letter prefix and a remainder. When `prefixOrder` is given, the prefix rank decides first (an unlisted prefix ranks lowest and produces `HO-SEL-006`). Then remainders compare numerically when both are digits, otherwise by natural ordering where letters compare as revision letters (`A < B < … < Z < AA`). Older revisions are excluded with `HO-SEL-001` (visible in the *Superseded* filter). A stem without a match is kept and reported with `HO-SEL-006`. The captured value is available as `{rev}`. + +#### C.4.6.4 Expected items + +```json +"expect": { + "keyRegex": "^([A-Z]-\\d{3})_", + "keys": ["A-101", "A-102"], + "registerCsv": { "path": "Register/drawing-register.csv", "column": "Sheet", "where": { "column": "Issue", "equals": "R04" } }, + "allowUnexpected": false +} +``` + +- `keyRegex` (required): searched in the file name; capture group 1 is the item key, available as `{key}`. +- Expected keys are the union of `keys` and the register values (`registerCsv.path` relative to the working root, header row required, `where` optional exact match, case-insensitive). +- Missing key → `HO-REQ-010` (one finding per key); several selected files with the same key → `HO-REQ-012`; key not expected → `HO-REQ-011` (warning unless `allowUnexpected`). Unreadable register → `HO-REQ-013`. + +### C.4.7 Formats and signatures + +A candidate's format is detected by **extension and content**: the extension selects the format and its content signature (if defined) must match, otherwise `HO-FMT-002`. An extension that belongs to no built-in or custom format yields format `unknown`: `HO-FMT-003` when `content.allowedFormats` is set, `HO-FMT-001` when the rule lists `formats`, otherwise `HO-FMT-004` (warning). Every extension maps to exactly one format. Built-in formats: + +| ID | Extensions | Signature check (from file start unless noted) | +| --- | --- | --- | +| `pdf` | .pdf | `%PDF-` within the first 1 024 bytes | +| `ai` | .ai | `%PDF-` (PDF-compatible) or `%!PS-Adobe` | +| `eps` | .eps | `%!PS-Adobe-` … `EPSF`, or DOS EPS binary `C5 D0 D3 C6` | +| `psd` | .psd, .psb | `8BPS` + version 1 (PSD) or 2 (PSB) | +| `svg` | .svg | UTF-8/UTF-16 text whose first element (after BOM, XML declaration, comments, DOCTYPE) is `