Files
meguri/docs/architecture.md
Ultradesu 5260c2828b
Checks / test (push) Failing after 2m25s
NEXT
2026-10-07 15:30:31 +01:00

36 KiB
Raw Permalink Blame History

Architecture

See workspace variables and visual matrix rules for design rationale; implemented behavior is specified in product.md.

A modular Rust application serves the web/API and runs a durable publication worker. The same executable can run either or both roles. PostgreSQL is the sole persistence and coordination service. Production targets GKE with Cloud SQL for PostgreSQL; no Redis or message broker is required.

Persistence and concurrency

Users, sessions, workspaces, memberships, audit, history, provider checkpoints, and sync jobs are Cot ORM models. Bounded typed documents use serialized JSON in text columns with a PostgreSQL JSON validity constraint and optimistic revision. Cot 0.7 lacks JSON/UUID model fields, so UUIDs also use validated strings. Do not introduce another ORM for these types.

Cot 0.7 has no transaction API. Store::commit uses one parameterized data-modifying PostgreSQL CTE through Cot Database::raw_with: compare-and-swap checks revision and authorization, then writes membership, history, audit, and the durable sync request atomically. A constraint failure rolls the entire statement back. Ordinary reads, sessions, and checkpoints use Cot queries/models. Pending requests do not change effective coverage. Published state is separate from desired state.

The worker conditionally claims a five-minute lease token. Publication has a four-minute deadline and HTTP requests a twenty-second timeout; no document lock spans HTTP. Verified operations get checkpoints. Completion updates status and audit atomically, guarded by the lease token. Newer revisions remain staged when older runs succeed or fail. Authorized jobs retry their pinned document with bounded backoff; a successful publication runs again only after another explicit Sync. Crashed workers leave recoverable leases. This is not a distributed fencing protocol: read-back/conflict handling remains essential after process suspension or an uncertain remote timeout.

Run Cot migrations once in a deployment job, then start replicas with MEGURI_SKIP_MIGRATIONS=true. The initial migration creates frozen Cot models; the second adds scoped foreign keys, constraints, and indexes in one atomic PostgreSQL block. A JSM destination can have only one local workspace binding. Never edit applied migrations.

Scheduling model

Rotation and priority-owner schedules are separate typed variants. Priority-owner schedules reference another schedule as fallback; the dependency graph must be acyclic. Evaluation splits at local handovers and override boundaries. Resolve a schedule's approved overrides before letting other schedules depend on it. Adjacent equivalent intervals merge; the result records the assignment reason.

The engine receives explicit state and time bounds. It must produce the same result regardless of database, network, or provider. Approved changes and dated revisions support historical inspection; future configuration changes must not rewrite past history.

configuration contains pure dependency-aware removal planning and route permutation operations. Services authorize, revalidate the plan against the submitted revision and explicit current time, then commit through the existing history/audit/outbox boundary. Providers still receive ordinary schedules, policies, and consecutively ordered routes.

absence computes pure, bounded multi-schedule replacement plans and before/after summaries. Range inputs use each schedule's own handover. Requests reference the absence that produced them, permitting approval to revalidate original ownership against the same document with that absence removed, while checking substitute availability against current state. Automatic coverage fallback remains an engine rule. Draft previews never write; absence and all generated requests commit as one state transition. Unavailable assignments are diagnostics, never eligible future publication periods, including single-participant rotations.

generation is a pure typed data/binding interpreter and materializer. generation_service validates one workspace revision and authorizes that workspace, then atomically persists its variables, generated resources, history, audit and sync request through the same commit boundary. Provider calls remain outside activation. Workspace documents retain exact variables, rule definitions, and resource ownership manifests so expansion is reproducible. Legacy migration 5 introduced variables; migration 8 consolidates them into the workspace document. No applied migration is changed.

Identity and permissions

Session cookies are HttpOnly, SameSite=Lax, and Secure under HTTPS. Mutations require same-origin requests and a session CSRF token. Passwords use Argon2. Resource lookup always includes workspace context and server-side membership. External identity linking is a future adapter, never a replacement for internal user IDs.

Replacement preview is a read-only service using the same engine, revision, participant and permission checks as submission. Calendar actions use server-generated interval boundaries; inclusive range inputs are converted through schedule handovers on the server. Volunteering changes the initiator, not the approval requirements. Comments remain in the backward-compatible reason/force_reason fields but are optional action annotations. Current-shift clipping uses one explicit server timestamp per validation/application.

Provider contract

Operations collection reads use pages of at most 50 items and traverse every page for ownership recovery and drift checks. Team identifiers retain the optional og- prefix through validation, storage and HTTP paths.

Readable provider names append the first 128 bits of SHA-256 over workspace and resource UUID bytes. Discovery matches this stable suffix or the legacy full technical name; duplicate matches fail. Checkpointed resources rename in place. Recovery without a checkpoint accepts the legacy name only when every other managed field matches the desired state.

Publication includes the explicit compilation clock as_of. For plans with boundaries outside local half-hour marks, the adapter uses the currently effective assignee as a finite base rotation, or noone if coverage has not started. Future assignments use schedule overrides with millisecond precision; future gaps and the rounded base tail use noone overrides. Native base endpoints are rounded outward to local half-hour marks; current/future effective coverage remains exact. JSM rejects new overrides starting in the past, so publication guarantees coverage from as_of forward without rewriting local history. A transition overtaken by HTTP latency fails and is recomputed on retry. Coverage checkpoints contain parent schedule ID, expected payload and returned alias; an empty alias records a durable create intent. Recovery claims a matching remote override only against such an intent, rejects ambiguous matches, and verifies read-back before recording success. Stale owned intervals are removed after replacements are written; unknown/manual overrides still block unless overwrite is explicit. Expired owned intervals may disappear from the provider's ongoing/future collection without being drift. Rotation arrays are compared without order only for these independent generated intervals; escalation and participant order remains significant.

The Provider trait consumes canonical Publication data and a checkpoint store. Plugins are Rust modules linked at build time, without an unstable dynamic-library ABI. New providers add a typed integration variant, capability validation, dispatcher constructor, module, and contract tests. No engine changes should be needed.

JSM publishes finite single-person rotations for half-hour-aligned plans, and base rotations plus exact future overrides otherwise (maximum 500 canonical periods per schedule). Escalations resolve local IDs to remote IDs; routing priorities become consecutive indices. Deterministic ownership names allow recovery when a create succeeds but its local checkpoint is lost. Read-back checks managed fields, arrays, timestamps, and external overrides. Only checkpointed owned resources may be deleted. Workspace-specific environment credentials never enter documents or audit.

Reconciliation covers configuration, not user notification preferences, forwarding rules, or delivered notifications. JSM has no cross-resource transaction here: partially published revisions stay failed until retries complete. A remote edit between GET and PATCH cannot be fenced with the local lease. A dedicated-team acceptance test must compare actual provider timelines as well as CRUD.

JSM REST references (verified September 2026):

Random recipients and escalation repeats require JSM Premium/Enterprise. Provider validation must not pretend a feature is available merely because it is representable in JSON.

JSM documents repeat.waitInterval=0 as disabling repeats even when count is nonzero; the reference Terraform uses this combination. Enabling repeats requires a positive interval. A disabled local count maps to provider count 1 / interval 0 because the provider count minimum is 1. The reference's immediate step uses delay 0; verify it in live acceptance because the published schema lists a minimum of 1. Never silently change immediate notification to a one-minute delay.

Scheduling metadata and import boundaries

WorkspaceDocument owns versioned scheduling people, country holidays, and per-schedule handovers. Contacts do not become authentication principals. Workspace-directory reads are workspace-scoped; identity IDs cannot be invented or linked through unverified email. CSV parsing and comparison are pure functions; preview is read-only, and import rechecks the workspace revision and commits history, audit, and outbox together. Working intervals, variables, schedules and access updates share the same workspace revision; working-interval changes atomically update its route snapshots.

Workspace creation atomically creates its document, owner membership, initial history, audit and sync request. There is no parent entity or hidden default team. Migration 8 replaces empty legacy tenant/team tables and refuses populated legacy databases; demo provenance checks run before migration.

Migration 3 upgrades old JSON snapshots without changing old handover boundaries or global-holiday meaning and introduces workspace working intervals. Migration 4 adds user country (CY) and an optional display timezone (null follows the browser). Current scheduling state copies the workspace timezone; historical documents retain the timezone effective at their revision. Country-profile changes propagate as dated workspace revisions, while display-timezone preferences remain outside the engine.

Email-only recipients resolve through Jira user search with an exact, active, unique email match. The authenticated service account needs Browse users and groups permission and appropriate Jira API scopes in addition to Operations access. Profile visibility may hide emails; explicit Atlassian account IDs remain the fallback. All required recipients resolve before the first provider write. Tests inject a localhost directory and never query real employee data.

Demo runtime boundary

demo_runtime chooses the database before Cot migrations. A separate public.meguri_runtime_guard records last startup mode; proof and reset recheck it under a PostgreSQL advisory transaction lock. Empty-DB claiming checks user relations. Reset enumerates only Meguri application tables, uses no CASCADE, and preserves migration/provenance metadata. Distinct application names detect another active client. A protected configured DB triggers a separate database and an immutable process notice surfaced by /api/session; no credentials are exposed. Normal startup marks its database normal and never schedules cleanup.

Migration 6 adds nullable demo deletion timestamps and seed pins. Cleanup gates every write on demo provenance and uses explicit creation timestamps and a supplied clock. Workspaces expire as complete configuration aggregates, revisions invalidate in-flight edits, listings/access exclude expired resources, and a later CTE deletes dependent rows in one statement. Request expiry saves a new historical document and outbox job. History compaction retains a boundary snapshot. A minute timer refreshes expired seed workspaces through the ordinary services; only the five baseline account identities are permanent during a run. External provider writes are disabled in demo at both configuration and dispatch boundaries.

Presentation catalog

Migration 7 adds nullable en/ru/fr user language. A LazyLock Rust catalog and eagerly loaded browser catalog share messages.json. Parameter substitution is single-pass text, never HTML. API diagnostics retain English compatibility and are localized at presentation. Account language updates use the profile write path without scheduling/outbox changes. Navigation uses browser history and ordinary links; stale workspace responses cannot replace the currently selected workspace.

Cover request date/time inputs are resolved through the engine's IANA wall-clock resolver on the server. Optional clocks preserve legacy schedule-handover API behavior when omitted. Preview and mutation share this normalization path and the same ownership/availability validation.

Audit reads enrich payloads from scoped history snapshots and current directory fallbacks without rewriting stored events. Workspace mutation audit timestamps reuse the effective timestamp returned by the history CTE, ensuring the event resolves against its own revision.

Applied requests retain original legs and optionally store effective_legs (None means original legs for legacy documents; an empty list means fully superseded). Application subtracts new spans from older active spans, preserving both sides of partial intervals. Engine, provider identities and configuration protections use effective spans; all updates share the existing revision-checked transaction.

Binding preview uses a presentation-only formatter registry for people, schedules, escalation policies and routing rules. A map built from the already authorized client state resolves known IDs/exact emails to shallow name/type/identity objects in a recursive walk. Unknown references and scalar values remain unchanged; generated-output metadata stays an example. No extra network queries or changes to binding values, generator types or saved JSON.

Generator issues carry optional rule/block/variable UUIDs for navigation, independent of localized paths. The client never publishes when navigating diagnostic links. Diagnostics always stay within the authorized workspace. CoverRange optionally contains exact UTC bounds and a schedule ID, validated inside the absence and declared duty days. Overlap checks compare effective intervals per schedule; exact ranges on distinct schedules may coexist. Existing bulk duty-day ranges keep their behavior.

CoverRange.local carries optional schedule scope plus local date/time bounds. The engine resolves both via the workspace IANA zone, rejecting DST gaps/folds and invalid bounds. Existing day-only and exact-UTC API ranges remain compatible; the UI emits local ranges exclusively.

Coverage diagnostics distinguish pending approval from missing substitutes. Pending request boundaries split uncovered intervals so partial requests never disguise remaining gaps. The UI shows substitutes and outstanding approvers, links to requests, and keeps publication blocked until approval. Browser regression covers partial pending coverage, remaining gaps, approver filtering, and responsive spacing.

Schedule setup places narrower variable and generation-rule panels beside a stacked availability sidebar. Absences and holidays remain open; their lists scroll internally. Country holiday groups retain accordions inside the bounded list, with add/import actions opening dialogs. Responsive layout moves the sidebar below the editors. Browser checks cover column position, bounded holiday expansion, visible actions and mobile width.

Generated routing placement is shown only when manual routes and an enabled route-generating rule coexist. Options name the manual routes in evaluation order; the stored insertion index is unchanged. Browser checks verify hidden controls for generated-only setups and named options/order for mixed routing.

Resource inventory is a distinct UI component (resource-inventory.html). All expanded resource kinds share resource-details.html: labeled icon rows, consistent spacing, sequential connectors, and a common source-rule control. Presentation rows and monochrome SVG paths come from pure UI helpers; schedules, routing order and provider state are unchanged. Check rotation, fixed owner, priority fallback, escalation timing/repeats and routing conditions on desktop/mobile.

Expanded resources use compact type-specific horizontal widgets instead of parameter tables: rotation metadata plus ordered people, priority people plus fallback, escalation steps plus repeat footer, routing conditions plus destination. Shared icon, chip and connector styles keep them consistent. Chains wrap within narrow screens; source links and edit actions retain their common placement.

The generation-rule editor is a generic technical constructor: visible iteration aliases and collection bindings, an explicit optional condition, and typed output blocks. New rules start without implicit project/responsible-variable detection or generated blocks. Variable names carry no semantic meaning. Compact controls retain the existing typed binding editor, value preview, live server diagnostics and draft semantics. Check no-iteration rules, nested collection bindings, conditions and multiple output types on desktop/mobile.

Binding selection uses one anchored, viewport-clamped popover beside the originating control; the constructor remains visible and keeps its scroll position. All source kinds, nested lookups/lists, paths, text affixes and live value previews remain supported. Literal escalation delays are directly editable. Done/Escape closes the popover and returns focus. Server and change previews are collapsed summaries with counts and bounded change lists. Browser checks cover context retention, source editing, nested bindings, keyboard dismissal and mobile bounds.

Generation rule results use the actual authorized server expansion, including schedules, escalation policies and routes, with resolved names and compact settings. The panel is always present with explicit checking, invalid, disabled and empty states; it respects iteration filters and does not simulate generation in the browser. Preview data contains only scheduling resources and scoped people, never integration configuration.

Workspace aggregate

WorkspaceRow stores one WorkspaceDocument and one revision. The document owns working interval, drift policy, variables, rules, schedules, people, holidays, absences, requests and provider configuration. WorkspaceMemberRow stores per-user grants and ownership; there is no parent entity. History, audit, jobs and checkpoints reference one workspace ID. Generator activation and working-interval updates use the ordinary atomic workspace commit. JSM team IDs identify external provider destinations, not internal teams.

Variable list summaries render nested nodes by type. Object entries prefer a nonempty text name or title; otherwise they show a bounded key/count summary. People resolve through the scoped directory. Summaries never read a container’s null scalar slot as its content.

Variable editing uses a bounded, progressively disclosed tree: only the root starts expanded, object rows show names and collection summaries, and nested values expand in place. Values mode hides type/key controls; Structure mode exposes them without losing edits. Duplicate assigns fresh IDs recursively; ordering and live server validation remain intact. Check nested editing, duplication, mode switching and narrow viewports.

Variable-tree indentation uses stylesheet depth classes, not inline custom properties (blocked by the application CSP). All row contents share the inset, with vertical guides for ancestor levels and smaller but preserved mobile steps.

Generation map opens from Generation rules as a read-only, draggable/zoomable Cytoscape canvas. Server preview returns actual iteration counts, input contexts, predicate outcomes and produced IDs; no scheduling evaluation is duplicated in JavaScript. Iterated outputs start collapsed into a group; inspecting a node or pass reveals source data and verified resources. Invalid drafts show diagnostics rather than stale resources. The map is a captured draft preview refreshed explicitly, and never applies changes. Bundled Cytoscape 3.33.1 is served locally with its MIT license; no CDN or new framework is required. Browser coverage checks groups, details, source navigation, pan/zoom and mobile layout; engine regression verifies traced pass results.

The generation map arranges numbered rules in saved execution order from left to right, joined by solid sequence arrows. Variables occupy the upper band in first-consumer order, rules and their iterations and conditions occupy the middle band, and outputs occupy their producer columns in the lower resource band. Dashed edges show data dependencies. Repeated output groups expand into individual resources within the resource band. Browser checks verify rule ordering, band separation, output expansion and manual resources.

Map input details display evaluated server values for each block and pass, never serialized bindings. Shared values are deduplicated across passes; differing values are explicitly counted. Generated references collapse to type/name cards, and people resolve by display name. Empty binding metadata and raw reference UUIDs are not presented. Condition operands and escalation recipients/delays come from the same resolver used during expansion.

The map’s all-passes input overview lists deduplicated upstream sources, not expanded output settings: variables with used binding addresses and resource references grouped by producing block. References internal to the selected whole rule are omitted; selecting an individual output block shows its dependencies. Selecting a concrete pass still exposes its evaluated field values.

Expanded map resources use type-specific labeled fields and the same compact reference cards as inputs, never JSON dumps. Priority schedules show ordered people, fallback and zoned handover; rotations add anchor/cycle; policies show recipient/timing/condition and repeat behavior; routes show conditions, working window and destination. Reference names resolve from the preview snapshot. Browser regression checks visible names, participant order and absence of UUID metadata.

When a checkpointed resource GET returns 404, discard its obsolete checkpoint and schedule child checkpoints, then rediscover the stable ownership marker before creating. This also recovers a replacement whose create response was lost. Do not query child collections of a missing parent. Rebuild dependent references with the new remote ID. A missing collection or authorization failure remains an error; only resource absence authorizes recreation.

Explicit publication and shared drafts

Saving workspace configuration, approving replacements, importing data and editing generator drafts never authorize provider writes. These changes persist in PostgreSQL and are visible to other authorized members. Ordinary unfinished modal forms remain local until Save. Generator drafts autosave separately, including invalid intermediate values, with their own optimistic revision and workspace scope.

The top Review changes panel compares desired resources with the last successfully published document. Its Sync button is yellow when unpublished changes exist. Sync validates/applies the shared generator draft and explicitly authorizes a specific workspace revision; stale reviews fail. Provider status, errors and create/update/remove counts live in this panel. Failed authorized publications may retry the same snapshot; newer saved changes must never enter those retries. Successful jobs do not automatically refresh their horizon: the displayed coverage end requires a further explicit sync to extend it.

The worker persists the authorized document and publishes that snapshot, not the newest workspace document. A click while a publication lease is active is rejected. Existing jobs are paused by the manual-publication migration until an explicit click. Published snapshots survive history pruning. Tests cover no writes before a click, pinned retries across later edits, no periodic writes after success, scoped shared-draft persistence, stale concurrent edits, resource review against the published baseline, and the browser workflow across sessions.

Compact recurring provider rotations

A plain base rotation whose effective published coverage matches its ordered calendar-day cycle is exported to JSM as one finite recurring rotation named after the schedule. The engine supplies the current cycle phase, ordered participants and calendar-day shift length; the provider only translates these fields to daily/weekly API values. The remote schedule ID and ownership marker stay unchanged. A publication replaces the old per-shift rotations rather than accumulating new ones. Native boundary precision must be representable; irregular effective coverage and replacements retain the exact interval export. The canonical expanded timeline remains available for verification and preview. Tests cover mid-shift starts, pre-anchor phases, participant ordering, DST, replacements, migration of an existing owned schedule and idempotent repeated publication.

When an owned JSM schedule has one current and one desired rotation, edits preserve the rotation ID through the dedicated rotation PATCH endpoint. Schedule metadata is patched without a rotations array. Verified intermediate writes are checkpointed; a durable pending-step snapshot recovers a lost response without mistaking that exact planned state for external drift. Tests model nested rotation replacement by schedule PATCH and require stable rotation identity across participant reorder, rename, retry and unchanged republish. Historical rows already retained by JSM are not deleted by this repair.

Workspace state save/reload controls live beside Sync in the top Review changes panel. The localized hint distinguishes shared draft autosave, validated local application, and explicit publication; Sync includes application, so a separate save is optional. Reload retrieves the shared draft, not a historical rollback. Removing a variable or generation rule requires confirmation before mutating or autosaving the draft. Browser regression covers cancellation, confirmation, top-panel controls and narrow layout.

JSM coverage rotation names use the schedule name, including priority coverage and the exact-time base rotation. Multiple finite rotations append the interval start in the schedule IANA timezone with date, time and UTC offset to distinguish DST folds. This is presentation only; ownership markers, coverage, checkpoints and explicit publication remain unchanged. Existing rotation names update on the next authorized synchronization.

Manual and generated routing editors expose the existing greater-than and less-than operations. Priority follows alert severity (P1 highest); P3 and above is represented as less-than P3 with negate=true, including the boundary. The editors show comparison controls without an additional priority instruction; the provider wire contract is unchanged.

Publication status exposes the lease expiry and a server-calculated publication_busy flag. Sync is disabled for an active lease, not merely the persisted publishing state. The review panel explains restart recovery and expired leases; normal polling refreshes eligibility. Restart never clears a live lease. PostgreSQL regression checks active-lease rejection and explicit reauthorization after expiry.

Nested binding paths retain the raw input text while typing, including trailing spaces and slash separators. The parsed path still updates live for shared drafts and server previews. Reopening shows the normalized path; changing source resets it. Browser regression types a nested list path character by character and checks its resolved value, edits, reopening and source reset.

JSM omits repeat when escalation repeats are disabled. Read-back and drift comparison accept an absent repeat field only for an expected escalation with waitInterval: 0 (the provider disabled representation). Present repeat objects and enabled repeats remain strictly compared. Fake-server regressions reproduce omission, recovery without a checkpoint, idempotence and detection of externally enabled repeats. No overwrite policy is needed to recover an otherwise matching escalation.

Sync immediately shows submitting feedback, then stays disabled for an authorized pending job as well as an active publication lease. Pending publication has a visible queue message. The UI polls every two seconds while queued or leased and keeps the twenty-second idle refresh; expired leases remain manually recoverable. A successful sync request updates local queue status before refreshing the workspace. Browser regression holds the queued response and verifies disabled state and automatic completion without reload.

Generated escalation steps support Everyone on the team in addition to schedules and people. The optional team_all flag defaults to false for existing drafts; team recipients do not resolve an individual binding. They compile to Recipient::TeamAll and JSM uses the configured workspace integration team ID with notifyType=all. Delay and acknowledgment condition remain explicit. Generator, wire-contract and browser regressions cover the 30-minute team step and legacy recipients.

Layered generation map and value provenance

The read-only map uses three separated horizontal bands: variables, generators, resources. Generator columns follow saved execution order; variables sort by first consumer, and generated resources occupy their producer column. Manual resources are included in the resource band with explicit manual attribution. Shapes distinguish bands; resource colors distinguish schedules, escalations and routes. Expanding output groups retains these bands.

Server preview provides binding provenance using the same resolver as generation: source bindings, iteration sources, lookup keys, list items, intermediate path values, final values, node identities and errors. The browser presents this captured snapshot rather than re-evaluating scheduling logic. An inspector popover supports history, source navigation and per-pass examples. Dotted address segments are independently colored, with neutral separators; quoted keys containing dots remain intact. Generated names retain separate bound and literal affix segments, and resource fields link to their binding or explicit default/manual origin. Clicking the map backdrop closes it; clicks on the canvas only clear selection. Tests cover provenance, layout, manual resources, source exploration and safe text rendering.

The value explorer renders source traversal as ordered monospace steps with neutral arrows and a selected-step indicator. Source navigation is a separate footer action; producer fields are independent choices rather than sequential path steps. Browser coverage checks stage values and separate source navigation.

Portable generation configuration

meguri/v2 defines compact JSON import/export (with legacy v1 import support) for variables and generators only. Merge by name retains omitted inputs; nested identity keys preserve resource identity across editing and repeated imports. People transfer by email, manual resource references by exact name, and generated links by generator/block. The server provides a read-only parsed/expanded preview and revalidates before saving the shared draft with workspace and draft revisions. Provider writes require a later explicit Sync. Tests cover cross-workspace remapping, repeated imports, unknown fields, scoped permissions, stale previews, email contacts, parse errors and browser application.

Compact portable configuration

meguri/v2 is the default export: ordinary JSON variables, typed wrappers only for special scalar types, compact binding references, flattened output fields, readable output keys and omitted semantic defaults. The v1 importer remains supported. Inactive binding metadata must never be exported. Human output keys persist in shared drafts; list entries reconcile by unique name/value first and position for unmatched edits. The preview remains authoritative for identity-changing edits. Regression checks compare expanded resources, repeat imports and actual size reductions.

The Import / Export dialog includes a localized Guide tab with a complete compact v2 reference, defaults, binding scope, operations, limits, merge semantics and a runnable example covering all output types. Copy guide copies every section as Markdown for hand editing or LLM use, without workspace data. Switching tabs preserves import text and does not apply it. Browser checks verify copying, sample preview, tab preservation and mobile layout.

Generation steps preserve backward-compatible team_all and team_random flags, mutually exclusive with user. Team recipients ignore stale person/schedule bindings. The compiler emits Recipient::TeamRandom; JSM publishes it as a team recipient with notifyType: random.

Site control plane

site owns global authorization, section-versioned settings, account access flags and an installation audit stream. Site mutations compare a global control revision and the actor role inside a single PostgreSQL statement; workspace metadata also compares its own revision. Site permissions never bypass workspace membership. Account suspension does not mutate historical scheduling state. Authentication configuration is stored as a typed section, but is intentionally not connected to login enforcement yet. A future identity adapter will consume the section after secure OIDC execution is implemented.

Approval calculation is shared by replacement and absence preview/save. Only actual workspace account participants other than the requester are approvers; unlinked email contacts never gain login, membership or privileges. Empty approval sets apply immediately through the same validated, atomic history/audit/staged-sync boundary, without forced_by. Requests involving a contact may be submitted on its behalf with EditSchedules; changes solely between other account holders retain the force requirement. Preview exposes the exact approval set for truthful UI status.

Account identity stores required unique normalized email in the user row. The domain User embeds its canonical Person (same ID); workspace Person records are snapshots, not independently editable account profiles. Directory, provider identity resolution and portable configuration use this email. Editing a linked person cannot change account identity; profile changes update current snapshots atomically while historical snapshots remain intact. Password signup never claims planning contacts merely by email.

The preparatory contact/account migration runs before account email enforcement on legacy databases. An explicit deployment email mapping authorizes association only in workspaces where the account is already a member. It rewrites current scheduling references, person-valued generator inputs, shared drafts and explicit provider identity keys, retaining resource IDs and old history/publication checkpoints. Missing/ambiguous mappings, conflicting provider identities or collapsed invalid schedules/replacements block the repair before writes. Already email-migrated installations are skipped. Regression coverage includes migration ordering, retry after failure, generator drafts and unchanged access/history.