21 KiB
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 queued when older runs succeed or fail. Jobs retry with bounded backoff; fifteen-minute reconciliation extends the horizon and detects drift. 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
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 with exact timestamps (maximum 500 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):
- https://developer.atlassian.com/cloud/jira/service-desk-ops/rest/v2/api-group-schedules/
- https://developer.atlassian.com/cloud/jira/service-desk-ops/rest/v2/api-group-schedule-rotations/
- https://developer.atlassian.com/cloud/jira/service-desk-ops/rest/v2/api-group-schedule-overrides/
- https://developer.atlassian.com/cloud/jira/service-desk-ops/rest/v2/api-group-escalations/
- https://developer.atlassian.com/cloud/jira/service-desk-ops/rest/v2/api-group-routing-rules/
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. Each rule owns a vertical column of iterations, conditions and outputs. Dashed secondary edges show data dependencies rather than execution order. Repeated output groups use offset background cards; decorative layers follow dragging and do not receive selection. Browser checks verify rule ordering, aligned output columns and stack movement.
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.