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 |
- How to use this document
- Decision record
- Part A — Feature overview and user guide
- Part B — Implementation task list
- Part C — Normative reference
- 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.mdapply 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.ps1and the GitHub Actions pipelines stay in parity (toolchain, prerequisites, filters, ordering, skip policy);- completion requires a full
scripts/runtests.ps1run 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.
| 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. |
| 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. |
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 |
| 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 |
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):
- 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).
| 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). |
| 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). |
| 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 | <package>.handoff-build.json beside the package: internal audit trail (source paths, reviewer decisions, spec snapshot). |
| Partial folder | The hidden .<package>.handoff-<id>.partial folder in which a package is built before publication. |
Working material (left pane, D:\Projects\ACME-Rebrand\):
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\.
- Navigate the left pane to
D:\Projects\ACME-Rebrandand the right pane toD:\Deliveries. Keep the left pane active. - Choose Plugins → Delivery Handoff → Build Delivery Package….
- In Build Delivery Package, the specification
client-delivery.handoff.jsonis preselected because it was found in the working folder's.handofffolder. Enter the variables the specification asks for — ClientACME, ProjectRebrand, RevisionR01. The package name preview showsACME-Rebrand_Delivery_2026-10-03_R01. Choose Scan…. - Review Delivery Package opens while the plug-in scans and inspects candidates:
- Approved PDFs:
Brand-Guidelines_v4.pdfandStationery_v2.pdfare selected;Brand-Guidelines_v3.pdfis 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.psdreports errorHO-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 evidencecity-skyline.licence.pdf, which will be copied into the package. - Licensed fonts:
Inter-Regular.otfmatches the register row inAssets\licences.csv(licence OFL-1.1, scope Unlimited). - Not included:
Artwork\WIP\Hero-explorations.psdand other unclaimed files are listed for information only;Thumbs.dbis listed as forbidden content that will never be packaged.
- Approved PDFs:
- 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.
- The result reads Verified — ready for review. The right pane refreshes and focuses the new folder:
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
- 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).
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 <current folder>\.handoff\). |
| Configuration (Plugins Manager) | Always | Specification library folder, recent specifications, review and staging preferences. |
The Specification list in the build dialog is populated in this order (duplicates by full path removed):
*.handoff.jsonin the nearest.handofffolder found by walking from the working root up to its volume root.*.handoff.jsondirectly in the configured specification library folder (for example a shared team folder).- Recently used specifications (up to 10) that still exist.
- 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.
- 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 new package folder named by the specification's
package.folderNametemplate 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.txtin the package root (names configurable by the specification;manifest.jsonis mandatory).- The build record beside the package (unless the specification sets
package.buildRecordto"none"). - A summary: Verified — ready for review, or Verified with warnings (listed), or Not built (reason; the working material and staging location are unchanged).
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).
| 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. |
| 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. |
Check each box when its acceptance criteria are met. Task IDs are stable; cite them in commits and pull requests.
-
T0.1 Scaffold the plug-in (→ C.2, C.3)
- Create
src/plugins/handoff/from the minimal menu-extension pattern insrc/plugins/demomenu/and the entry/connect/config pattern insrc/plugins/checksum/checksum.cpp. - Add
vcxproj/handoff.vcxproj(+.filters,handoff.props) forDebug|Win32,Debug|x64,Release|Win32,Release|x64, importingx86.props/x64.props,plugin_base.props,plugin_debug.props/plugin_release.propsexactly assrc/plugins/checksum/vcxproj/checksum.vcxproj:53-100does. - Set
<LanguageStandard>stdcpplatest</LanguageStandard>(precedentsrc/plugins/ftp/vcxproj/ftp.vcxproj:105) and add..\..\..toAdditionalIncludeDirectoriessocommon/relative_file_operations.hresolves (precedentsrc/plugins/ftp/vcxproj/ftp.props:8,src/plugins/ftp/precomp.h:23). - Add
vcxproj/lang_handoff.vcxproj+lang_handoff.props(ShortProjectName=handoff) andvcxproj/lang_pl_handoff.vcxprojmirroringsrc/plugins/checksum/vcxproj/lang_*.vcxproj; outputs areplugins\handoff\lang\english.slgandpolish.slg(src/plugins/shared/vcxproj/lang_base.props:7-18,lang_pl_base.props:7-19). -
handoff.defexportsSalamanderPluginEntryandSalamanderPluginGetReqVer(copysrc/plugins/checksum/checksum.def). -
versinfo.rh2with plug-in version1.0.0, description "Delivery Handoff plugin for Open Salamander", copyright "Copyright © 2026 Taskscape Ltd". - Add the three projects to
src/vcxproj/salamand.slnwith all four solution configurations mapped, next to the other plug-in projects. - 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 0x0000010044600000inbaseaddr_x64.txt;0x21b00000,0x31b00000,0x44600000inbaseaddr_x86.txt. These slots are unused at165a2c9; confirm withdumpbin /headersthathandoff.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
foldersat0x22000000, sohandoffmoved to0x000001002a800000/0x2a800000(free up tolang). See C.13.1.
- Implementation note: Done with a correction: the Debug x64 image (5.5 MB, C++/WinRT and debug STL) overlapped
- Acceptance: the solution builds
Debug|Win32,Debug|x64,Release|x64with 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.
- Create
-
T0.2 Engine/host boundary (→ C.2.2)
- Create
src/plugins/handoff/engine/. Engine files MUST NOT includeprecomp.h, SDK headers (spl_*.h),dbg.h, or WinLib; only Windows SDK, C++ standard library, andsrc/common/relative_file_operations.h. - Mark engine
.cppfilesPrecompiledHeader=NotUsingin the plug-in project. - Engine code MUST NOT depend on
charsignedness: 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 onwindows-2025(.github/workflows/nightly-parser-fuzz.yml), where VS 2026v145is 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).
- Implementation note: Not verified: no VS 2022 toolset on the implementation machine. The probe command line defines
- Add the host abstraction interfaces
IHandoffFileSystem,IHandoffClock,IHandoffProgress,IPdfPageInspector,IImageInspector(→ C.2.3).- Implementation note: Implemented as
IHandoffFileSystem,IProgress/IBuildCallbacks,IPdfPageInspector, andIImageInspector; time is passed inBuildInputinstead of anIHandoffClock(C.13.1).
- Implementation note: Implemented as
- Acceptance: an empty native console project can compile every engine
.cppwith only/I src/plugins/handoff/engine /I src.
- Create
-
T0.3 Spike: page inspection with
Windows.Data.Pdf(→ C.5.4.3)- Implement
engine/pdf_pages_winrt.cpp(C++/WinRT headers from the Windows SDK, no PCH,#undef GetCurrentTimebefore WinRT headers) on an MTA worker thread. Linkshcore.liband the import library C++/WinRT requires (WindowsApp.libper C++/WinRT guidance, orruntimeobject.libifWindowsApp.libre-routes Win32 imports undesirably); record the choice in C.13. - Open through
SHCreateStreamOnFileEx(\\?\path,STGM_READ | STGM_SHARE_DENY_WRITE) →CreateRandomAccessStreamOverStream→PdfDocument::LoadFromStreamAsync,wait_for(30 s), readPageCount,IsPasswordProtected, and each page'sSize(DIPs). - Verify on Win32 and x64, in Debug and Release, on Windows 10 1809+ and Windows 11; run
tools/audit-pe-hardening.ps1on the Releasehandoff.spl.- Implementation note: Windows 11: x64 Debug exercised by engine and UI tests; Win32/x64 Debug/Release build cleanly;
audit-pe-hardening.ps1passes for the Release x64handoff.spl. Windows 10 1809+ not verified.
- Implementation note: Windows 11: x64 Debug exercised by engine and UI tests; Win32/x64 Debug/Release build cleanly;
- Record the outcome under C.13. 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.
- Implement
-
T0.4 Spike: WIC header probe (→ C.5.4.4)
- On an MTA worker:
IWICImagingFactory::CreateDecoderFromFilename(..., WICDecodeMetadataCacheOnDemand), frame 0GetSize,GetResolution,GetPixelFormat,IWICPixelFormatInfo2, frame count, and GPS query for JPEG/TIFF. - 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.
- On an MTA worker:
-
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.ps1with-BaseCommit origin/mainandtools/test-unsafe-api-baseline.ps1on 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.ps1passes. Re-run the scripts after committing.
- Implementation note: The diff ratchets compare commits; the uncommitted work was checked with their exact patterns (no findings) and
-
T1.1 Bounded JSON reader
engine/json_reader.{h,cpp}(→ C.4.1)- Strict RFC 8259: one value, no trailing content, no comments, no trailing commas, no
NaN/Infinity. - Input is UTF-8; a leading BOM is accepted and skipped; invalid UTF-8 →
HO-SPEC-001.\uescapes decode surrogate pairs; an unpaired surrogate and\u0000are errors. - Duplicate member names in one object →
HO-SPEC-007. - Every value records line and column (1-based, columns in Unicode scalar values) and a JSON Pointer path.
- Numbers are kept as lexemes; the model converts them (integer members reject fractions and values outside ±2^53).
- Enforce the limits table per profile (spec / manifest); exceeding a limit →
HO-SPEC-006with 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.
- Strict RFC 8259: one value, no trailing content, no comments, no trailing commas, no
-
T1.2 Specification model and validator
engine/spec_model.{h,cpp}(→ C.4)- Typed model for every member in C.4.2–C.4.13 with defaults applied.
- Validation emits
HO-SPEC-*findings with JSON Pointer, line, and column; validation continues after errors to report as many as possible (cap 200 findings). - Unknown members →
HO-SPEC-002warning with a "did you meanx?" suggestion when a known member is within Levenshtein distance 2.$commentmembers are ignored at any level. - 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). - 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_modelitself 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.
-
T1.3 Glob matcher
engine/glob.{h,cpp}(→ C.4.6.1)- Acceptance: table-driven tests for
*,**,?,[...],[!...],{a,b}, case-insensitivity, separators, static-prefix extraction.
- Acceptance: table-driven tests for
-
T1.4 Regular-expression guard
engine/regex_guard.{h,cpp}(→ C.4.6.2)- Wrap
std::wregex(ECMAScript | icase | optimize); compile at spec validation; reject patterns over 512 UTF-16 units; catchstd::regex_error(includingerror_complexity,error_stack) at compile and match time and convert to findings. - Bound backtracking with MSVC's
_REGEX_MAX_COMPLEXITY_COUNTand_REGEX_MAX_STACK_COUNT, defined project-wide with identical values inhandoff.props, the engine test project, and the parser-probe command line (per-file definitions would violate the one-definition rule for the<regex>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.
- Implementation note: Superseded: the VS 2026 STL bounds backtracking itself (no
- Acceptance: catastrophic patterns from the hostile corpus finish within 1 s per subject and produce
HO-SPEC-010(at validation) orHO-SEL-008(at match time).
- Wrap
-
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 andyyyy-MM-ddparsing. -
T1.6 Name templates and sanitization
engine/name_template.{h,cpp}(→ C.4.10)- 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
portableandunicodemodes.
- Token expansion, NFC normalization (
-
T1.7 Embedded templates validate cleanly (→ C.4.14, C.4.15): the four template files validate with zero findings in an engine test.
-
T2.1 Scanner
engine/scanner.{h,cpp}(→ C.5.2)- Implementation note: Implementation and limit/cancellation tests done; the 100 000-entry timing budget has not been measured.
- 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/**. - Cooperative cancellation between directories and every 256 entries; progress every ≥ 100 ms (
GetTickCount64). - 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.
-
T2.2 Assembler
engine/assembler.{h,cpp}(→ C.5.3)- Rule matching in specification order with first-match ownership (unless
select.shared), globalcontent.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-*andHO-REQ-*code.
- Rule matching in specification order with first-match ownership (unless
-
T2.3 Format detection
engine/format_detect.{h,cpp}(→ C.4.7): built-in signature table + custom formats; reads ≤ 4 KiB from the file start. -
T2.4 PDF structure probe
engine/pdf_structure.{h,cpp}(→ C.5.4.2): header/version,%%EOF,startxref,/Encrypt,/Linearized; reads ≤ 2 × 64 KiB. -
T2.5 PDF page inspector
engine/pdf_pages_winrt.cppbehindIPdfPageInspector(from T0.3), plusNullPdfPageInspectorused by thecl.exeparser probe. -
T2.6 Image inspector
engine/image_probe.{h,cpp}(→ C.5.4.4, C.5.4.5): WIC raster formats and PSD/PSB header parsing; GPS presence. -
T2.7 CSV reader
engine/csv_reader.{h,cpp}(→ C.4.1): RFC 4180, UTF-8 (BOM optional), header row required, quoted fields with embedded commas/quotes/newlines, limits. -
T2.8 Evidence
engine/evidence.{h,cpp}(→ C.4.8, C.4.9): approval patterns, sidecars, registers; licence sidecars, registers, expiry, scope, evidence files to copy. -
T2.9 Findings and gating
engine/findings.{h,cpp}(→ C.6): finding model, severity resolution (default → spec override → reviewer override), non-overridable set, gate computation (canBuild,needsAcknowledgement).
- T3.1 Planner
engine/planner.{h,cpp}(→ C.5.5): 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. - T3.2 File-system abstraction
engine/file_system.{h,cpp}: Win32 implementation ofIHandoffFileSystemand a fault-injecting test double able to fail any call by ordinal and error code. - T3.3 SHA-256
engine/sha256_cng.{h,cpp}:BCryptOpenAlgorithmProvider(BCRYPT_SHA256_ALGORITHM)once per worker, reusable hash objects (BCRYPT_HASH_REUSABLE_FLAG), linkbcrypt.lib. - T3.4 Stager
engine/stager.{h,cpp}(→ C.5.7): 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. - T3.5 Output writers
engine/output_writers.{h,cpp}(→ C.7): deterministicmanifest.json,manifest.csv(formula guard),CONTENTS.txt. - T3.6 Build record
engine/build_record.{h,cpp}(→ C.7.4). - T3.7 Publisher
engine/publisher.{h,cpp}(→ C.5.7, steps 9–10): handle-bound directory rename viaRenameRelativePublicationFile(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 group Build pass, including every fault-injection point leaving no final package and no unowned deletions.
- T4.1 Manifest reader
engine/manifest_reader.{h,cpp}: stricthandoffManifest: 1schema (C.7.1) using the manifest limit profile. - T4.2 Specification resolution for verification (→ C.5.8): build record snapshot → library → recent list → user choice; hash comparison.
- 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.
- T5.1 Entry point
handoff.cpp(→ C.3.1): version check againstLAST_VERSION_OF_SALAMANDER,LoadLanguageModule(parent, "Handoff"),SetBasicPluginData(..., FUNCTION_CONFIGURATION | FUNCTION_LOADSAVECONFIGURATION, ..., "Handoff"), help filehandoff.chm,InitializeWinLib("Handoff", DLLInstance),SetupWinLibHelp. - T5.2 Connect: menu items with the state masks in C.3.2, translator comment block for
export_mnu.py(as insrc/plugins/checksum/checksum.cpp:243-252), plug-in icon (SetBitmapWithIcons,SetPluginIcon,SetPluginMenuAndToolbarIcon). - T5.3
ExecuteMenuItem(→ C.3.3): main-thread session capture, single-session guard, start the UI thread; internal commandCMD_INTERNAL_FOCUSfor "Show in panel" and package focus (FocusNameInPanelis main-thread only,spl_gen.h:1578-1584). - 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; onIsCriticalShutdown()cancel immediately without UI. - T5.5 Configuration persistence (→ C.9) and the Configuration dialog.
- T5.6 String boundary helpers
ui/sdk_strings.{h,cpp}: UTF-8 ↔ UTF-16 conversions withMB_ERR_INVALID_CHARS/WC_ERR_INVALID_CHARS, long-path normalization, heap buffers sized from SDK limits; no fixedMAX_PATHarrays (ratchet,tools/max-path-buffer-exemptions.md). - T5.7
HelpForMenuItemmapping command IDs toIDH_*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.
- T6.1 Build session dialog
IDD_HO_SESSION(→ C.8.2): paths, swap, scope, specification list/browse/validate/show, variables editor, live package-name preview and validation. - T6.2 Review window
IDD_HO_REVIEW(→ C.8.3): 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. - 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.
- T6.4 Result dialog
IDD_HO_RESULT(→ C.8.4). - T6.5 Verify window
IDD_HO_VERIFY(→ C.8.5). - T6.6 New specification dialog
IDD_HO_NEWSPEC(→ C.8.6); writes withCREATE_NEWand never overwrites. - T6.7 Validate specification dialog
IDD_HO_VALIDATE(→ C.8.7). - T6.8 Override dialog
IDD_HO_OVERRIDE(→ C.8.8). - T6.9 Report export: plain-text report (UTF-8 with BOM, CRLF) through
IFileSaveDialogon the UI thread. - T6.10 Accessibility, DPI, theming (→ C.8.1): 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).
- T7.1 Templates
templates/*.handoff.jsonembedded asRCDATAinhandoff.rc(language-independent resources) (→ C.4.14). - T7.2 English resources
lang/lang.rc,lang/lang.rc2,lang/lang.rhwith#pragma code_page(65001)(patternsrc/plugins/checksum/lang/lang.rc:3): all dialogs, menu texts, findings messages (IDS_HO_<code>), contents-list labels.- 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 ownedstd::wstrings, because the returned buffer is shared and cyclic. - Messages are formatted with bounded APIs (
StringCchPrintfW/_snwprintf_s);sprintf-family calls are rejected bytools/verify-no-new-unsafe-string-calls.ps1. Placeholders are positional-safe for Polish word order (format pieces separately where order differs).
- UI and worker code load text with
- T7.3 Polish resources
lang/pl/lang_pl.rc,lang/pl/lang_pl.rc2with complete parity. Acceptance:tools/verify-language-resource-parity.ps1 -BuildDirectory .\src -IncludePattern '*\salamander\Debug_x64\*'passes. - 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. AddhandofftoPLUGIN_LISTinhelp/src/compileall.bat; add the merge entry next to Checksum inhelp/src/salamand.hhc:914-918; mapIDH_*inhandoff.rh2and[MAP]/[ALIAS]ofhandoff.hhp(patternsrc/plugins/checksum/help/checksum.hhp:24-25).- Implementation note: Help pages and project are generated and wired into
compileall.bat,salamand.hhc, andsalamand.hhp; CHM compilation was not run (HTML Help Workshop not installed).
- Implementation note: Help pages and project are generated and wired into
- T7.5 Icon:
src/res/toolbars/PluginHandoff.svg,24/PluginHandoff.svg,32/PluginHandoff.svg(Fluent System Iconsbox_checkmarkregular or the nearest available glyph; first line<!-- Fluent System Icons: … -->; one approved palette colour; exactwidth/height/viewBoxper size); add{"handoff.spl", "PluginHandoff"}toGetPluginSVGNameinsrc/svg.cpp:200-232; add a 16 × 16 menu bitmaphandoff.bmp. Acceptance:tools/verify-fluent-icon-coverage.ps1passes.- Implementation note: Uses the vendored Fluent
boxglyph (also used by DriveDropbox) becausebox_checkmarkis not in the vendored set; registered influent-map.jsonandfluent-provenance.json.
- Implementation note: Uses the vendored Fluent
- T7.6 Other languages: no
translations/*/handoff.sltin v1; other UI languages fall back to English. Record in C.13.
- T8.1 Native engine tests
tests/HandoffEngineTests/(→ C.12.1): console executable compiling engine sources directly (patterntests/PictViewEngineTests/PictViewEngineTests.vcxproj:29-41), all fixtures generated at runtime below a GUID directory under%TEMP%, removed afterwards; non-zero exit on any failure. - T8.2 Parser probe and corpus (→ C.12.2):
tools/test-handoff-spec-parser.ps1,tools/handoff_spec_parser_probe.cpp,tests/handoff-specs/{valid,invalid,hostile}/with expected-findings files;-Architecture x64|x86and-Iterations N. - T8.3 UI tests
tests/FileManager.UiTests/HandoffUiTests.cs(categoryUI) (→ C.12.3).- Generalize the private
WaitForFtpPluginCommand(int pluginCommand, string commandName)(Infrastructure/FileManagerUiTestBase.cs:506-541) intoWaitForPluginCommand(string dllLeaf, int pluginCommand, string commandName)readingUiTestSettings.PluginCommandMapPathfor the launchedApplication.ProcessId; keepWaitForFtpPluginCommandas a thin wrapper (the root runner contract asserts its name,tests/FileManager.UiTests/NativeSafetyRegressionTests.cs:53-55). - Add
RequireHandoffPluginRuntime()that fails (not ignores) whenplugins\handoff\handoff.splorplugins\handoff\lang\english.slgis missing beside the executable under test — the runner always builds the plug-in, so absence is a defect, not a capability skip. - Extend the focused runner
run-ui-tests.ps1(Test-FtpUiRuntime/New-FtpUiTestRuntime, lines 55-125) so it stageshandoff.splandlang\english.slgfromsrc\plugins\handoff\vcxproj\salamander\<configuration>\plugins\handoffinto the disposable runtime when the filter can select Handoff tests. - Reference dialog/control IDs with comments naming their
lang.rhsymbols (patternInfrastructure/NativeCommands.cs:493).
- Generalize the private
- T8.4 Source-contract tests
tests/FileManager.UiTests/HandoffSourceContractTests.cs(non-UI): publication usesRenameRelativePublicationFile; the engine contains noMOVEFILE_REPLACE_EXISTINGorCopyFile*;DeleteFileW/RemoveDirectoryWappear only inengine/file_system.cppbehindDeleteOwned; every template intemplates/is embedded inhandoff.rc; everyHO-*code in C.6 has a string in both language files.
- T9.1
scripts/runtests.ps1- Add
$handoffEngineProjectandInvoke-HandoffEngineTestsmirroringInvoke-PictViewEngineTests(scripts/runtests.ps1:444-475), andInvoke-AutomatedCheck -Name 'HandoffEngineTests (Debug x64)'after the PictView check (:893). - Run
tools\test-handoff-spec-parser.ps1forx64andx86inside the existingforeach ($architecture in @('x64', 'x86'))probe loop used for zlib/bzip2. - Keep skip policy unchanged: both checks are blocking and never skip when VS 2026 is present.
- Add
- T9.2 GitHub workflows
-
.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', aspr-msbuild.yml:125-135), same invocation as the runner. -
.github/workflows/build-installer.yml: no new step — the release gate runsruntests.ps1(:104); confirm the new checks appear in its output. -
.github/workflows/nightly-parser-fuzz.yml: step "Replay Handoff specification hostile corpus" with-Iterations 250.
-
- T9.3 Runner contract: extend
Root_test_runner_collects_every_documented_automated_test_layer(tests/FileManager.UiTests/NativeSafetyRegressionTests.cs:16) withDoes.Contain("test-handoff-spec-parser.ps1")andDoes.Contain("HandoffEngineTests"). - T9.4 Packaging: add
'handoff\handoff.spl'and'handoff\lang\english.slg'to$requiredPluginPayloadsintools/prepare_installer.ps1:155-160so a regression that drops the plug-in fails staging. Nosetup.isschange (it copiesplugins\*recursively,Installer/setup.iss:91). - T9.5 Documentation
-
README.md: "What's new in 6.0 → Plugins and network": one bullet for Delivery Handoff. -
architecture.md§5.3 "Representative plug-in families → utilities": add "delivery handoff". -
testing.md: catalog entries forHandoffEngineTests,test-handoff-spec-parser.ps1,HandoffUiTests,HandoffSourceContractTests; quick-start commands. -
tests/FileManager.UiTests/README.md: Handoff cases and their fixtures. - No
doc/third_party.txtchange (no third-party code is added).
-
- 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.
- 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
- 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) 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.
- The quick-start scenario in A.5.1 works end to end with the Client delivery (example) template.
- Omissions (empty required rule, missing expected key, count below minimum) block the build and are listed.
- Naming, format, PDF, image, allowed-content, approval, and licence rules produce the documented findings.
- A built package contains exactly the planned files plus outputs; every hash re-verifies; the build record is beside the package.
- Cancel, I/O failure, source change, name collision, and process termination never leave a package under the final name and never modify working material.
- Verify detects missing, modified, and unlisted files and re-checks rules.
- English and Polish resources are complete; help topics exist for every command.
- All new automated checks run in
runtests.ps1and GitHub Actions with identical inputs; the full regression run passes.
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 to2 * MAX_PATHbytes,CFileData::NameLenis 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 (
CSalamanderForOperationsAbstractexposes only progress andMoveFiles,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).
| 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). |
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
| 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 |
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
// 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/GetFreeSpaceEngine entry points (each runs on a worker thread and is cancellable):
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&);| 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 |
| 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).
- Main thread (
ExecuteMenuItem): read both pane paths withGetPanelPath(reject non-PATH_TYPE_WINDOWS→HO-SES-001/002); determine which physical pane is source withGetSourcePanel(); read the selection (names, is-dir) or focused item; readSALCFG_ALWAYSONTOPwithGetConfigParameter(main thread only). Build an immutableSessionInput(UTF-16). No disk I/O on the main thread. - 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.
- UI thread: started through
CThread::Create(ThreadQueue)(patternsrc/plugins/checksum/dialogs.cpp:2221-2310); callsCoInitializeEx(COINIT_APARTMENTTHREADED); creates modeless top-level windows withCDialog::Create()and no owner so the main window stays usable; honours always-on-top; registers windows in aCWindowQueue; runs its ownGetMessage/IsDialogMessageloop; ends when its last window closes. - Workers: started from the UI thread through
CThreadQueue::StartThread(CThreadQueueStopBody, …); each callsCoInitializeEx(COINIT_MULTITHREADED)(WIC, WinRT). Workers never touch HWNDs exceptPostMessageto the owning window; results are transferred as heap objects owned by the message (receiver deletes). Retry prompts use a cross-threadSendMessageto the UI thread while no lock is held (architecture.md§7.1). - Release:
Release(parent, force)closes windows (CWindowQueue::CloseAllWindows(force)); if a build is running andforce == FALSE, returnFALSEafter telling the user; otherwise signal stop, wait (CThreadQueue::KillAll(force, 5000, 2000)), and the stager deletes owned files or leaves a marked partial folder. UnderIsCriticalShutdown()no UI is shown. - The plug-in creates no threads except through
CThreadQueue/CPluginThreadOwner(ratchetverify-no-new-raw-thread-creation.ps1) and never callsTerminateThread.
- File name
*.handoff.json; UTF-8 with optional BOM; strict RFC 8259 JSON;$commentmembers are ignored anywhere. - Member names are case-sensitive. Unknown members produce
HO-SPEC-002warnings (forward-compatible authoring) — an unsupportedhandoffSpecversion 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 | — |
| 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). |
"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.
"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).
{
"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. |
- 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) andselect.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-002informs about later matches).
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.
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}.
"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
keysand the register values (registerCsv.pathrelative to the working root, header row required,whereoptional 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 unlessallowUnexpected). Unreadable register →HO-REQ-013.
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- 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 <svg |
indd |
.indd | GUID 06 06 ED F5 D8 1D 46 E5 BD 31 EF E7 FE 74 B7 1D |
png |
.png | 89 50 4E 47 0D 0A 1A 0A |
jpeg |
.jpg, .jpeg, .jpe | FF D8 FF |
tiff |
.tif, .tiff | 49 49 2A 00, 4D 4D 00 2A, BigTIFF 49 49 2B 00, 4D 4D 00 2B |
gif |
.gif | GIF87a or GIF89a |
bmp |
.bmp | BM |
webp |
.webp | RIFF + 4 bytes + WEBP |
heic |
.heic, .heif | box ftyp at offset 4 with brand heic, heix, mif1, or msf1 |
avif |
.avif | box ftyp at offset 4 with brand avif or avis |
zip |
.zip | PK 03 04 or PK 05 06 |
docx, xlsx, pptx |
.docx, .xlsx, .pptx | PK 03 04 (container only) |
dwg |
.dwg | AC10 + two digits |
dxf |
.dxf | ASCII: optional whitespace, 0, line break, SECTION |
ifc |
.ifc | ISO-10303-21; |
otf |
.otf | OTTO |
ttf |
.ttf | 00 01 00 00 or true |
ttc |
.ttc | ttcf |
woff, woff2 |
.woff, .woff2 | wOFF, wOF2 |
mp4 |
.mp4, .m4v | box ftyp at offset 4 with any brand except those listed for heic, avif, and mov |
mov |
.mov | box ftyp at offset 4 with brand qt |
txt |
.txt | no NUL bytes in the first 4 KiB and valid UTF-8 (or BOM-marked UTF-16) |
csv |
.csv | as txt |
md |
.md | as txt |
Raster formats (image constraints apply): png, jpeg, tiff, gif, bmp, webp, heic, avif, psd.
Custom formats:
"formats": {
"rvt": { "extensions": [".rvt"], "magic": [ { "offset": 0, "hex": "D0CF11E0A1B11AE1" } ], "label": "Revit model" },
"log": { "extensions": [".log"], "text": true }
}magic entries are alternatives (hex or ascii, offset ≤ 4 096 − length); text: true applies the txt check; no magic/text means extension-only. Custom IDs must not shadow built-in IDs, and custom extensions must not reuse an extension of a built-in or another custom format (HO-SPEC-014).
"pdf": {
"version": { "min": "1.4", "max": "2.0" },
"pages": { "min": 1, "max": 1 },
"pageSizes": ["A1", "A3", { "widthMm": 841, "heightMm": 594 }],
"orientation": "any",
"toleranceMm": 2,
"allPagesSameSize": false,
"allowPasswordProtected": false
}| Member | Finding | Notes |
|---|---|---|
version.min / .max |
HO-PDF-002 |
Header version, "1.0"–"2.0". |
allowPasswordProtected |
HO-PDF-010 |
Default false; /Encrypt or a password-protected load. |
pages.min / .max |
HO-PDF-020 |
Requires page inspection; unavailable → HO-PDF-031. |
pageSizes |
HO-PDF-021 |
Every page must match one entry; unknown name → HO-SPEC-018. |
allPagesSameSize |
HO-PDF-022 |
Default false. |
orientation |
HO-PDF-023 |
"any" (default), "portrait", "landscape". |
Named sizes (portrait width × height in mm): A0 841×1189, A1 594×841, A2 420×594, A3 297×420, A4 210×297, A5 148×210, A6 105×148, B1 707×1000, B2 500×707, B3 353×500, B4 250×353, Letter 215.9×279.4, Legal 215.9×355.6, Tabloid 279.4×431.8, ARCH-A 228.6×304.8, ARCH-B 304.8×457.2, ARCH-C 457.2×609.6, ARCH-D 609.6×914.4, ARCH-E 914.4×1219.2. A size matches either orientation unless orientation is "portrait" or "landscape". toleranceMm (default 2, decimal allowed, 0–20) applies to each dimension.
"image": {
"width": { "min": 1200, "max": 10000 },
"height": { "min": 800 },
"longEdge": { "min": 3000 },
"shortEdge": { "min": 2000 },
"exact": [ { "width": 512, "height": 512 }, { "width": 1024, "height": 1024 } ],
"aspectRatio": { "value": "16:9", "tolerance": 0.01 },
"dpi": { "min": 300 },
"colorModel": ["rgb", "cmyk"],
"alpha": "any",
"bitDepth": { "min": 8 },
"frames": { "max": 1 },
"forbidGps": true
}| Member | Finding | Notes |
|---|---|---|
width, height |
HO-IMG-010 |
pixels |
exact |
HO-IMG-011 |
one of the listed pixel sizes |
aspectRatio |
HO-IMG-012 |
"W:H" or decimal; relative tolerance (default 0.01) |
longEdge, shortEdge |
HO-IMG-013 |
pixels |
dpi.min |
HO-IMG-020 (default warning) |
horizontal and vertical resolution metadata; unknown resolution → HO-IMG-032 |
colorModel |
HO-IMG-021 |
rgb, cmyk, gray, indexed, lab |
alpha |
HO-IMG-022 |
"any", "required", "forbidden" |
bitDepth.min |
HO-IMG-023 |
bits per channel |
frames.max |
HO-IMG-024 |
frames/pages/layers as reported by WIC (not PSD layers) |
forbidGps |
HO-IMG-025 (default warning) |
GPS latitude present in EXIF/XMP |
"approval": {
"mode": "evidenceAndConfirm",
"evidence": {
"pathRegex": "(^|/)05_Approved/",
"nameRegex": "_APPROVED(?=\\.[^.]+$)",
"sidecar": ["{stem}.approved", "{stem}.approval.txt"],
"registerCsv": { "path": "Approvals.csv", "fileColumn": "File", "statusColumn": "Status", "approvedValues": ["Approved", "A"] }
}
}mode |
Meaning |
|---|---|
none |
No approval requirement. |
evidence |
At least one evidence kind must match; otherwise HO-APP-001. |
confirm |
The reviewer must approve each candidate (Approve/Approve all in rule); otherwise HO-APP-002. |
evidenceAndConfirm |
Both. |
Evidence kinds: pathRegex (relative path), nameRegex (file name), sidecar (file names relative to the candidate's folder; tokens {stem}, {name}; */? allowed), registerCsv (row whose fileColumn equals the candidate's relative path, or its file name when the cell contains no /, and whose statusColumn is in approvedValues, case-insensitive). An unreadable register is HO-APP-003. Reviewer approvals record the Windows user name (GetUserNameExW(NameSamCompatible), falling back to GetUserNameW) and UTC time in the build record only.
"license": {
"required": true,
"sidecar": ["{stem}.licence.pdf", "{stem}.licence.txt", "LICENSE*.txt"],
"registerCsv": {
"path": "Assets/licences.csv",
"fileColumn": "File", "licenceColumn": "Licence", "licensorColumn": "Licensor",
"expiresColumn": "Expires", "scopeColumn": "Scope",
"allowedScopes": ["Client delivery", "Unlimited"]
},
"includeEvidence": true,
"evidenceFolder": "03_Licensed_Assets/Licences",
"expiryWarningDays": 30
}- Evidence is satisfied by a matching sidecar file or a register row (same matching as approval registers). Missing evidence with
required: true→HO-LIC-001. expiresColumnvalues areyyyy-MM-dd; empty means perpetual. Expired before the build date →HO-LIC-002; withinexpiryWarningDays→HO-LIC-003. Unparseable date →HO-LIC-005.allowedScopes: when present, a register row whose scope is not listed →HO-LIC-004.includeEvidence: truecopies sidecar evidence into the package (default folder: the asset's target folder;evidenceFolderoverrides, package-relative template). Evidence files keep their source file name, sanitized with the naming policy, and take part in collision checks. They are manifest entries with rolelicence-evidenceand alicenceForlink to the asset path(s). One evidence file used by several assets is copied once. Register rows are never copied; their licence data is written to the manifest entry of the asset.- Files used as evidence or registers are not reported as Not included.
"naming": {
"charset": "portable",
"allowSpaces": false,
"case": "keep",
"maxNameLength": 100,
"transliterate": true,
"replacement": "_",
"sourceNameRegex": "^[^ ]+$"
}| Member | Default | Notes |
|---|---|---|
charset |
"portable" |
portable: A–Z a–z 0–9 . _ - (plus space if allowSpaces); unicode: any character valid in Windows names. |
allowSpaces |
false | |
case |
"keep" |
"lower", "upper" apply to the whole name including the extension. |
maxNameLength |
120 | UTF-16 units; truncation shortens the stem and keeps the extension (HO-NAME-015). |
transliterate |
true | For portable: NFKD decomposition with combining marks removed, plus the table Ł→L ł→l Đ→D đ→d Ø→O ø→o Æ→AE æ→ae Œ→OE œ→oe ß→ss Þ→Th þ→th ı→i. |
replacement |
"_" |
Single portable character replacing disallowed characters; runs collapse. |
sourceNameRegex |
none | Working file names not matching → HO-NAME-001 (warning). |
Rule target:
| Member | Default | Notes |
|---|---|---|
folder |
"" (package root) |
Template; /-separated; each segment sanitized. |
name |
"{name}" |
Template for the file name. |
keepSubfolders |
false | When true, {relDir} (path below the matching include glob's static prefix) is appended to folder. |
Template tokens: package variables and built-ins; {date:fmt}; per file {stem}, {ext} (with dot, as in the source), {name}, {key}, {rev}, {rule}, {relDir}, {seq}/{seq:000} (1-based within the target folder after sorting by source relative path; the digits give zero padding). Unknown token → HO-SPEC-012; {key}/{rev} used without expect/latestRevision → HO-SPEC-012.
Sanitization pipeline (each segment and name): expand → NFC → transliterate (portable) → replace disallowed characters → collapse replacement runs → trim leading/trailing spaces, dots, and replacement characters → case transform → truncate → reject empty (HO-NAME-011), Windows-reserved names CON PRN AUX NUL COM1–COM9 LPT1–LPT9 with any extension and trailing dot/space (HO-NAME-014). A result differing from the expanded template → HO-NAME-010 (info, shows original and new).
"content": {
"allowedFormats": ["pdf", "ai", "psd", "svg", "png", "jpeg", "tiff", "otf", "ttf", "woff2"],
"forbid": ["**/Thumbs.db", "**/.DS_Store", "**/desktop.ini", "**/~$*", "**/*.tmp", "**/*.bak", "**/__MACOSX/**", "**/.git/**"],
"hidden": "forbid",
"system": "forbid",
"emptyFiles": "forbid",
"maxFileSize": "4 GB",
"duplicates": "warn",
"unassigned": "info"
}| Member | Default | Effect |
|---|---|---|
allowedFormats |
any | Package-wide allowlist by detected format (HO-FMT-003). |
forbid |
**/Thumbs.db, **/.DS_Store, **/desktop.ini, **/~$*, **/*.tmp, **/*.bak, **/__MACOSX/**, **/.git/** |
Globs. A specified list replaces the default (repeat default entries to keep them). Matching working files are never candidates (HO-CONT-001, info in Not included); matching package files fail verification (HO-VER-006). |
hidden / system |
"forbid" |
Hidden/system candidates → HO-CONT-002/003. "allow" permits them. |
emptyFiles |
"forbid" |
"forbid" error, "warn" warning, "allow" nothing (HO-CONT-004). |
maxFileSize |
none | HO-CONT-005. |
duplicates |
"warn" |
Identical SHA-256 at different target paths, evaluated during build (HO-CONT-006); "forbid" fails the build before publication. |
unassigned |
"info" |
Severity of Not included entries (HO-SEL-003); "off" hides them. |
Implicit, non-configurable content rules: directory links are never traversed (HO-SCAN-002); file links (symbolic links, hard-link-free reparse files other than cloud placeholders) are never candidates (HO-SCAN-003); only the default data stream is copied and sources with named streams get HO-CONT-007 (info); names invalid on Windows and case/normalization collisions are errors.
"severity": { "HO-IMG-020": "error", "HO-NAME-010": "off" },
"policy": { "allowErrorOverride": false, "requireWarningAcknowledgement": true }- Defaults:
policy.allowErrorOverrideisfalse;policy.requireWarningAcknowledgementistrue. - Resolution order: catalogue default → top-level
severity→ ruleseverity→ reviewer override (errors only, whenallowErrorOverride). - Codes marked 🔒 in C.6 cannot be changed by
severity(HO-SPEC-017) or overridden by reviewers. - Reviewer overrides require a reason of at least 10 characters; they are recorded in the build record and counted in
summary.overridesof the manifest.
The validator reports HO-SPEC-* findings as path(line:column) CODE severity: message [JSON Pointer], for example:
client-delivery.handoff.json(42:17) HO-SPEC-010 error: Invalid regular expression in /rules/0/select/latestRevision/regex: missing ')'.
client-delivery.handoff.json(57:5) HO-SPEC-002 warning: Unknown member "licence" in /rules/2 — did you mean "license"?
| Resource | Title | Highlights |
|---|---|---|
design-studio.handoff.json |
Design studio handoff | logos (SVG/AI + PNG at exact sizes 512/1024/2048), guidelines PDF (latest version, approved), fonts with licence register, no working files |
architecture-issue.handoff.json |
Architecture drawing issue | sheet PDFs with expect.registerCsv from a drawing register, latest revision _R(\d{2}), sizes A1/A3, DWG and IFC models, naming {project}-{key}-{rev}{ext} |
consultancy-report.handoff.json |
Consultancy report delivery | final report PDF approved by sidecar {stem}.approved, appendices, CSV/XLSX data, forbid *draft*, *comments* |
client-delivery-example.handoff.json |
Client delivery (example) | exactly C.4.15 (the motivating request) |
Templates are stored in UTF-8 without BOM, validate with zero findings (T1.7), and are written by New Specification from Template… byte-for-byte.
{
"handoffSpec": 1,
"id": "studio.client-delivery",
"name": "Client delivery: approved PDFs, source artwork, licensed assets",
"revision": "2026.1",
"description": "Final client handoff with a contents list.",
"variables": {
"client": { "label": "Client code", "required": true, "pattern": "^[A-Z0-9]{2,12}$" },
"project": { "label": "Project code", "required": true, "pattern": "^[A-Za-z0-9-]{2,32}$" },
"revision": { "label": "Delivery revision", "default": "R01", "pattern": "^R\\d{2}$" }
},
"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" },
"limits": { "maxFiles": 2000, "maxTotalBytes": "10 GB", "maxRelativePathLength": 180 },
"buildRecord": "beside"
},
"naming": { "charset": "portable", "allowSpaces": false, "case": "keep", "maxNameLength": 100 },
"content": {
"allowedFormats": ["pdf", "ai", "psd", "svg", "eps", "tiff", "png", "jpeg", "otf", "ttf", "woff2"],
"forbid": ["**/Thumbs.db", "**/.DS_Store", "**/desktop.ini", "**/~$*", "**/*.tmp", "**/__MACOSX/**"],
"hidden": "forbid",
"emptyFiles": "forbid",
"duplicates": "warn",
"unassigned": "info"
},
"rules": [
{
"id": "approved-pdfs",
"title": "Approved PDFs",
"role": "deliverable",
"required": true,
"select": {
"include": ["Approved/**/*.pdf"],
"exclude": ["**/*draft*"],
"latestRevision": { "regex": "_v(\\d+)$" }
},
"formats": ["pdf"],
"pdf": { "pages": { "min": 1 }, "allowPasswordProtected": false },
"approval": { "mode": "evidenceAndConfirm", "evidence": { "pathRegex": "^Approved/" } },
"target": { "folder": "01_Approved_PDFs", "name": "{client}-{project}_{stem}{ext}" }
},
{
"id": "source-artwork",
"title": "Source artwork",
"role": "source",
"required": true,
"select": { "include": ["Artwork/Final/**/*.{ai,psd,svg,eps}"] },
"formats": ["ai", "psd", "svg", "eps"],
"image": { "longEdge": { "min": 2000 } },
"target": { "folder": "02_Source_Artwork", "keepSubfolders": true }
},
{
"id": "licensed-fonts",
"title": "Licensed fonts",
"role": "asset",
"select": { "include": ["Assets/Fonts/**/*.{otf,ttf,woff2}"] },
"formats": ["otf", "ttf", "woff2"],
"license": {
"required": true,
"registerCsv": {
"path": "Assets/licences.csv",
"fileColumn": "File", "licenceColumn": "Licence", "licensorColumn": "Licensor",
"expiresColumn": "Expires", "scopeColumn": "Scope",
"allowedScopes": ["Client delivery", "Unlimited"]
}
},
"target": { "folder": "03_Licensed_Assets/Fonts" }
},
{
"id": "licensed-images",
"title": "Licensed stock images",
"role": "asset",
"select": { "include": ["Assets/Stock/**/*.{jpg,jpeg,tif,tiff,png}"] },
"formats": ["jpeg", "tiff", "png"],
"image": { "longEdge": { "min": 3000 }, "dpi": { "min": 300 }, "forbidGps": true },
"license": {
"required": true,
"sidecar": ["{stem}.licence.pdf", "{stem}.licence.txt"],
"includeEvidence": true,
"evidenceFolder": "03_Licensed_Assets/Licences"
},
"target": { "folder": "03_Licensed_Assets/Images" }
}
]
}Matching register Assets/licences.csv:
File,Licence,Licensor,Expires,Scope
Inter-Regular.otf,OFL-1.1,The Inter Project Authors,,Unlimited- Main thread captures
SessionInput(C.3.3). Staging inside the working root, or the working root inside the staging location, →HO-SES-003(compare normalized full paths with\\?\removed, case-insensitively, on segment boundaries). - The session dialog loads the selected specification on a worker (
LoadSpecification); anyHO-SPECerror disables Scan… and offers Validate. - Variables are validated live; the package folder name is expanded and sanitized; if
<staging>\<name>exists →HO-SES-005. - Stale partial folders in the staging location (C.5.7, step 2 naming, containing a
.handoff-partialmarker) are detected. A folder is live when opening it withDELETEaccess and no sharing fails withERROR_SHARING_VIOLATION(its building session holds a handle withoutFILE_SHARE_DELETE, C.5.7 step 2). For non-live folders,HO-SES-006offers Remove (deletes only after confirmation; links inside are deleted as links, never followed).
- Roots: the working root, or with scope Selected items only, each selected item (files are added directly; directories are enumerated). Relative paths are always computed from the working root, so globs behave identically in both scopes.
- Enumeration: iterative queue;
FindFirstFileExW(…, FindExInfoBasic, …, FIND_FIRST_EX_LARGE_FETCH)on\\?\-prefixed paths; skip.and..; skip.handoffat any depth. - Directory with
FILE_ATTRIBUTE_REPARSE_POINT→ not traversed,HO-SCAN-002. File with reparse tagIO_REPARSE_TAG_SYMLINKor other non-cloud tags → not a candidate,HO-SCAN-003. Cloud placeholders (FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESSorFILE_ATTRIBUTE_OFFLINE, orIO_REPARSE_TAG_CLOUD*) are candidates whose content probes are deferred to build (HO-SCAN-005), so reviewing never hydrates them (consistent witharchitecture.md§5.2.1). - Captured per file: relative path (UTF-16,
/), name, size, attributes, last-write time, reparse tag. File identity (volume serial + file ID) is captured at build when the file is opened. - Enumeration errors (
ERROR_ACCESS_DENIED, etc.) →HO-SCAN-001with the folder and Windows error text; scanning continues. - Limits: 1 000 000 entries, depth 64 →
HO-SCAN-004(scan stops).
- Drop files matching
content.forbid(record for Not included). - For each rule in order: include ∧ ¬exclude ∧
nameRegex∧pathRegex∧ (unclaimed ∨shared). - Apply
latestRevision; record superseded files. - Extract keys; evaluate
expect. - Evaluate
countandrequired(omissions). - Remaining unclaimed files minus evidence/register files → Not included (
HO-SEL-003). - After inspection (C.5.4), evaluate per candidate: format rules (C.4.7), PDF and image constraints (C.4.7.1–C.4.7.2), file size and content policy (C.4.11),
naming.sourceNameRegex(C.4.10), approval and licence evidence (C.4.8–C.4.9). - Reviewer decisions (exclude, approve, override) are applied on top and re-evaluated without rescanning.
Format detection for every candidate; PDF and raster inspection for candidates of those formats (also when the rule has no constraints, so details appear in the review and contents list). Inspections run on the worker sequentially in v1; each is cancellable between files. Cloud placeholders are skipped until build.
Read the first and last 64 KiB. Required: %PDF-x.y within the first 1 024 bytes, startxref and %%EOF within the last 1 024 bytes (allowing trailing whitespace/NULs) → else HO-PDF-001. Version from the header (a /Version in the catalogue of incremental updates is not parsed in v1). /Encrypt in the last 64 KiB → encrypted. /Linearized in the first 1 024 bytes → recorded as linearized.
IPdfPageInspector::Inspect (Windows.Data.Pdf, T0.3) with a 30 s timeout: page count and each page's Size in DIPs converted to millimetres (mm = dip × 25.4 / 96; Size already accounts for CropBox/MediaBox and rotation). A load failure with ERROR_WRONG_PASSWORD/IsPasswordProtected → encrypted. Any other failure or timeout → HO-PDF-030 if the rule has no page constraints, HO-PDF-031 if it has. Page sizes are named when within 2 mm of a named size ("A1 landscape"), otherwise "594.0 × 420.0 mm".
WIC on the worker (MTA): CreateDecoderFromFilename with WICDecodeMetadataCacheOnDemand (no pixel decode), GetFrameCount, frame 0 GetSize, GetResolution, GetPixelFormat → IWICComponentInfo → IWICPixelFormatInfo2 (GetChannelCount, GetBitsPerPixel, SupportsTransparency). Colour model: CMYK pixel formats → cmyk; gray/black-white → gray; indexed → indexed; GUID_WICPixelFormat*Lab* if exposed → lab; else rgb. GPS: IWICMetadataQueryReader::GetMetadataByName for /app1/ifd/gps/{ushort=2} (JPEG), /ifd/gps/{ushort=2} (TIFF), and System.GPS.Latitude (photo metadata policy) — any present value → GPS present. A missing decoder (WINCODEC_ERR_COMPONENTNOTFOUND) → HO-IMG-030 (no constraints) or HO-IMG-031 (constraints present); other decode failures → HO-IMG-001.
Parse the 26-byte header: signature 8BPS, version (1/2), channels (u16 BE @12), height (u32 BE @14), width (u32 BE @18), depth (u16 BE @22), colour mode (u16 BE @24: 1 gray, 2 indexed, 3 rgb, 4 cmyk, 9 lab). Resolution is read from image resource 0x03ED when present (bounded walk of the image-resources section ≤ 1 MiB); otherwise unknown (HO-IMG-032 when a dpi constraint exists). Alpha is "unknown" and treated as satisfying "any" only.
- Expand
package.folderName; sanitize. - For each included candidate (sorted by source relative path, ordinal case-insensitive): expand
target.folder(+{relDir}whenkeepSubfolders) andtarget.name; sanitize; allocate{seq}per target folder. - Add licence evidence files (deduplicated by source path).
- Add outputs at the package root.
- Collision detection on the full relative path after NFC + case folding (
CompareStringOrdinal(…, TRUE)on normalized strings) →HO-NAME-012naming every source involved. - Relative path length →
HO-NAME-013; totals →HO-PKG-001/002. - Free space:
GetDiskFreeSpaceExWon the staging location must be ≥ total bytes × 1.05 + 16 MiB → elseHO-PKG-003.
canBuild⇔ no unresolved error (after severity resolution and permitted overrides) ∧ plan computed ∧ no scan/inspection in progress.needsAcknowledgement⇔ at least one warning ∧policy.requireWarningAcknowledgement.- Build is enabled when
canBuildand (¬needsAcknowledgementor the acknowledgement box is ticked). Any change (re-scan, include/exclude, approval) clears the acknowledgement.
- Re-validate: staging location exists and is a directory;
<staging>\<packageName>does not exist (elseHO-BUILD-001). - Partial folder: create
<staging>\.<packageName>.handoff-<8 hex of session GUID>.partialwithCreateDirectoryW; immediately open it (DELETE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE, shareFILE_SHARE_READ | FILE_SHARE_WRITE— noFILE_SHARE_DELETE— withFILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT), verify it is a directory without the reparse attribute, record its file ID, and retain this handle until publication or cleanup. The retained handle prevents any other process or instance from renaming or deleting the folder and marks the build as live (C.5.1 step 4). SetFILE_ATTRIBUTE_HIDDENthrough the handle. Create the identification marker.handoff-partial(CREATE_NEW) containing{"handoffPartial":1,"session":"<GUID>","pid":<pid>,"startedUtc":"…"}and close it. - Copy each planned file in plan order:
- Create intermediate folders below the partial folder (
CreateDirectoryW; an existing entry that was not created by this session →HO-BUILD-003). - Open the source:
CreateFileW(GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN);GetFileInformationByHandle; size and last-write time must equal the scan snapshot (elseHO-BUILD-002, build stops). Sharing mode blocks writers during the copy. - Create the target with
CREATE_NEW,GENERIC_WRITE, share none,FILE_ATTRIBUTE_NORMAL. - Loop: read 1 MiB → write → SHA-256 update → progress; check the stop event every chunk. Short read/write and errors → retry prompt (Retry/Cancel). Retry closes both handles, deletes the incomplete target (owned), and restarts that file from step 3.2, including the identity check; Cancel stops the build.
- Set last-write time when
preserveModifiedTime;FlushFileBuffers; close; reopen the target for read and re-hash; mismatch →HO-BUILD-004. - Deferred inspections (cloud placeholders, now hydrated) run on the staged copy; resulting errors stop the build (
HO-BUILD-005).
- Create intermediate folders below the partial folder (
- Duplicates: evaluate
content.duplicatesfrom collected hashes. - Outputs: write
CONTENTS.txt, thenmanifest.csv, thenmanifest.json(which records the other two hashes); each withCREATE_NEW, flush, close, re-read verification. - Tree verification: enumerate the partial folder; the set of files must equal plan ∪ outputs ∪ {marker} and the set of folders must equal the planned folders; sizes and recorded hashes must match. Any difference stops the build (
HO-BUILD-004). - Close every handle below the partial folder (the retained folder handle stays open); delete the marker.
- Clear hidden attribute of the partial folder through the retained handle (
SetFileInformationByHandle(FileBasicInfo)). - Publish: open the staging folder (
FILE_LIST_DIRECTORY | SYNCHRONIZE,FILE_FLAG_BACKUP_SEMANTICS); confirm throughGetFileInformationByHandleExthat the retained handle still refers to the directory and file ID recorded in step 2; callRenameRelativePublicationFile(retainedHandle, stagingHandle, packageName)(ReplaceIfExists = FALSE). Sharing violations or access denied (another process holds a handle below the folder) are retried up to 10 times, waiting 500 ms on the stop event between attempts; persistent failure →HO-BUILD-006, partial folder retained (hidden attribute restored) and reported. Name collision →HO-BUILD-001, partial folder retained. Close the retained handle afterwards in every outcome. - After publication: write the build record
<staging>\<packageName>.handoff-build.json(CREATE_NEW; failure →HO-BUILD-007, package remains valid);PostChangeOnPathNotification(staging, FALSE); queueCMD_INTERNAL_FOCUSfor the staging pane if configured and the pane still shows the staging location. - Cancellation or failure before step 9: delete created files and folders in reverse creation order using the session's creation list (never anything else), then delete the partial folder through the retained handle (
FileDispositionInfo); incomplete cleanup →HO-BUILD-008with the retained path. With Keep failed staging folders enabled, keep the partial folder and its marker (re-created if already deleted) and close the handle. Working material is never touched.
stateDiagram-v2
[*] --> Reviewing
Reviewing --> Building: Build
Building --> Verifying: all files copied and re-hashed
Building --> CleaningUp: cancel or error
Verifying --> Publishing: tree equals plan
Verifying --> CleaningUp: mismatch
Publishing --> Published: rename by handle succeeded
Publishing --> Retained: collision or persistent sharing violation
CleaningUp --> NotBuilt
Published --> [*]
Retained --> [*]
NotBuilt --> Reviewing
- Target: the focused directory in the active pane if it contains a manifest; else the pane's current path if it contains one; else Browse… (
IFileOpenDialogwithFOS_PICKFOLDERS). A manifest ismanifest.json, or the single root*.json(≤ 10 checked, ≤ 32 MiB) whose top-level memberhandoffManifestis1. - Manifest: strict schema (C.7.1); invalid →
HO-VER-001and stop. - Specification: the first found of — build record beside the package (
spec.textsnapshot), library folder, recent list — with matchingspec.id; prefer matchingspec.sha256. Different hash →HO-VER-010. None →HO-VER-011(integrity and implicit content rules still run). The user MAY choose another specification with Browse…. - Integrity: every
files[]andoutputs[]entry exists, sizes and SHA-256 match (HO-VER-002/003/005); any other file below the package root →HO-VER-004. When the build record beside the package is found, itspackage.manifestSha256must equal the SHA-256 of the manifest file (HO-VER-007); without a build record the manifest itself cannot be authenticated, which the report states. - Rule re-check (when a specification is available): content policy (forbidden patterns →
HO-VER-006), naming policy on package names, format detection, PDF and image constraints, counts per rule fromfiles[].rule, expected keys fromfiles[].key, licence evidence files present whenincludeEvidence. Approvals cannot be re-derived from a package; they are reported as recorded (HO-VER-012). - Status: Verified (no errors or warnings), Verified with warnings, Failed.
Severity: E error, W warning, I info. 🔒 = cannot be changed by severity or overridden by reviewers. Every code has localized message strings IDS_HO_<CODE> with %s placeholders described in the message column.
| Code | Sev | 🔒 | Message (EN) |
|---|---|---|---|
| HO-SPEC-001 | E | 🔒 | JSON syntax error: %s. |
| HO-SPEC-002 | W | Unknown member "%s"%s. | |
| HO-SPEC-003 | E | 🔒 | Required member "%s" is missing. |
| HO-SPEC-004 | E | 🔒 | "%s" has an invalid type or value: %s. |
| HO-SPEC-005 | E | 🔒 | Specification format version %s is not supported (supported: 1). |
| HO-SPEC-006 | E | 🔒 | Limit exceeded: %s. |
| HO-SPEC-007 | E | 🔒 | Duplicate %s "%s". |
| HO-SPEC-010 | E | 🔒 | Invalid regular expression: %s. |
| HO-SPEC-011 | E | 🔒 | Invalid pattern: %s. |
| HO-SPEC-012 | E | 🔒 | Unknown or unavailable token "{%s}". |
| HO-SPEC-013 | E | 🔒 | Path "%s" must be relative and stay inside its folder. |
| HO-SPEC-014 | E | 🔒 | Unknown or conflicting format "%s". |
| HO-SPEC-015 | E | 🔒 | Output name "%s" is invalid or used twice. |
| HO-SPEC-016 | W | Constraint "%s" does not apply to any format of rule "%s". | |
| HO-SPEC-017 | E | 🔒 | Severity of "%s" cannot be changed. |
| HO-SPEC-018 | E | 🔒 | Unknown page size "%s". |
| HO-SES-001 | E | 🔒 | The working pane must show a folder on a disk or network share. |
| HO-SES-002 | E | 🔒 | The staging pane must show a folder on a disk or network share. |
| HO-SES-003 | E | 🔒 | The staging location and the working folder must not contain each other. |
| HO-SES-004 | E | 🔒 | Variable "%s": %s. |
| HO-SES-005 | E | 🔒 | Package folder "%s" already exists or is not a valid name. |
| HO-SES-006 | W | %d unfinished package folder(s) from interrupted builds were found. | |
| HO-SCAN-001 | W | Folder could not be read: %s (%s). | |
| HO-SCAN-002 | I | Linked folder not scanned: %s. | |
| HO-SCAN-003 | I | Linked file skipped: %s. | |
| HO-SCAN-004 | E | 🔒 | Too many items to scan (limit %s). |
| HO-SCAN-005 | I | Online-only file; it will be downloaded and checked during the build. | |
| HO-SEL-001 | I | Superseded by revision %s. | |
| HO-SEL-002 | I | Also matches rule "%s"; assigned to "%s". | |
| HO-SEL-003 | I | Not included by any rule. | |
| HO-SEL-004 | I | Excluded by reviewer. | |
| HO-SEL-006 | W | Revision could not be read from the name. | |
| HO-SEL-007 | W | Path too long for pattern matching; not matched. | |
| HO-SEL-008 | W | Pattern "%s" was too complex to evaluate for this name; not matched. | |
| HO-REQ-001 | E | Required item missing: %s. | |
| HO-REQ-002 | E | %s: %d selected, at least %d required. | |
| HO-REQ-003 | E | %s: %d selected, at most %d allowed. | |
| HO-REQ-010 | E | Expected item "%s" is missing. | |
| HO-REQ-011 | W | Item "%s" is not in the expected list. | |
| HO-REQ-012 | E | Several files provide item "%s". | |
| HO-REQ-013 | E | Expected-items register cannot be read: %s. | |
| HO-NAME-001 | W | Name does not follow the naming convention. | |
| HO-NAME-010 | I | Renamed for delivery: "%s" → "%s". | |
| HO-NAME-011 | E | 🔒 | Target name is empty or invalid after cleanup. |
| HO-NAME-012 | E | 🔒 | Target "%s" is produced by more than one file. |
| HO-NAME-013 | E | Path in package is %d characters (limit %d). | |
| HO-NAME-014 | E | 🔒 | "%s" is a reserved or invalid Windows name. |
| HO-NAME-015 | W | Name shortened to %d characters. | |
| HO-FMT-001 | E | Format %s is not allowed for "%s". | |
| HO-FMT-002 | E | Content does not match the "%s" extension. | |
| HO-FMT-003 | E | Format %s is not allowed in this package. | |
| HO-FMT-004 | W | Unknown file format. | |
| HO-PDF-001 | E | Not a valid PDF file. | |
| HO-PDF-002 | E | PDF version %s is outside %s. | |
| HO-PDF-010 | E | PDF is password-protected or encrypted. | |
| HO-PDF-020 | E | %d pages; expected %s. | |
| HO-PDF-021 | E | Page %d size %s is not allowed (%s). | |
| HO-PDF-022 | W | Pages have different sizes. | |
| HO-PDF-023 | E | Page %d orientation is %s; expected %s. | |
| HO-PDF-030 | W | Page details could not be read. | |
| HO-PDF-031 | E | Page rules cannot be checked: %s. | |
| HO-IMG-001 | E | Image cannot be read: %s. | |
| HO-IMG-010 | E | Image is %d × %d px; expected %s. | |
| HO-IMG-011 | E | Image size %d × %d px is not one of %s. | |
| HO-IMG-012 | E | Aspect ratio %s; expected %s. | |
| HO-IMG-013 | E | %s edge is %d px; expected %s. | |
| HO-IMG-020 | W | Resolution %s dpi is below %d dpi. | |
| HO-IMG-021 | E | Colour model %s is not allowed (%s). | |
| HO-IMG-022 | E | Transparency requirement not met (%s). | |
| HO-IMG-023 | E | Bit depth %d is below %d. | |
| HO-IMG-024 | E | %d frames; at most %d allowed. | |
| HO-IMG-025 | W | Image contains GPS location data. | |
| HO-IMG-030 | W | No decoder installed for this image format. | |
| HO-IMG-031 | E | Image rules cannot be checked: no decoder installed. | |
| HO-IMG-032 | W | Resolution unknown; dpi rule not checked. | |
| HO-APP-001 | E | Approval evidence not found. | |
| HO-APP-002 | E | Awaiting reviewer approval. | |
| HO-APP-003 | E | Approval register cannot be read: %s. | |
| HO-LIC-001 | E | Licence evidence not found. | |
| HO-LIC-002 | E | Licence expired on %s. | |
| HO-LIC-003 | W | Licence expires on %s. | |
| HO-LIC-004 | E | Licence scope "%s" is not allowed. | |
| HO-LIC-005 | E | Licence register cannot be read: %s. | |
| HO-CONT-001 | I | Forbidden content; never packaged. | |
| HO-CONT-002 | E | Hidden file. | |
| HO-CONT-003 | E | System file. | |
| HO-CONT-004 | E | Empty file. | |
| HO-CONT-005 | E | File size %s is outside %s. | |
| HO-CONT-006 | W | Same content as "%s". | |
| HO-CONT-007 | I | Additional data streams are not copied. | |
| HO-PKG-001 | E | %d files; limit %d. | |
| HO-PKG-002 | E | Total size %s; limit %s. | |
| HO-PKG-003 | E | 🔒 | Not enough free space in the staging location (%s needed). |
| HO-BUILD-001 | E | 🔒 | Package folder "%s" already exists. |
| HO-BUILD-002 | E | 🔒 | Source changed since review: %s. |
| HO-BUILD-003 | E | 🔒 | Cannot copy %s: %s. |
| HO-BUILD-004 | E | 🔒 | Copy verification failed: %s. |
| HO-BUILD-005 | E | 🔒 | Downloaded file failed its checks: %s. |
| HO-BUILD-006 | E | 🔒 | The package could not be published (%s); it was kept at %s. |
| HO-BUILD-007 | W | Build record could not be written: %s. | |
| HO-BUILD-008 | W | Unfinished package folder could not be removed: %s. | |
| HO-BUILD-009 | I | Build cancelled. | |
| HO-VER-001 | E | 🔒 | Manifest missing or invalid: %s. |
| HO-VER-002 | E | 🔒 | Listed file missing. |
| HO-VER-003 | E | 🔒 | File was modified after the build. |
| HO-VER-004 | E | 🔒 | File is not listed in the manifest. |
| HO-VER-005 | E | 🔒 | Contents list or CSV manifest was modified. |
| HO-VER-006 | E | File matches forbidden content "%s". | |
| HO-VER-007 | E | 🔒 | Manifest differs from the one recorded at build time. |
| HO-VER-010 | W | The specification changed since the build. | |
| HO-VER-011 | W | Specification "%s" not found; rule checks skipped. | |
| HO-VER-012 | I | Approval recorded at build time: %s. |
Rule-scoped findings (HO-REQ-*, HO-FMT-001, constraints) honour rule severity; package-wide findings honour top-level severity only.
General: deterministic content for identical inputs (except timestamps and the session GUID); entries sorted by package-relative path (ordinal, case-insensitive); paths use /; timestamps ISO-8601 UTC with Z and second precision; sizes in bytes as integers.
UTF-8 without BOM, LF line endings, two-space indentation, members in the order shown.
{
"handoffManifest": 1,
"generator": { "name": "Open Salamander Delivery Handoff", "version": "1.0.0", "host": "6.0.1234" },
"package": {
"name": "ACME-Rebrand_Delivery_2026-10-03_R01",
"createdUtc": "2026-10-03T14:22:05Z",
"variables": { "client": "ACME", "project": "Rebrand", "revision": "R01", "date": "2026-10-03" }
},
"spec": { "id": "studio.client-delivery", "name": "Client delivery: approved PDFs, source artwork, licensed assets", "revision": "2026.1", "sha256": "…64 hex…" },
"summary": { "status": "verified", "files": 8, "bytes": 123456789, "errors": 0, "warnings": 0, "overrides": 0 },
"outputs": [
{ "path": "CONTENTS.txt", "bytes": 2048, "sha256": "…" },
{ "path": "manifest.csv", "bytes": 1536, "sha256": "…" }
],
"files": [
{
"path": "01_Approved_PDFs/ACME-Rebrand_Brand-Guidelines_v4.pdf",
"bytes": 2345678, "sha256": "…", "modifiedUtc": "2026-09-30T08:01:00Z",
"rule": "approved-pdfs", "role": "deliverable", "format": "pdf", "revision": "4",
"pdf": { "version": "1.7", "pages": 24, "pageSizes": ["A4 portrait"] },
"approval": "evidence+confirmed"
},
{
"path": "03_Licensed_Assets/Images/city-skyline.jpg",
"bytes": 3456789, "sha256": "…", "modifiedUtc": "2026-08-14T10:00:00Z",
"rule": "licensed-images", "role": "asset", "format": "jpeg",
"image": { "width": 4000, "height": 2667, "dpi": [300, 300], "colorModel": "rgb" },
"license": { "evidence": ["03_Licensed_Assets/Licences/city-skyline.licence.pdf"] }
},
{
"path": "03_Licensed_Assets/Licences/city-skyline.licence.pdf",
"bytes": 45678, "sha256": "…", "modifiedUtc": "2026-08-14T10:00:00Z",
"role": "licence-evidence", "format": "pdf", "licenceFor": ["03_Licensed_Assets/Images/city-skyline.jpg"]
}
],
"findings": []
}A non-empty findings array holds objects { "severity": "warning" | "info", "code": "HO-…", "path": "<package-relative path or empty>", "message": "…" }; summary.warnings equals the number of warning entries and summary.status is then "verifiedWithWarnings".
| Member | Rules |
|---|---|
summary.status |
"verified" or "verifiedWithWarnings" (a package is never published with unresolved errors). |
outputs |
Every enabled output except manifest.json itself. |
files[].key, revision, pdf, image, approval, license, licenceFor, source |
Present only when applicable. approval is "evidence", "confirmed", or "evidence+confirmed". license has licence, licensor, expires, evidence when known. source only with includeSourcePaths. |
findings |
Warnings and infos remaining at build (excluding HO-SEL-003, HO-SEL-001, HO-SCAN-*). Messages are in the UI language at build time. |
UTF-8 with BOM (spreadsheet detection), CRLF, RFC 4180 quoting. Columns: Path, Bytes, SHA-256, Modified (UTC), Rule, Role, Format, Key, Revision, Pages, Page sizes, Width (px), Height (px), DPI, Colour model, Approval, Licence, Licensor, Licence expires. Multi-valued cells are joined with ; . Formula-injection guard: a cell beginning with =, +, -, @, TAB, or CR is prefixed with '. Header labels are English (stable for tooling).
UTF-8 with BOM, CRLF, monospace layout; labels from the plug-in language module.
ACME Rebrand — delivery contents
Package: ACME-Rebrand_Delivery_2026-10-03_R01
Date: 2026-10-03
Specification: Client delivery: approved PDFs, source artwork, licensed assets (2026.1)
Files: 8 (117.7 MB)
01_Approved_PDFs/
ACME-Rebrand_Brand-Guidelines_v4.pdf 2.2 MB PDF, 24 pages, A4 Approved PDFs
ACME-Rebrand_Stationery_v2.pdf 1.1 MB PDF, 3 pages, A4 Approved PDFs
02_Source_Artwork/Campaign/
Hero.psd 88.0 MB PSD, 6000 × 4000 px Source artwork
…
03_Licensed_Assets/Fonts/
Inter-Regular.otf 310 KB OpenType font, OFL-1.1 Licensed fonts
SHA-256 checksums for every file are listed in manifest.csv and manifest.json.
Sizes are formatted by the engine itself (it does not call the host) with binary multiples and one decimal (310 KB, 2.2 MB), consistent with how Open Salamander displays sizes. With groupBy: "rule" sections are rule titles instead of folders. Variables are written only when they are used in contents.title/notes or the package name.
<staging>\<packageName>.handoff-build.json, UTF-8 without BOM. Internal; documented as not for the client.
{
"handoffBuildRecord": 1,
"package": { "name": "…", "path": "D:\\Deliveries\\…", "manifestSha256": "…" },
"session": { "id": "<GUID>", "user": "DOMAIN\\name", "machine": "WS-042", "startedUtc": "…", "finishedUtc": "…", "host": "6.0.1234", "plugin": "1.0.0" },
"workingRoot": "D:\\Projects\\ACME-Rebrand",
"scope": "all",
"spec": { "path": "D:\\Projects\\ACME-Rebrand\\.handoff\\client-delivery.handoff.json", "sha256": "…", "text": "<complete specification text>" },
"decisions": [ { "action": "approve", "source": "Approved/Stationery_v2.pdf", "user": "DOMAIN\\name", "utc": "…" },
{ "action": "exclude", "source": "…", "user": "…", "utc": "…" },
{ "action": "override", "code": "HO-…", "source": "…", "reason": "…", "user": "…", "utc": "…" } ],
"files": [ { "source": "Approved/Brand-Guidelines_v4.pdf", "target": "01_Approved_PDFs/ACME-Rebrand_Brand-Guidelines_v4.pdf", "sha256": "…" } ],
"findings": [ … all findings including info … ]
}- Dialog templates live in the language modules (
lang/lang.rc,lang/pl/lang_pl.rc); command/RCDATA IDs inhandoff.rh; dialog and control IDs inlang/lang.rh. IDs below are names; numeric values are assigned once inlang.rh(dialogsIDD_HO_*from 100, controlsIDC_HO_*from 1000, stringsIDS_*from 2000, finding stringsIDS_HO_*from 3000) and then frozen — UI tests depend on them. - All windows are keyboard-operable (mnemonics, tab order, Enter/Esc, F1 help via
SetupWinLibHelp); list views useLVS_REPORT, full-row select, header labels, andLVS_EX_CHECKBOXESwhere noted; severity is text plus a system icon (never colour alone); system colours only. - Static labels precede their controls in tab order so UIA names are derived correctly.
- Long work runs on workers; windows stay responsive; progress updates are throttled to ≤ 10 per second.
- Positions persist (
Review Placement,Verify Placement) and are restored only when visible on a current monitor (MultiMonEnsureRectVisible-equivalent check on the UI thread). - Window titles: "Build Delivery Package", "Review Delivery Package — ", "Verify Delivery Package — ".
IDD_HO_SESSION, modeless, fixed size.
┌ Build Delivery Package ─────────────────────────────────────────────┐
│ Working folder: D:\Projects\ACME-Rebrand [Swap] │
│ Staging folder: D:\Deliveries │
│ Scope: (•) Entire working folder ( ) Selected items only (3) │
│ Specification: [client-delivery.handoff.json (.handoff) ▾] [Browse…] │
│ [Validate] [Show in panel] │
│ Client delivery: approved PDFs, source artwork, licensed assets │
│ 4 rules · revision 2026.1 │
│ Variables: ┌Variable──────────┬Value──────┬Required┐ │
│ │Client code │ACME │yes │ │
│ │Project code │Rebrand │yes │ │
│ │Delivery revision │R01 │ │ │
│ │Date │2026-10-03 │ │ │
│ └──────────────────┴───────────┴────────┘ │
│ Value: [ACME ] │
│ Package folder: ACME-Rebrand_Delivery_2026-10-03_R01 ✓ available │
│ [Scan…] [Cancel] [Help] │
└──────────────────────────────────────────────────────────────────────┘
| Control | ID | Behaviour |
|---|---|---|
| Working path | IDC_HO_WORKING_PATH |
Read-only edit. |
| Staging path | IDC_HO_STAGING_PATH |
Read-only edit. |
| Swap | IDC_HO_SWAP |
Swaps working/staging (and the physical panes used for focus); re-validates. |
| Scope radios | IDC_HO_SCOPE_ALL, IDC_HO_SCOPE_SELECTION |
Selection radio disabled when nothing was selected; shows the count. |
| Specification | IDC_HO_SPEC_COMBO |
Drop-down list in the discovery order of A.5.3; display "file name (origin)". |
| Browse | IDC_HO_SPEC_BROWSE |
IFileOpenDialog, filter *.handoff.json. |
| Validate | IDC_HO_SPEC_VALIDATE |
Opens IDD_HO_VALIDATE for the selected file. |
| Show in panel | IDC_HO_SPEC_SHOW |
Focuses the spec file in the working pane (CMD_INTERNAL_FOCUS). |
| Summary | IDC_HO_SPEC_SUMMARY |
Name, rule count, revision, description, or the first validation error. |
| Variables | IDC_HO_VARIABLES |
List view: Variable, Value, Required. |
| Value | IDC_HO_VARIABLE_VALUE, IDC_HO_VARIABLE_CHOICE |
Edit for free values, drop-down for choices; bound to the selected row; validated live. |
| Package name | IDC_HO_PACKAGE_NAME, IDC_HO_PACKAGE_STATUS |
Live preview and status text (available / exists / invalid). |
| Scan | IDOK |
Enabled when spec valid, variables valid, name available; opens the review window and closes this dialog. |
IDD_HO_REVIEW, modeless, resizable (min 800 × 560 DIP at 96 dpi).
| Control | ID | Behaviour |
|---|---|---|
| Status banner | IDC_HO_STATUS_BANNER |
"Scanning… 12 345 items", "2 errors, 3 warnings", "Ready to build", "Building… 3 of 8". |
| Rules | IDC_HO_RULES |
Columns: Rule, Role, Required, Selected, Expected, Status. First row "All rules". |
| Filter | IDC_HO_FILTER |
All items, Problems only, Omissions, Not included, Superseded. |
| Items | IDC_HO_ITEMS |
Check-box list: Include, Source, Target, Size, Details, Approval, Licence, Status. Space toggles include; Enter = Show in panel. Not-included/superseded rows have no check box (state image index 0). |
| Findings | IDC_HO_FINDINGS |
Severity, Code, Item, Message; double-click selects the item and focuses it in the working pane. |
| Approve | IDC_HO_APPROVE |
Approves selected items of rules with confirm. |
| Approve all in rule | IDC_HO_APPROVE_ALL |
Approves all included items of the selected rule. |
| Show in panel | IDC_HO_SHOW_IN_PANEL |
Focuses the source in the working pane. |
| Override | IDC_HO_OVERRIDE |
Visible only with policy.allowErrorOverride; enabled for an overridable error. |
| Re-scan | IDC_HO_RESCAN |
Rescans and re-inspects; keeps decisions for unchanged files (same path, size, time). |
| Save report | IDC_HO_SAVE_REPORT |
Text report of rules, items, and findings. |
| Acknowledge | IDC_HO_ACK_WARNINGS |
"I have reviewed all warnings"; visible when needed; cleared on change. |
| Progress | IDC_HO_PROGRESS_TOTAL, IDC_HO_PROGRESS_FILE, IDC_HO_PROGRESS_TEXT |
Visible during scan/inspection/build (CSalamanderGUIAbstract::AttachProgressBar). |
| Build | IDOK |
Gated by C.5.6; during a build it is disabled. |
| Close / Cancel | IDCANCEL |
Closes; during scan or build it cancels after confirmation. |
IDD_HO_RESULT, modal to the review window. Controls: IDC_HO_RESULT_ICON, IDC_HO_RESULT_TEXT ("Verified — ready for review", "Verified with warnings", "Not built"), IDC_HO_RESULT_PATH, IDC_HO_RESULT_TOTALS, IDC_HO_FINDINGS, IDC_HO_FOCUS_PACKAGE ("Show package"), IDC_HO_SAVE_REPORT, IDOK ("Close"). Closing after success also closes the review window.
IDD_HO_VERIFY, modeless, resizable. Controls: IDC_HO_VERIFY_PATH, IDC_HO_VERIFY_SPEC (static with origin and hash state), IDC_HO_VERIFY_SPEC_BROWSE, IDC_HO_STATUS_BANNER, IDC_HO_PROGRESS_TOTAL, IDC_HO_PROGRESS_TEXT, IDC_HO_FINDINGS, IDC_HO_SHOW_IN_PANEL (focuses the package file in the pane that shows the package), IDC_HO_SAVE_REPORT, IDC_HO_VERIFY_AGAIN, IDCANCEL ("Close").
IDD_HO_NEWSPEC, modal. Controls: IDC_HO_TEMPLATES (list box of C.4.14 titles), IDC_HO_TEMPLATE_DESC, IDC_HO_NEWSPEC_PATH (default <current folder>\.handoff\<template file name>), IDC_HO_NEWSPEC_BROWSE, IDOK ("Create"), IDCANCEL, IDHELP. Creates .handoff if needed; writes with CREATE_NEW; an existing file → message and no write; on success focuses the new file in the working pane.
IDD_HO_VALIDATE, modal. Controls: IDC_HO_VALIDATE_PATH, IDC_HO_VALIDATE_BROWSE, IDC_HO_FINDINGS (Line, Column, Code, Message), IDC_HO_VALIDATE_AGAIN, IDC_HO_COPY (copy findings as text), IDOK ("Close").
IDD_HO_OVERRIDE, modal. Controls: IDC_HO_OVERRIDE_FINDING (static: code, item, message), IDC_HO_OVERRIDE_REASON (multi-line edit, ≥ 10 characters to enable OK), IDOK, IDCANCEL.
IDD_HO_CONFIG, modal (from Plugins Manager). Controls: IDC_HO_CFG_LIBRARY, IDC_HO_CFG_LIBRARY_BROWSE, IDC_HO_CFG_SHOW_UNASSIGNED, IDC_HO_CFG_FOCUS_PACKAGE, IDC_HO_CFG_KEEP_PARTIAL, IDC_HO_CFG_CLEAR_RECENT, IDOK, IDCANCEL, IDHELP. Uses CDialog::Validate/Transfer (architecture.md §3.1).
Stored in the plug-in's private registry key through LoadConfiguration/SaveConfiguration (CSalamanderRegistryAbstract). regKey == NULL loads defaults. Writes happen only in SaveConfiguration (the host schedules saves; the plug-in never writes the registry directly).
Paths are stored as REG_BINARY holding UTF-8 bytes without a terminator and read with GetSize + GetValue. REG_SZ is not used for paths because the host wrapper writes it through the ANSI RegSetValueEx (src/regwork.cpp:530-536), which would lose characters outside the active code page. A value that is not valid UTF-8 is ignored (default used).
| Value | Type | Default | Notes |
|---|---|---|---|
Version |
DWORD | 1 | CURRENT_CONFIG_VERSION. |
Spec Library |
REG_BINARY (UTF-8) | absent | Absolute folder path. |
Recent Spec 1 … Recent Spec 10 |
REG_BINARY (UTF-8) | absent | Most recent first; missing files are pruned when the session dialog loads. |
Show Unassigned |
DWORD | 1 | |
Focus New Package |
DWORD | 1 | |
Keep Failed Staging |
DWORD | 0 | |
Review Placement, Verify Placement |
REG_BINARY | absent | WINDOWPLACEMENT. |
Review Columns, Verify Columns |
REG_BINARY | absent | Column widths (DIP). |
The plug-in creates no files at startup or load; nothing is written outside folders the user selected, the staging location, or (for New Specification) the chosen path — required by the UI-test sandbox (testing.md, Native UI tests).
- Specifications, registers, and manifests are untrusted (they are shared between people and organizations). Parsers are bounded (C.4.1), non-recursive beyond the depth limit, and fuzzed (T8.2).
- Specifications cannot execute anything, access the network, expand environment variables, or reference absolute paths. Register paths resolve against the working root; target paths against the package root. After
GetFullPathNameW, the result must remain below its root on a segment boundary, elseHO-SPEC-013. - Regular expressions are length-bounded, compiled once, and every
std::regex_erroris converted to a finding. - The plug-in never follows directory links during scanning or deletion, never writes through an existing entry (every file it creates — package content and outputs inside the private partial folder, the build record, a new specification — uses
CREATE_NEW, and folders useCreateDirectoryW), and publishes by handle with no replacement.
- Client-facing outputs contain no absolute paths, user names, or machine names by default (
includeSourcePathsadds only working-root-relative paths). - The build record contains internal paths, user and machine names, and the full specification; it is written beside the package so the user decides whether it travels. Help documents this.
- Images with GPS metadata are flagged when
forbidGpsis set; the plug-in does not strip metadata (non-goal). - No telemetry and no network access.
- Working material is opened read-only with
FILE_SHARE_READduring copy; nothing in the working root is created, modified, or deleted. - A published package is complete and verified or does not exist under its final name.
- Process termination during a build leaves only a hidden, marked partial folder that the next session offers to remove.
- Every handle, COM object, and WinRT object is released on all paths (RAII wrappers;
wilis available insrc/common/dep/wiland MAY be used). - Debug builds use
CALL_STACK_MESSAGE*andTRACE_*in host-facing code (not in the engine).
| Operation | Budget |
|---|---|
| Scan of 100 000 entries on local NVMe | < 10 s; UI responsive; cancel honoured within 250 ms |
| Format detection | ≤ 4 KiB read per candidate |
| PDF structure | ≤ 128 KiB read per PDF |
| PDF pages | ≤ 30 s timeout per file; typical < 200 ms |
| Raster inspection | header/metadata only; typical < 20 ms per image |
| Copy | ≥ 80 % of CopyFile2 throughput on the same volume pair (1 MiB buffers), plus one verification read of every target |
| Memory | ≤ 2 KiB per candidate plus findings; no file content kept in memory beyond buffers |
tests/HandoffEngineTests/HandoffEngineTests.exe (Debug x64, built by runtests.ps1). Groups and minimum cases:
| Group | Cases |
|---|---|
| JSON | valid documents; every syntax error with line/column; BOM; invalid UTF-8; surrogates; duplicate keys; each limit at limit−1/limit/limit+1; deep nesting does not overflow |
| Spec | every HO-SPEC-*; defaults; $comment; did-you-mean; templates validate cleanly |
| Glob/regex | C.4.6 tables; static prefix; brace expansion; full-match versus search members; catastrophic regex bounded (HO-SEL-008); long subjects (HO-SEL-007) |
| Templates/naming | tokens; {seq:000}; transliteration (Polish, German, French, Czech, Nordic); CJK in unicode mode; reserved names; truncation; collisions (case and NFC) |
| Scanner | junction cycle; junction outside root; directory symlink (when privilege available, otherwise explicit skip); file symlink; access-denied folder; .handoff excluded; limits; cancellation |
| Selection | first-match ownership; shared; latest revision numeric/letters/mixed/no-match/prefixOrder (P07 < C01); expect inline/register/where/unexpected/ambiguous; counts; not-included excludes evidence and registers |
| Formats | every signature positive and negative; custom formats; extension/content mismatch |
generated PDFs: valid 1/3 pages, A4/A3/A1, landscape, mixed sizes, encrypted (/Encrypt + password), truncated, no %%EOF; WinRT unavailable path via test double |
|
| Images | WIC-encoded PNG/JPEG/TIFF/GIF at given sizes and dpi; CMYK TIFF; gray PNG; alpha PNG; multi-frame TIFF/GIF; JPEG with GPS EXIF; PSD/PSB synthetic headers with and without resolution resource |
| Evidence | approval path/name/sidecar/register; licence sidecar/register/expired/expiring/scope/bad date; evidence copy dedup |
| Planner | output collisions; path length; limits; free-space shortfall (test double) |
| Build | happy path byte-exact; timestamps; fault injection at each I/O call (create dir, open source, identity mismatch, create target, read, write, flush, close, reopen, re-hash mismatch, outputs, marker, attribute clear, rename sharing violation ×N then success, rename collision); Retry restarts a file from scratch; cancellation at each phase; verify no final folder and only owned deletions; stale-partial detection for a live folder (retained handle) and a dead one; a second instance cannot delete or rename a live partial folder |
| Outputs | golden manifest.json, manifest.csv (formula guard, quoting, BOM), CONTENTS.txt (both groupings) |
| Verify | missing, modified (same size different bytes), unlisted, outputs modified, forbidden content (HO-VER-006), manifest edited versus build record (HO-VER-007), spec changed, spec missing, manifest invalid; fresh package verifies clean |
tools/handoff_spec_parser_probe.cpp: compilesjson_reader.cpp,spec_model.cpp,glob.cpp,regex_guard.cpp,units.cpp,name_template.cppwithcl.exe /std:c++latest /EHsc /W4 /WX /sdl /guard:cf; reads each corpus file, compares produced codes/locations with<file>.expected(oneCODE line:columnper line), and with-Iterations Napplies deterministic seeded mutations (byte flip, truncation, duplication, deep nesting insertion) asserting no crash and < 1 s per input.tests/handoff-specs/valid/(templates + edge cases),invalid/(one perHO-SPEC-*),hostile/(huge strings, deep nesting, regex bombs, BOM/UTF-16 confusion, NULs).tools/test-handoff-spec-parser.ps1 [-Architecture x64|x86] [-Iterations N]followstools/test-bzip2-compatibility.ps1: it locatesVsDevCmd.batitself throughvswhere(astools/test-zlib-compatibility.ps1:12-25), builds in a temporary directory, runs, deletes its outputs, and exits non-zero on any mismatch. This letsruntests.ps1call it from its Windows PowerShell probe loop and the hosted nightly workflow call it with-Iterations 250.
tests/FileManager.UiTests/HandoffUiTests.cs, [Category("UI")], fixtures below the guarded filemanager-testdata root, panes set with -l/-r, commands issued through WaitForPluginCommand("handoff.spl", <id>):
| Test | Asserts |
|---|---|
Build_creates_verified_package_with_manifest_and_contents |
Example spec in .handoff; variables entered; approve all; build; package tree equals expected; manifest hashes equal file hashes; CSV/TXT present; build record beside; no partial folder; staging pane focused on package |
Missing_required_item_blocks_build_and_lists_omission |
Remove required file; HO-REQ-001 listed; Build disabled; no folders created in staging |
Image_below_minimum_is_reported |
Generated 1 800 px PSD/PNG; HO-IMG-013 shown |
Superseded_revision_is_excluded |
_v3/_v4; only v4 staged |
Existing_package_name_is_refused |
Pre-create folder; HO-SES-005; Scan disabled |
Cancel_during_build_leaves_no_package |
Large generated file; cancel; no final and no partial folder; working files unchanged (hash) |
Verify_detects_modified_and_unlisted_files |
Build, then modify one file and add one; Verify lists HO-VER-003 and HO-VER-004 |
Invalid_specification_reports_line_and_column |
Validate dialog shows HO-SPEC-001 with location |
New_specification_from_template_never_overwrites |
Create twice; second refused; content equals template |
Unicode_and_long_paths_are_staged |
Polish/CJK names and > 260-character working paths; portable transliteration in targets |
Control lookups use the frozen IDs with comments naming their lang.rh symbols. Tests must not depend on localized text except where they assert localized messages, and must clean up only below their GUID fixture folder.
- Quick-start A.5.1 end to end (English UI), then repeat in Polish UI.
- 100 %, 150 %, 200 % DPI on one and two monitors; move the review window between monitors.
- Working folder on OneDrive with online-only files: review shows
HO-SCAN-005, no hydration during review, build hydrates and verifies. - Staging on an SMB share and on a FAT32/exFAT USB drive (no ACL/ADS): build and verify succeed;
HO-CONT-007appears for sources with streams. - Explorer window open inside the partial folder during publication → retry then
HO-BUILD-006; next session offers cleanup. - Terminate
salamand.exeduring a build (Task Manager) → restart → stale partial cleanup offered and works. - Unload the plug-in from Plugins Manager during review and during a build.
- Keyboard-only run through every dialog; Narrator reads list headers and statuses.
| Check | runtests.ps1 |
pr-msbuild.yml |
build-installer.yml |
nightly-parser-fuzz.yml |
|---|---|---|---|---|
| Plug-in build (Debug Win32/x64) | via solution build | yes (matrix) | via runtests.ps1 |
— |
| Plug-in build (Release x64) | release mode | — | Build Installer job | — |
HandoffEngineTests |
yes | — (as PictViewEngineTests) |
via runtests.ps1 |
— |
test-handoff-spec-parser.ps1 x64/x86 |
yes | x64 | via runtests.ps1 |
x64, 250 iterations |
HandoffUiTests, HandoffSourceContractTests |
NUnit project | — | via runtests.ps1 |
— |
| Language parity, icon coverage | existing checks | existing steps | via runtests.ps1 |
— |
| ID | Item | Mitigation / status |
|---|---|---|
| R-01 | Windows.Data.Pdf behaviour in an in-process desktop plug-in (first WinRT use in the repository), Win32 build, and on Windows 10 1809 |
Spike T0.3; isolated translation unit; fallback to structural checks with HO-PDF-030/031. Outcome: to be recorded. |
| R-02 | WIC codec availability (HEIC/WebP/AVIF) varies by machine | HO-IMG-030/031; documented. |
| R-03 | Directory rename blocked by open handles (indexer, antivirus, Explorer preview) | Bounded retry; partial folder retained; cleanup flow. |
| R-04 | std::wregex performance and stack use on hostile patterns |
Length limits, compile-time validation, exception capture, hostile corpus; replace with a linear-time engine if T8.2 shows unbounded cases. |
| R-05 | Debug base-address slot size for a WinRT-using module | T0.1 dumpbin check. |
| R-06 | CONTENTS.txt labels follow the producer's UI language, not the client's |
v1 limitation; future contents.language. |
| R-07 | No translations beyond English and Polish | Fallback to English; add translations/*/handoff.slt later. |
| O-01 | Future: GUI specification editor (D-05) | Deferred. |
| O-02 | Future: package as ZIP with manifest, transmittal PDF, e-mail/upload | Deferred. |
| O-03 | Future: incremental re-staging (_R02 from _R01 diff) and a virtual "package view" file system |
Deferred. |
| O-04 | Future: PDF content checks (annotations, layers, fonts), PDF/A/X | Deferred; requires a PDF library decision. |
Spike outcomes:
- T0.3 Windows.Data.Pdf: works in-process from multithreaded-apartment workers through
SHCreateStreamOnFileEx→CreateRandomAccessStreamOverStream→LoadFromStreamAsyncwith a 30 s wait. The plug-in linksruntimeobject.lib(notWindowsApp.lib, which would re-route Win32 imports) together withshcore.libandoleaut32.lib(required by the C++/WinRT error helpers). Generated PDFs report page counts and sizes within 0.5 mm (engine tests). - T0.4 WIC: header and metadata inspection works on MTA workers; an absent codec yields
HO-IMG-030/031. PSD/PSB headers are parsed directly. - Base address (R-05): the Debug x64 image is 5.5 MB, so the planned
0x21b00000slot overlappedfolders;handoffnow uses0x000001002a800000(x64) and0x2a800000(x86).
Deviations from this specification:
| Area | Implemented behaviour | Reason |
|---|---|---|
| Module layout (C.2.2) | Engine units are consolidated: json, spec (model + validator), patterns (glob + regex guard), name_template, formats, csv, scanner, review (assembler, evidence, findings evaluation), plan, file_system (Win32 file system + CNG SHA-256), build (stager, publisher, build record), outputs, verify (manifest reader + verifier), inspect.h with pdf_structure, pdf_pages_winrt, image_probe, text_util (units, dates, paths). UI files follow C.2.2 plus wdialog, spec_source, build_session.h, handoff_ui.h; configuration lives in config.{h,cpp}. |
Fewer translation units with the same responsibilities. |
| Engine interfaces (C.2.3) | IProgress (report + stop) and IBuildCallbacks (adds AskRetry); no IHandoffClock — the session's local time is captured once and passed in BuildInput. |
Deterministic tests and stable names across midnight. |
| Regex bounds (T1.4) | The VS 2026 STL bounds backtracking (error_complexity/error_stack) and has no _REGEX_MAX_* macros; they are defined only on the parser-probe command line for older hosted toolsets. |
Project-wide definitions are inert with v145. |
| JSON limits (C.4.1) | An extra MaxValues limit (65 536 specification / 2 000 000 manifest values) bounds memory instead of the "4× input size" criterion. |
A value count is enforceable while parsing. |
| Templates (C.4.10) | {seq} is rejected in folder templates. |
Folder-level numbering would be ambiguous. |
| CSV registers (T2.7) | ; is auto-detected as the delimiter when the header has no comma. |
European spreadsheet exports. |
| Scanner (C.5.2) | Directories with cloud reparse tags are traversed; other directory reparse points are skipped (HO-SCAN-002). |
OneDrive folders are ordinary content. |
| Manifest (C.7.1) | Manifest findings exclude HO-SEL-002/004 and use package-relative paths. |
Client-facing output must not expose working paths. |
| Verification (C.5.8) | Package names are checked with HO-NAME-001; HO-PDF-001 applies to every PDF; register-based expectations are not re-checked (registers stay in the working folder). |
Data available inside a package. |
| Localized text (T7.2) | Messages use positional {0}…{9} placeholders rendered by the engine, not printf formats. |
Word order in Polish; no format-string risk. |
| Dialogs (C.8.1, C.8.9) | A Unicode W dialog base (ui/wdialog) replaces WinLib CDialog, including the configuration dialog. |
WinLib dialogs are ANSI and lose characters outside the code page. |
| Progress (C.8.3) | Standard progress controls in the review window instead of AttachProgressBar. |
Works in the Unicode dialog base. |
| Review items (C.8.3) | The include check box sits on the Source column; omissions (missing items, empty required rules) appear as Missing rows; Enter on a list shows the item in the panel instead of building. | Accessible names and keyboard safety. |
| Spec discovery (C.5.1) | Discovery and loading of specifications run on the plug-in UI thread (bounded 1 MiB reads), not on a worker. | Small bounded I/O; the main window stays responsive. |
| UI-test bootstrap | The UI-test sandbox pre-assigns plug-in menu IDs for ftp.spl and handoff.spl in one root pass (CPlugins::InitUiTestPluginMenuItems takes a list). |
A second root pass would invalidate FTP's published IDs. |
| Icon (T7.5) | Fluent box glyph. |
box_checkmark is not vendored. |
| Resources | Templates are RCDATA in handoff.rc2. |
Matches the other hand-edited resources. |
Open items found during implementation:
HO-SES-004reasons (for example "a value is required") are English fragments inside an otherwise localized message.- Not verified on this machine: Windows 10, the Polish UI walk-through, 150 %/200 % DPI, unloading from Plugins Manager during review/build, menu enablement for archive and plug-in file-system panes, a VS 2022 probe build, the 100 000-entry scan budget, and CHM compilation.
Repository (baseline 165a2c9):
AGENTS.md— build environment, CI parity, regression-run and comment rules.README.md— product overview, building, plug-in list, update detection.architecture.md— §2 lifecycle, §3.1 WinLib, §5.2.1 reparse-point policy, §5.3 plug-in host and SDK, §7 concurrency and lock ordering, §11.4 adding a plug-in capability.testing.md— runner, UI lanes, catalog, pull-request ratchets.src/plugins/shared/spl_base.h(:211-233menu events,:298-320AddMenuItem,:430-438FUNCTION_*,:472-CPluginInterfaceAbstract),spl_menu.h,spl_gen.h(:246-254,:1290-1356,:1578-1584,:1868-1882,:1940-1949,:2321),spl_com.h(:215-245CFileData,:848-897operations),spl_vers.h(:129-),auxtools.h,plugin_thread_owner.h,winliblt.h/.cpp(CDialog::Execute/Create,:429-442).src/plugins/checksum/— entry, connect, configuration, dialog thread, focus command, UTF-8 file helpers (precomp.h,misc.cpp).src/plugins/demomenu/— minimal menu-extension project layout.src/plugins/ftp/precomp.h:23-25,vcxproj/ftp.props:8,vcxproj/ftp.vcxproj:105— reuse ofsrc/commonheaders and C++ standard setting.src/common/relative_file_operations.h— handle-bound open/rename without replacement.src/common/conditional_file_publication.h— repository pattern for durable publication (reference).src/svg.cpp:200-232— plug-in SVG icon mapping.src/plugins_loading.cpp:3706-3709,src/path_checking.cpp:2372-2409— UI-test plug-in command map.src/plugins/shared/baseaddr_x64.txt,baseaddr_x86.txt,vcxproj/plugin_*.props,vcxproj/lang_*.props.tests/PictViewEngineTests/— native plug-in engine test pattern.tests/FileManager.UiTests/Infrastructure/UiTestSettings.cs:43-44,NativeCommands.cs,NativeSafetyRegressionTests.cs:16-.scripts/runtests.ps1:444-475,:889-893;.github/workflows/pr-msbuild.yml,build-installer.yml:100-105,nightly-parser-fuzz.yml.tools/prepare_installer.ps1:118-165,Installer/setup.iss:38,:91.tools/verify-language-resource-parity.ps1,tools/verify-fluent-icon-coverage.ps1,tools/verify-no-new-*.ps1,tools/max-path-buffer-exemptions.md,tools/test-unsafe-api-baseline.ps1.help/src/compileall.bat,help/src/salamand.hhc:914-918,src/plugins/checksum/help/.
External (Microsoft Learn and standards):
- RFC 8259 (JSON), RFC 4180 (CSV), RFC 2119 (requirement keywords), ISO 216 and ANSI/ASME Y14.1 (paper and architectural sheet sizes).
Windows.Data.Pdf—PdfDocument.LoadFromStreamAsync,PdfDocument.PageCount,PdfPage.Size.- C++/WinRT — consuming APIs from desktop apps;
winrt::init_apartment;IAsyncOperation::wait_for. CreateRandomAccessStreamOverStream(shcore.h),SHCreateStreamOnFileEx.- Windows Imaging Component —
IWICImagingFactory::CreateDecoderFromFilename,IWICBitmapFrameDecode::GetResolution,IWICPixelFormatInfo2, Photo Metadata Policies (System.GPS.Latitude), metadata query language. - CNG —
BCryptOpenAlgorithmProvider,BCryptCreateHashwithBCRYPT_HASH_REUSABLE_FLAG. - File management —
FindFirstFileExW(FIND_FIRST_EX_LARGE_FETCH),FILE_RENAME_INFO,SetFileInformationByHandle,FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS, maximum path length limitation (\\?\). NormalizeString,CompareStringOrdinal,GetUserNameExW.IFileOpenDialog/IFileSaveDialog(FOS_PICKFOLDERS).