Wakigaki architecture
This document owns Wakigaki-specific system boundaries and durable decisions. The shared sovereign-file machinery is documented by the platform; its security contract is in platform security. SECURITY.md records only Wakigaki's security additions, while ROADMAP.md and DEBT.md own future work.
Core boundaries
- The validated structured Wakigaki document is canonical. Editable DOM, browser undo, controls, measurements, page sheets, and print furniture are projections.
- Save clones the captured trusted shell and replaces only the validated document island, exact verified material, attachment blocks, and public title. It never serializes live interface state. The write, the blocking operation surface, password encryption, and browser recovery are the platform's controllers; Wakigaki supplies the document as the platform's
Session(src/document-session.ts) and presents each in its own words. - Open internalizes only a contributing file's document and attachments. Its shell, runtime, interface, and capability material never enter the receiving application.
- Save, Open/import, document replacement, encryption, recovery, attachments, interchange, and updates are serialized whole-document operations. One operation surface blocks editing, reports its phase, and permits cancellation until commit becomes irreversible.
- A saved file remains independently usable without an account, server, CDN, package registry, or runtime feature download.
- New boundaries use strict TypeScript. Legacy JavaScript migrates when its responsibility is substantially rewritten.
The shipped file
Wakigaki.html contains a fixed startup/failure surface and bounded inert blocks for:
- the compressed or authenticated-encrypted canonical document;
- the compressed shell containing the interface, Help, styles, and notices;
- the compressed browser runtime;
- build-owned capability and language records; and
- individually addressable attachment payloads.
The bootloader verifies canonical Base64, hashes, declared compressed and expanded bounds, and fatal UTF-8 before importing the runtime from a temporary Blob URL. The document island is the platform's canonical island: the validated Wakigaki document travels in its domain region, its ledger names the attachment payloads the file carries, and its identity is the document's — minted at the first Save, carried unchanged by every save, copy, and encryption, and the key recovery and versions are filed under (src/document-island.ts; the platform's DN-DocumentIdentity.md owns the rule). The shipped seed carries no identity and is recognised by that, never by its filename. A file written before the island existed carries the document envelope directly; the reader still opens it, under the docId it already had, and test/fixtures/envelope-v1/ proves it. The attachment manifest is inside canonical document data, while its payload blocks remain outside so Open and startup need not inflate them. Save writes exactly the referenced set in stable order.
Runtime ownership
| Boundary | Owns |
|---|---|
| Model | Schema, normalization, validation, stable identities, assets, comments, fragments, and checkpoints |
| Editor | ProseMirror transactions, application history, selection commands, input, lists, tables, and clipboard adaptation |
| Layout | Page settings, ruler, measurement, pagination plan, and disposable screen/print projection |
| Persistence | The platform's mutation gate, operation surface, serialization, encryption, and recovery controllers; Wakigaki's island codec, portable checkpoints, and the save, history, and encryption chrome over them |
| Interchange | Plain text, Markdown, and DOCX projections plus their whole-document orchestration |
| Material | Profile inclusion, bounded verification, language catalogs, and local hydration |
| Updates | Complete-only signed manifest verification and copy-forward renewal |
| UI | Focused controllers, desktop/narrow projections, and the app.ts composition root |
| Build | Deterministic bundling, capability construction, notices, profile shells, and artifact audits |
Controllers communicate through narrow ports. src/app.ts composes them and exposes product operations through window.wakigaki; it is not a second state store and does not expose ProseMirror internals.
window.wakigaki.statistics() is a synchronous, detached read of the canonical proposed document. Its document and selection figures count authored text, headings, table cells, notes, and captions; generated contents, labels, comments, and repeated page projection are absent because they are not in that model traversal. Pending insertions count and pending deletions do not. Page fields are null until the current document's pagination pass settles, and a selection page field is also null for an empty selection. Metrics are never serialized.
window.wakigaki.selection(element?) is another synchronous, detached read. Its kind is caret, range, node, or cell, and its from and to always address the canonical document even when a range or cell selection crosses visible frame hosts. hasFocus and within describe the live visible projection; restoring that focus cannot replace a cross-frame canonical selection with one host's local caret.
Profiles and language material
Complete carries every included interface language, Markdown import, DOCX import/export, and the explicit signed release check. Compact carries the same core editor, English plus at most one other interface language, plain-text interchange, and Markdown export. It carries no attachments and no Complete-only capsule. Complete creates Compact from a prebuilt Compact shell and exact selected material, never from live DOM. Both profiles carry the same full signed inventory and exact publisher attestation; only allowed language bodies are omitted. Ordinary Save preserves them without a signing key. Neither profile upgrades in place; a newer trusted Complete opens the document and creates another file.
Supported languages are those the application understands; included languages are the records in one file. English is always included and is the fallback. Records are verified before startup and hydrated locally only on use. Emergency messages for every supported language live in the core runtime so a subset file can explain its missing interface without fetching it.
Interface language belongs to the reader and is selected from browser preferences; it is never document state. settings.documentLanguage is a BCP 47 tag carried with the writing and projected as lang. settings.locale carries regional number, date, time, currency, week-start, and measurement choices. Stable identifiers, not displayed samples, are serialized. The family locale model owns deterministic language/region defaults and format registries. Material and interface choice never enter document history or recovery.
Layout and pagination
Canonical layout intent consists of page settings and document-node attributes: geometry, running content, indents, tabs, flow rules, tables, images, and style choices. Pagination is a pure plan over canonical positions and bounded renderer measurements. Page assignments, fragment placement, reservations, gaps, and measurements never enter document state, history, recovery, or serialization.
The inert physical-width measurement view renders the same canonical nodes. On narrow screens it remains closed and non-interactive while the visible editor uses a continuous touch layout. It has no focus, selection, dispatch, or save path. WebKit can wrap edited text differently in the two views, so the plan owns break positions and the visible grid absorbs sub-line divergence in disposable gap remainders. Print never reads those remainders.
Atomic content moves whole when it fits and reports overflow when it cannot. A float advances no flow height; text lines already reflect its shortened width. The planner instead protects the float's vertical span, hoisting a conflicting break above it. Forced breaks stay forced, overlapping spans move together, and an over-page span reports overflow.
Sections that need regions use a disposable frame plan over one canonical DocumentStore. Each page owns ordered regions and each region owns one to three width-specific column frames. Visible frame editors contain only their canonical ranges; every local step, native selection, composition, clipboard gesture, table selection, and history command maps back into the canonical transaction stream. They never own persistence or history. The canonical editor stays mounted as the serialization and plugin authority while its editable surface is hidden. Generated contents rows and repeated table headers are renderer projections with no second canonical document-node identity. Contents rows instead keep stable (contents ID, row, line) projection addresses; only a row's first line exposes its link to the canonical heading.
Cropped or quarter-turned images use a measured carrier box shared by schema serialization and the editing node view. Crop changes the box, rotation swaps its axes, and resize commits through the inverse transform so pagination and text wrapping agree with what is drawn.
Sections
A section_break node opens a section continuously on the current physical sheet or on the next sheet. It carries the section's paper, orientation, margins, running content, page-numbering rule, and column geometry: count, optional unequal widths, and gutter. A null field follows the document, and the document's own settings are its first section. documentSections in model.js is the one reading of that structure. A continuous region remains on the sheet only when the physical page and margin origin agree; otherwise it starts a new sheet. The planner switches page height at the break, lays each sheet and frame at its own geometry, and settles printed numbers from that plan. The live projection mounts each canonical range in its owning frame; the hidden measurement view supplies width-specific spans without becoming another editor. Before browser print, Wakigaki replans at physical geometry and the print projection clones each planned sheet with its frames, generated rows, repeated table headers, running content, and settled page labels. After print it restores the current responsive projection. Named page rules still prevent the browser from cascading incompatible section geometry or margin content. Word sections map one to one.
Speculative reservation
Footnote editing, table/image resizing, and future resizable objects share one interaction rule: reserve an estimated footprint at gesture start, grow it only when needed, keep previews disposable, then commit one canonical change and perform exact reflow at release or another definite boundary. Cancellation restores the previous projection, and every reservation must make bounded forward progress.
Structured document semantics
Named styles
settings.styles is canonical formatting vocabulary. Definitions have stable identities, bounded names, structural roles, inheritance, next-style behavior, and block/character defaults. Built-ins always survive normalization at their canonical identities. Invalid references collapse safely; inheritance is bounded and stops on cycles. null means no override, and character size is a multiplier of the document base size.
Every text block carries a styleId. Generated CSS is only a projection, used by both editing and measurement. Deleting a style reassigns its blocks to Normal in the same undoable transaction. Enter at the end of a nonempty block uses that style's next; other splits preserve the current style. Footnote paragraph identity remains model-enforced rather than style-enforced.
Footnotes and comments
A footnote contains paragraphs of plain text. Schema, normalization, and command guards enforce the rule on typed, pasted, undone, and loaded content. Comments and insertion/deletion revision marks are the explicit exceptions; visual formatting and formatting-revision marks are removed. Enter creates a new paragraph inside the same note, notes cannot contain notes, and numbering follows reference order rather than creation order.
Comments are canonical anchored relations whose cards and tint are projection. They are allowed in notes because editorial discussion is not document formatting.
Review and tracked revisions
Insertion, deletion, and character-format revisions are marks; created blocks and tracked paragraph joins use canonical attributes. These marks and attributes are the sole review state. Visual strike/tint, reviewer hover text, and projected join boundaries never define semantics or change pagination.
Accept/reject, next/previous traversal, and bulk decisions are deterministic model transitions, each one undoable event. The clean draft is one shared reject-all transform used by print — Wakigaki's own action and the browser's own print menu alike — export, and Compact creation. The clipboard instead copies as if revisions were accepted so proposals do not escape into an unrelated document. Recording state and the current reviewer name belong to the session; a name reaches the file only in revision metadata the session authors.
Links, paragraph formatting, named styles, and structural edits apply directly and the interface says so. Version comparison and merge are not inferred from bounded recovery history. If scheduled, they require the explicit immutable baseline described in ROADMAP.md.
Numbered objects and tables
For a numbered image or table, canonical state records caption text, counting intent, and target identity. Normalization derives the displayed label in the document language; cross-references follow that derived label and identify a missing target. Labels remain inside already measured atomic captions.
A table of contents is canonical in the same way and for the same reason. Its rows are derived from the selected semantic heading depths and optional Title by normalization and stored on the node, so serialization, export, and layout print a row without re-walking the document. The block owns its choices of heading levels, Title inclusion, and page numbers; stable heading IDs anchor each generated row. The page each row names is not stored: it belongs to the pagination plan, which is a projection, so a row leaves an empty box that the page projection fills and print reads from. Width-specific row and wrapped-line addresses let a long contents block continue across columns and pages without cloning its canonical node. Only the first line of each generated row owns its keyboard link; wrapped continuations remain inert. Clicking a generated page label selects the one canonical contents node across every continuation, so removal and undo remain one canonical edit. Full generated-line measurements are reused only for the same immutable contents node, frame width, canonical settings, projected style revision, and language. Font readiness clears that geometry, and detached row snapshots are retained only for the current canonical document. A document may hold at most 2,000 selected rows per contents block; a larger insertion or settings change is refused before it changes the document. Cached labels longer than 500 UTF-16 units show an ellipsis while the authored heading remains intact.
A table is one structured node of ordered rows and cells plus bounded style intent. Wrappers, column groups, handles, selection tint, caption DOM, and page placement are projections. Commands keep deterministic focus and commit one history event per action or resize. Native <caption> and <th> preserve semantics: a cell covering several rows or columns carries its own span, and a table's heading is its leading header rows rather than a second attribute stating the same fact. A span is bounded canonical state: integrity refuses a span below one or beyond the widest grid anything could walk, a column-width list that does not name one width per covered column, a merge reaching past the last row, and rows that disagree about the grid they describe.
A table travels as one object while it fits a page. One that cannot reports its row boundaries and its heading's height, and the planner cuts it only between row groups that no vertical span crosses. It starts each continuation page with the heading redrawn. The heading is redrawn only where the page it opens can still hold the row beneath it: a courtesy that costs the reader a row is not one, and a row too tall for any page continues at measured text-line boundaries inside the owning merged cells; the cell shell stays inert while each visible text range maps to its canonical textblock. That redrawn heading and the boundary it follows are decoration the plan owns: they carry no identity, are not editable, and never reach the document, history, or any interchange projection.
Attachments
Attachments let a sovereign document carry its supporting evidence without a second folder, service, or account. Their scope is an owner decision, not the common-document test that set the original feature list. Their payloads are opaque canonical bytes and round-trip unchanged. The manifest records bounded metadata and digests inside the document island; payloads inflate or decrypt only on use. A transient viewer may use only the fixed passive media types listed in SECURITY.md. Manifest metadata cannot affect document rendering, pagination, interchange, or executable capability.
npm run audit:size owns artifact budgets and npm run benchmark:pagination owns deterministic pagination-work limits. Measurements and historical implementation detail belong in their baselines or Git history, not in this architectural contract.
Heading numbering
heading-numbering.ts owns the opt-in three-level scheme and counters; outline-model.ts reads semantic headings once for editor labels, outline, contents, and exports. Visible Heading 1–3 use semantic depths 1–3 (stored HTML levels 2–4); Title and Subtitle stay unnumbered. Exclusions consume no number and do not reset descendants. Parent changes reset descendant counters; missing ancestors read as zero. Layout section boundaries do not reset them. Numbers are separate noneditable spans; authored heading text has no prefix.
HeadingNumberingController applies document scheme and selected-heading intent in one canonical undo event. Its public automation delegate is wakigaki.formatting.headingNumbering({ scheme?, heading? }), where scheme is { enabled, levels: [{ format, startAt }, ...] } with exactly three levels, and heading is { id, excluded, startAt } with a nullable restart. Invalid schemes or stale/nonheading IDs throw before dispatch. Reopen and checkpoint restore derive cached labels before the first frame without adding history. wakigakiReady is the readiness contract; a format number is not readiness.
Pure document/clipboard migrations run before ProseMirror reads saved JSON. Old documents and every old checkpoint remain unnumbered, even if ignored future-looking keys were present. Unknown future versions are refused before assets or canonical state can change. Plain text and Markdown render the same numbers as readable prefixes; DOCX carries heading numbering independently of ordinary lists.
Wakigaki security
This document owns Wakigaki-specific threats and security constraints. Shared envelope, cryptography, operation, attachment-storage, and signed-update rules belong to the platform security model. DEBT.md tracks known hardening work; ROADMAP.md owns future features.
Reporting a vulnerability
Do not put vulnerability details in a public issue. Report them privately to ams@commerce.net with affected versions, browser/OS versions, a safe minimal reproducer where possible, and the expected confidentiality, integrity, authenticity, or local-data impact.
Threat and trust boundaries
Protected assets include document content, comments, attachment bytes, checkpoints, passwords and derived keys, the executable shell and bundled material, recovery records, and the family release key.
Wakigaki handles malformed and oversized input within stated bounds, offline attacks on encrypted files, update substitution/replay, relation corruption, operation races, and accidental plaintext recovery after encryption. It cannot protect against a compromised browser, operating system, extension, local account, or release key; weak passwords; an authorized reader copying plaintext; or denial of service outside application resource bounds.
The validated document is canonical data. Shell, runtime, and capabilities are executable state. Password encryption protects the document and attachments, not an arbitrary surrounding HTML shell.
Opening files safely
A saved Wakigaki file is an application, not passive data. Opening an untrusted file directly executes its shell, which could imitate Wakigaki and steal a password or plaintext. For uncertain provenance, start a trusted Wakigaki and use Open; it validates and internalizes only the contributing document and attachments.
An update signature authenticates the release-channel artifact, not every file a reader later saves. Self-checks cannot authenticate a shell and embedded key that an attacker replaced together.
Startup verifies bundled payloads with Web Crypto and therefore requires a secure context. Local files and HTTPS qualify; remote plain HTTP fails closed.
Password encryption
Wakigaki uses the platform construction: Argon2id 1.3 (64 MiB, three iterations, parallelism four, 32-byte result), AES-256-GCM-SIV, a 16-byte salt, and a fresh 12-byte nonce from crypto.getRandomValues. The strict envelope is $v1$<base64url-salt>$<base64url-nonce>$<ciphertext-and-tag>.
The salt changes when encryption is enabled or the password changes, then persists with the document; every save has a new nonce. The canonical island JSON — the document in its domain, the attachment ledger, and the identity — is raw-deflated before encryption, so an encrypted file exposes nothing stable across saves in its shell plaintext. Compression metadata and parameters are authenticated, and decryption performs bounded streaming expansion, fatal UTF-8 decoding, and strict document validation before state changes. The compression header is mandatory: an envelope without one is refused before any key derivation. Wrong-password and damaged-ciphertext errors are deliberately indistinguishable.
Attachment payloads are sealed separately under the same derived key. Each has a fresh nonce and associated data binding it to its manifest identity. The plaintext digest stays inside the encrypted document island; payloads decrypt only on use. Mixed encryption modes, swapped blocks, or damaged payloads reject the file or use without disclosing which condition occurred.
The derived key may remain in memory while the document is open and is cleared when encryption is disabled, its password changes, or another document replaces it. Browser memory cannot promise forensic erasure. There is no password reset. Enabling encryption must remove plaintext recovery before success, fail the transition if it cannot, and suppress later plaintext records. Portable checkpoints remain inside ciphertext.
Parsing and active content
Document, fragment, interchange, envelope, material, and update inputs are strict, bounded, and all-or-nothing. User text is escaped from serialized script blocks. Importers either construct one fully validated document or make no change. Canonical layout values are bounded data, never CSS or DOM input; screen and print output use the correct text or CSS-string escaping.
DOCX packages are hostile ZIP/XML input: resource ceilings apply, external resolution and active content are rejected, and export copies no foreign entries, macros, scripts, remote images, passwords, or key material. Raw Markdown HTML stays literal and external images are never fetched. INTERCHANGE.md owns fidelity and loss.
The editable DOM, pagination plan, ruler, print furniture, and measurement view are projections and cannot become serialization, recovery, history, or encryption input.
Content security policy
The build uses the shared family policy. A saved copy preserves it. Document intake still follows the strict parsing and inert-content rules above; Launch follows the family trust checks.
Attachments
Manifest names, types, descriptions, timestamps, sizes, and digests are inside canonical document data. Payload blocks remain outside it for bounded lazy use. Names and descriptions therefore receive the same encryption as document text.
A declared content type is hostile metadata. It is normalized and can select only an identical fixed passive browser type from this closed list: text/plain, PNG, JPEG, GIF, WebP, AVIF, MP3, WAV, WebM audio/video, and PDF. HTML, SVG, unknown, and missing types remain export-only. Names, extensions, descriptions, classifications, and byte signatures cannot add a capability or switch viewers. Byte recognition may warn about a mismatch but does not change the selected passive type. Adding a viewer is a security decision.
The host platform supplies the type at import — a field, not a detection algorithm. It is therefore the one manifest field that heals instead of refusing: a dropped type costs only a caption, while a dropped entry orphans a payload.
Blob URLs inherit Wakigaki's origin, so attachment-controlled active content is never assigned to them. scripts/dev-attachment-isolation.mjs retains the cross-browser probe and active HTML/SVG controls.
Recovery, profiles, and updates
Unencrypted recovery and automatic versions are plaintext browser records, not an audit log. Portable checkpoints travel in the document. Deletion cannot promise physical erasure from storage, backups, swap, or browser internals.
Build-owned capability and language records are bounded and verified before hydration. Wrong release/profile, duplicate, missing, damaged, or out-of-bounds material fails startup; no network fallback exists. Complete-to-Compact creation selects a canonical snapshot, prebuilt Compact shell, exact runtime, and at most one additional language record in one gated operation. It copies no attachments or Complete-only capability. Complete and Compact retain the same full signed inventory and publisher attestation; ordinary Save uses no signing key. Launch refuses either profile if signature, fixed structure, or present material fails verification.
Complete performs the sole network request only after Check for updates. The credential-free, referrer-free, abortable fetch reads a bounded signed manifest from the compiled-in HTTPS origin and sends no document or identifier. It downloads no application bytes. A target version must be newer; when the reader later obtains it, Wakigaki independently validates the Complete shell, runtime contract, profile, bounds, digests, origin, and release material before writing a new copy. Failure or cancellation leaves the running and original file unchanged.
Launch and the served copy
Family Launch policy owns recognition, signed inventory verification, inherited browser policy, handoff, and offline installation. Every member follows that contract without exceptions.
Release trust and changes
The private P-256 family key stays outside repositories, artifacts, sites, fixtures, transcripts, and CI. Existing files trust exactly their embedded anchor, extended only by signed key transitions; rotation, revocation, recovery, and the out-of-band path for a file that trusts only a retired key belong to the key guide, and operational custody to the repository release guide.
For any security-sensitive change, state assets, attacker, trust boundary, and failure behavior first. Test malformed/boundary inputs, tampering, cancellation, ordering, and no-partial-write behavior with ephemeral keys. Treat dependency changes as supply-chain changes. Collaboration must define authority, copying, revocation, key distribution, observable metadata, and convergence before an optional transport is implemented.