{
  "generatedAt": "2026-09-27T00:00:00.000Z",
  "seeds": [
    {
      "id": "dial",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Dial",
      "dir": "calendly",
      "category": "scheduling",
      "categoryLabel": "Scheduling",
      "spine": {
        "id": "scheduling",
        "record": "booking against an availability rule"
      },
      "replaces": [
        {
          "name": "Calendly",
          "edition": null
        }
      ],
      "version": "0.1.0",
      "oneLiner": "An invitee-first, self-hosted scheduler for one business or independent host.",
      "summary": "An invitee-first, self-hosted scheduler for one business or independent host.",
      "scope": "",
      "maturity": "runtime-spine-public-port",
      "clauseCount": 69,
      "clauses": [
        {
          "id": "AUTH-001",
          "title": "Public authority.",
          "text": "Supersedes `BASE-ACCESS-004` for the paths listed in `seed.json → deploy.publicPaths` and for no others. Those paths authorise the caller by the capability material named in `AUTH-002` to `AUTH-005` instead of an operator session, each capability grants exactly the one action or record it names, and a request carrying no valid capability material changes nothing. Every other mutation requires a valid operator session and a server-side ownership check."
        },
        {
          "id": "AUTH-002",
          "title": "Booking nonce.",
          "text": "Reaching the details step for a selected slot, and reaching the review step of an alternative request, each issue a single-use nonce of at least 256 bits from a cryptographically secure random source, bound to the event type and to either the selected slot or the composed alternative request, and expiring 30 minutes after issue. The nonce is not bound to a browser: possession of an unexpired, unconsumed nonce is what authorises the confirmation. A confirmation whose nonce is missing, unknown, expired, or bound to different values creates nothing and answers per `PUBLIC-003`; a nonce that has already been consumed is answered by `BOOK-003`."
        },
        {
          "id": "AUTH-003",
          "title": "Booking access.",
          "text": "A booking is readable and changeable from a public surface only through the manage token defined in `BOOK-006`. A booking ID, an invitee email address, or any other guessable identifier grants access to no booking data."
        },
        {
          "id": "AUTH-004",
          "title": "Calendar authorisation callback.",
          "text": "The calendar OAuth callback accepts only signed state that is bound to the operator session that started the connection and was issued less than 10 minutes earlier. State that is missing, unsigned, mismatched, replayed, or expired stores no credential, connects no account, and returns 400."
        },
        {
          "id": "AUTH-005",
          "title": "Provider callbacks.",
          "text": "A calendar or mail provider callback is accepted only when the provider's own authentication passes — its registered channel identity or its request signature — together with the secret configured for that provider. A callback that fails the check returns 401 and changes nothing."
        },
        {
          "id": "OPER-001",
          "title": "Equal operators.",
          "text": "Every operator can see and act on the same host profile, event types, availability, calendar connections, bookings, and requests. There are no roles."
        },
        {
          "id": "OPER-002",
          "title": "Operator management.",
          "text": "Operators are invited to, and removed from, the sign-in allow-list in settings. Removing an operator ends their sessions within 60 seconds and changes no record they created, including the calendar connection they authorised. *Policy: `operator.management.v1`; default: any operator may invite or remove another, the operator configured at setup can be removed only by themselves, and the last remaining operator cannot be removed.*"
        },
        {
          "id": "OPER-003",
          "title": "Booking workspace.",
          "text": "Operators can list bookings filtered by the lifecycle states of `BOOK-004`, and independently by whether the start instant is in the future or the past, by event type, by date range, and by whether any external delivery has failed. Case-insensitive substring search over invitee name and email address returns within 500ms. Lists are sorted by start instant."
        },
        {
          "id": "OPER-004",
          "title": "Booking record.",
          "text": "A booking record shows the event type, every invitee answer and custom-field value, the start and end in both the host's and the invitee's IANA zones, the location or meeting link, the history from `BOOK-010`, the separate state of the calendar event, the meeting link, and each email, and every recorded failure with its `CAL-005` or `NOTIFY-005` reason."
        },
        {
          "id": "PROFILE-001",
          "title": "Host profile.",
          "text": "Operators edit the host's name, title, biography, avatar, IANA timezone, focus areas, and social-proof entries, within the counts and lengths in `seed.json → limits`. A social-proof entry is a quotation and an attribution typed by an operator: Dial collects no invitee feedback and publishes no rating, score, or count it did not receive from an operator. There is exactly one host timezone per deployment and it is the zone every other clause means by \"the host's zone\". The in-app preview renders the same content as the public page."
        },
        {
          "id": "PROFILE-002",
          "title": "Public profile.",
          "text": "The host profile is served at `/book/<handle>`, where the handle is set by an operator. It shows the configured host content and every published, listed event type, and never a draft, unlisted, or disabled one. An operator can change the handle; the previous handle then answers per `PUBLIC-005`."
        },
        {
          "id": "PROFILE-003",
          "title": "Avatar asset.",
          "text": "Supersedes `BASE-INPUT-003` for the `/media/` prefix only. The avatar is an upload within the size and type limits in `seed.json → limits`, stored in R2 and served under `/media/` with its declared content type and inline rather than with a download disposition, because it renders as an image on the public profile and event pages. That prefix is the public delivery channel the baseline requires a product contract to name; every other upload rule still applies, and no other upload in this product is publicly readable."
        },
        {
          "id": "EVENT-001",
          "title": "Event type lifecycle.",
          "text": "Operators create, edit, publish, disable, and delete event types. An event type is draft, published, or disabled, and a published one is either listed on the public profile or unlisted and reachable only by its own URL. Each carries a title, description, a single duration of 5 to 480 minutes, colour, location, and a slug unique among event types, and is served at `/book/<handle>/<slug>`. A slug can be edited; the previous slug is not retained and then answers per `PUBLIC-005`, while existing bookings and their manage links are unaffected."
        },
        {
          "id": "EVENT-002",
          "title": "Scheduling constraints.",
          "text": "Each event type configures its recurring availability, date-specific overrides, slot start increment of 5, 10, 15, 30, or 60 minutes, minimum notice of 0 minutes to 7 days, maximum booking horizon of 1 day to `seed.json → limits.availabilityLookaheadDays`, before and after buffers of 0 to 120 minutes each, and an optional maximum number of bookings per host day, where a host day is one calendar date in the host's timezone. A new event type starts at a 30-minute duration, a 15-minute increment, 4 hours' minimum notice, a 60-day horizon, no buffers, and no host-day limit."
        },
        {
          "id": "EVENT-003",
          "title": "Invitee questions.",
          "text": "Each event type has ordered booking questions of type text, textarea, single choice, multiple choice, or checkbox, each required or optional, within the question and option counts in `seed.json → limits`. Choice options have stable values and editable labels, and a submitted option not configured on the event type is rejected. A name and a syntactically valid email address are always required. *Policy: `booking.questions.v1`; default: name and email, plus an optional agenda of at most 500 characters, on every new event type.*"
        },
        {
          "id": "EVENT-004",
          "title": "Location.",
          "text": "An event type uses exactly one location: a Google Meet conference provisioned per `CAL-003`, a fixed link the operator supplies, a host phone number the invitee calls, or an in-person address. The public event page shows only the location's label; the link, number, or street address appears only after confirmation — in the receipt, the invitee's email, and the calendar event. Dial never collects an invitee phone number."
        },
        {
          "id": "EVENT-005",
          "title": "Removing an event type.",
          "text": "Supersedes `BASE-DATA-003` for event types only: disabling or deleting an event type removes its public page and all its future availability but does not cascade to its bookings, which keep their times, manage links, calendar events, history, and their own copy of the event title, duration, and location. Invitee data is erased under `DATA-002`, not by deleting an event type."
        },
        {
          "id": "EVENT-006",
          "title": "Publication readiness.",
          "text": "An event type can be published only when it has at least one open availability window and when its transactional-mail configuration, its destination calendar, and, where its location is provider-generated, its conferencing capability each passed a connection check within the last 24 hours or since they last changed. A connection check is a live read of the destination calendar, a credential probe against the mail provider, and, where conferencing is used, a capability check against the conferencing provider; an operator can run it on demand. The operator sees each unmet requirement named. A requirement that becomes unhealthy after publication does not unpublish the event type: the connection state changes under `CAL-006` and public booking behaves under `AVAIL-004`."
        },
        {
          "id": "HOME-001",
          "title": "Owner front door.",
          "text": "The deployment root `/` is a sessionless owner-facing landing, not a vendor marketing page. It renders the deployment-configured owner identity and single-line description, and links to the host's public booking profile when one is available. Its `Powered by Dial · runeditrun.com` footer credit is a static link controlled by deployment configuration, enabled by default and omitted when disabled."
        },
        {
          "id": "PUBLIC-001",
          "title": "Event presentation.",
          "text": "A published event page names the host and the event and shows its description, duration, public location label, the invitee's selected timezone, the recommended times of `AVAIL-007`, every other valid slot in the displayed week, week-by-week navigation bounded by today and the booking horizon, the change window the `booking.reschedule.v1` and `booking.cancellation.v1` policies currently enforce, and, when the event type enables it, the alternative-request path."
        },
        {
          "id": "PUBLIC-002",
          "title": "Invitee-first flow.",
          "text": "An invitee views availability and completes a booking without an account and without signing in. Before confirmation the flow asks only for the event type's configured questions, names who receives the answers — the host and the connected calendar provider — and links the privacy notice URL configured for the deployment."
        },
        {
          "id": "PUBLIC-003",
          "title": "Progress and recovery.",
          "text": "The public flow has distinct choose-time, details, and confirmation states, each at its own URL, and reloading or navigating back preserves a still-valid selection. The selection and its nonce are carried in the URL and in the browser's session storage; the public flow sets no cookie. An expired nonce, or an expired or rotated manage token, returns 410; an unknown or mismatched one returns 404. Both render a page that offers a fresh start and that contains no booking, invitee, or sample data."
        },
        {
          "id": "PUBLIC-004",
          "title": "Embed.",
          "text": "Operators copy an asynchronous script tag naming a public event slug and `inline` or `modal` mode. Inline mode renders where the tag sits; modal mode opens only from an element whose `data-dial-embed` attribute names that embed. The initial script is under 50KB gzipped, and the embedded flow offers the same booking, reschedule, and cancel actions as the standalone page."
        },
        {
          "id": "PUBLIC-005",
          "title": "Unavailable pages.",
          "text": "An unknown, draft, disabled, deleted, or renamed profile or event URL returns 404 and reveals nothing about whether a record ever existed there. This governs profile and event URLs only; manage-token responses are governed by `PUBLIC-003`, where a rotated or expired token deliberately returns 410 so the invitee knows to ask for a new link. A published event type with no valid slot inside its horizon still returns 200, says so, and offers the alternative-request path when the event type enables it."
        },
        {
          "id": "PUBLIC-006",
          "title": "Abuse.",
          "text": "A public request refused by the rate limits of `BASE-ACCESS-003` returns 429 and the flow keeps the invitee's selection and answers so the same person can retry without re-entering them. An operator can block an email address or a domain; a submission from a blocked address is refused with a message that does not say it was blocked, and creates no booking, request, notification, or calendar event."
        },
        {
          "id": "AVAIL-001",
          "title": "Schedule and overrides.",
          "text": "Recurring availability is a set of wall-clock windows per weekday in the host's IANA timezone. A date override replaces the recurring windows for one host date and may open or close that whole date. Editing a schedule or an override changes future availability only; it never alters a confirmed booking."
        },
        {
          "id": "AVAIL-002",
          "title": "Valid slots.",
          "text": "A slot is offered only when every one of these holds: its start falls on the event type's start increment measured from the start of its open window; the full duration fits inside that one window; the start is within the booking horizon and beyond the minimum notice; its host day is under the event type's booking limit; and it overlaps no booking in the `confirmed` state for this host on any event type and no busy interval from any selected conflict calendar, an external event the provider marks free being ignored. Anything else is not offered."
        },
        {
          "id": "AVAIL-003",
          "title": "Buffers.",
          "text": "The event type's before and after buffers extend the interval that must be free around a candidate slot, over both confirmed Dial bookings and external busy intervals. A buffer never changes the invitee's start or end time, the duration shown anywhere, or the times on the calendar event and invitation."
        },
        {
          "id": "AVAIL-004",
          "title": "Freshness and failure.",
          "text": "Availability is recomputed when the event page or a week loads. Changing the invitee's timezone re-renders and re-ranks the same slots and needs no new calendar read, because slot validity does not depend on the invitee's zone. A displayed slot may rest on cached provider busy data no more than 5 minutes old; the final check in `BOOK-001` uses provider data no more than 60 seconds old. Every selected conflict calendar is required, and a provider read that does not complete within 5 seconds counts as unreadable. If a required calendar is unreadable at either point, Dial offers and confirms no slot for that host and tells the invitee that booking is temporarily unavailable, rather than offering unverified time."
        },
        {
          "id": "AVAIL-005",
          "title": "Timezones.",
          "text": "Every invitee-facing time is rendered in the invitee's selected timezone, which starts as the browser's IANA zone when the browser reports one and as the host's zone when it does not. The zone is resolved in the browser and slot instants are fetched in UTC, so no visitor-derived value reaches the server on page load, as `BASE-PUBLIC-001` requires: the invitee's zone is sent only with a request their own action causes — changing the zone, moving week, or selecting a slot. The invitee can change the zone at any point before confirmation, every rendered time carries its zone name and UTC offset, and the details and confirmation surfaces also show the host's local time. A confirmed booking stores the UTC instant and both the host's and the invitee's IANA zone identifiers."
        },
        {
          "id": "AVAIL-006",
          "title": "Daylight saving time.",
          "text": "A local time that does not exist during a forward transition is never offered. Repeated wall-clock times during a backward transition are shown with distinct UTC offsets, so each offered time maps to exactly one instant. A DST transition between confirmation and the meeting never moves a booking's stored UTC instant, and the host and invitee renderings each follow their own zone's rules."
        },
        {
          "id": "AVAIL-007",
          "title": "Recommendations and filters.",
          "text": "At most 5 recommended times are shown, each a slot already valid under `AVAIL-002` and each showing the reason it was chosen. The ordering is deterministic: the same inputs always produce the same order. The first render of a page ranks in the host's zone alone; the ranking is recomputed once the invitee's zone reaches the server under `AVAIL-005`. An invitee-facing filter only narrows or reorders slots already valid under `AVAIL-002` and can never surface one that is not. *Policy: `availability.recommendation.v1`; default: slots fully inside 09:00–17:00 in both the host's and the invitee's selected zone first, then the larger sum of contiguous free minutes immediately before and after the slot, then the earlier UTC start.*"
        },
        {
          "id": "AVAIL-008",
          "title": "Response time.",
          "text": "Loading one week of availability for one event type returns within 2 seconds."
        },
        {
          "id": "AVAIL-009",
          "title": "Operator diagnostics.",
          "text": "For any time an operator evaluates, Dial names which rule excluded it — schedule, date override, start increment, minimum notice, horizon, buffer, host-day limit, an existing Dial booking, or a conflict-calendar busy interval — without disclosing external event detail that `CAL-002` does not retain."
        },
        {
          "id": "BOOK-001",
          "title": "Final validation.",
          "text": "Confirming a booking revalidates every answer against the event type's current questions and rechecks the selected slot under `AVAIL-002` against a fresh conflict-calendar read. Any failure creates no booking, no calendar event, and no email, and names the reason: an invalid or missing answer returns 400, and a slot that is no longer valid returns 409."
        },
        {
          "id": "BOOK-002",
          "title": "Atomic claim.",
          "text": "Every slot admits at most one booking in the `confirmed` state. Claiming the slot and recording its calendar, meeting, and notification commands happen in one transaction that either commits entirely or leaves nothing behind. Of any number of concurrent confirmations for the same slot exactly one commits; every other returns 409 with a `slot_taken` code and is returned to the choose-time step with availability recomputed."
        },
        {
          "id": "BOOK-003",
          "title": "Idempotent confirmation.",
          "text": "Replaying a confirmation with an already-consumed nonce and an identical payload returns the stored result of the original booking and creates no second booking, calendar event, meeting link, or email. Replaying it with a different payload returns 409 and creates nothing."
        },
        {
          "id": "BOOK-004",
          "title": "States and occurrences.",
          "text": "A booking is confirmed, cancelled, or rescheduled, and has one or more occurrences of which exactly one is current: a reschedule closes the current occurrence and opens a new one under the same booking ID and iCalendar UID. The calendar event, the meeting link, and each email carry their own pending, succeeded, or failed state; none of them changes the booking's state, and no external failure silently cancels or moves a booking."
        },
        {
          "id": "BOOK-005",
          "title": "Confirmation receipt.",
          "text": "Within 2 seconds of a successful claim the invitee is shown the event, the start and end in their own zone and in the host's, the duration, the host, the location or its pending state, the calendar file of `CAL-009`, and the manage link of `BOOK-006`. The join control is enabled only when a provisioned joining URL is stored on the booking; while `CAL-003` is still pending it is disabled and labelled as pending."
        },
        {
          "id": "BOOK-006",
          "title": "Manage link.",
          "text": "Every confirmed booking has a manage link whose token carries at least 256 bits from a cryptographically secure random source and grants view and change access to that one booking, without creating an invitee account. The token is transmitted only to the invitee, in their own email and on their confirmation page; operator surfaces show the booking but never the token, offering an action that copies or resends the link instead. A reschedule keeps the token, which always resolves to the booking's current occurrence. The token expires 90 days after the booking's start instant, and an operator can rotate it, which invalidates the previous token immediately. A request with an expired, rotated, or unknown token answers per `PUBLIC-003` and discloses no booking data."
        },
        {
          "id": "BOOK-007",
          "title": "Invitee rescheduling.",
          "text": "The holder of a booking's manage link can move it to another slot on the same event type. The new slot is validated under `BOOK-001` with only this booking's own occurrence excluded from internal conflicts, then claimed under `BOOK-002` before the old slot is released; if any step fails, the original booking is unchanged and still confirmed. *Policy: `booking.reschedule.v1`; default: any number of reschedules, allowed until the booking's start.*"
        },
        {
          "id": "BOOK-008",
          "title": "Invitee cancellation.",
          "text": "The holder of a booking's manage link can cancel it with an optional reason. Cancellation cannot be reversed, releases the slot and its buffers within 5 seconds and before the next availability computation, and repeating it returns the same result without sending a second notification. *Policy: `booking.cancellation.v1`; default: allowed until the booking's start.*"
        },
        {
          "id": "BOOK-009",
          "title": "Operator changes.",
          "text": "An operator can reschedule or cancel any confirmed booking under `BOOK-002`, `BOOK-007`, `BOOK-008`, `BOOK-010`, `CAL-003`, and `NOTIFY-004`. The reschedule and cancellation policies constrain invitees only: an operator may also act on a booking that has already started, and may reschedule onto a time outside published availability after confirming that override explicitly, but never onto a time that overlaps another confirmed booking."
        },
        {
          "id": "BOOK-010",
          "title": "History.",
          "text": "Rescheduling records the original occurrence as rescheduled and links it to its replacement. Every creation, cancellation, and reschedule records who acted — the manage-link holder, the named operator, or the calendar provider under `CAL-008` — the time, the previous and new start, and any reason given."
        },
        {
          "id": "BOOK-011",
          "title": "Attendance.",
          "text": "After a booking has ended an operator can mark it attended or a no-show and add a note. The mark records the operator and the time, appears in the booking record and the export, changes no lifecycle state and no calendar event, and sends the invitee nothing."
        },
        {
          "id": "BOOK-012",
          "title": "Operator-created booking.",
          "text": "An operator can create a booking, for an invitee whose name and email address they enter, on any slot valid under `AVAIL-002`, optionally overriding the minimum notice and the horizon but never the conflict and buffer checks. The invitee then receives the ordinary `NOTIFY-001` confirmation and manage link, and `BOOK-010` records the operator as the actor."
        },
        {
          "id": "CAL-001",
          "title": "Calendar connection.",
          "text": "An operator connects the host's Google Calendar account through OAuth under `AUTH-004`, selects up to `seed.json → limits.maxConflictCalendars` calendars whose busy intervals block availability, and selects exactly one calendar to receive Dial's bookings. There is exactly one connection; it belongs to the deployment rather than to the operator who authorised it, reconnecting replaces it and keeps the destination selection, and an operator can disconnect. After a disconnect, sync stops, the connection shows as disconnected, published event types keep their pages but offer no slot under `AVAIL-004`, and existing bookings and their history are unchanged. The implementation is replaceable through `calendar.provider.v1`."
        },
        {
          "id": "CAL-002",
          "title": "Busy privacy.",
          "text": "A calendar read retains only the identifiers, time bounds, transparency, and deletion state needed for conflict detection and sync. Titles, descriptions, attendees, and locations from conflict calendars are never stored, and so are never shown to an invitee or to an operator."
        },
        {
          "id": "CAL-003",
          "title": "Managed event.",
          "text": "Within 60 seconds of a successful claim the destination calendar holds one event for the booking carrying the event title, the booking's start and end with explicit timezone data, a stable iCalendar UID, the configured location or a Google Meet conference provisioned for this booking, and the invitee as an attendee so the provider's own invitation and updates also reach them — or the booking's calendar state is pending and `CAL-005` applies. An unsuccessful or ambiguous result is handled under `CAL-004` and `CAL-005`."
        },
        {
          "id": "CAL-004",
          "title": "Idempotent side effects.",
          "text": "Every calendar and meeting command carries the booking and occurrence IDs as its provider idempotency key. After a timeout, a lost response, or any other ambiguous result, the next attempt looks the object up by that key and updates it instead of creating a second one."
        },
        {
          "id": "CAL-005",
          "title": "Provider failure.",
          "text": "A transient calendar or meeting failure is retried up to 5 times over 1 hour. Until it succeeds the operator and the invitee receipt show the side effect as pending; after the retries are exhausted the operator sees a typed sanitised error code, safe remediation, and a retry action, while the invitee sees only that calendar setup is incomplete and that they should contact the host. The booking stays confirmed, and raw provider errors are neither persisted nor rendered."
        },
        {
          "id": "CAL-006",
          "title": "Connection health.",
          "text": "Revoked credentials, a refused token refresh, an invalid sync cursor, a push channel that cannot be renewed, or exhausted provider quota puts the connection in an unhealthy state that operators see in settings by name and that raises `calendar.connection-health-changed.v1`. An invalid cursor or a lapsed channel triggers a full resync before the next availability computation. While a required conflict or destination calendar is unreadable, public booking fails closed under `AVAIL-004` and no event type can be published under `EVENT-006`."
        },
        {
          "id": "CAL-007",
          "title": "Provider callbacks.",
          "text": "Calendar push channels are renewed before they expire. A callback authenticated under `AUTH-005` is processed independently of arrival order: duplicate or out-of-order delivery cannot duplicate a booking change or undo a newer one, and a callback describing a state older than the one held is discarded."
        },
        {
          "id": "CAL-008",
          "title": "External deletion and edits.",
          "text": "Deleting Dial's managed event in the destination calendar cancels the booking exactly once and sends the ordinary cancellation notices of `NOTIFY-004`. Any other external edit to the event's time or location is recorded as a sync conflict on the booking showing both times; the operator resolves it by restoring the managed event to the canonical time or by cancelling under `BOOK-009`, and Dial never adopts such an edit by itself. *Policy: `calendar.external-deletion.v1`; default: deletion cancels the booking.*"
        },
        {
          "id": "CAL-009",
          "title": "Calendar file.",
          "text": "The confirmation and manage pages offer an `.ics` download for the booking carrying its stable UID and current occurrence. A reschedule increments that file's sequence and a cancellation marks the occurrence cancelled, so an invitee's calendar updates the same entry rather than gaining an unrelated one. The download makes no request to any provider not declared in `seed.json → externals`."
        },
        {
          "id": "REQUEST-001",
          "title": "Compose.",
          "text": "Where an event type enables it, an invitee can select future dates and time windows within the counts in `seed.json → limits`, whose starts precede their ends and which do not overlap each other, together with their IANA timezone, a name, an email address, and an optional note of at most 500 characters, and can review the exact request before submitting. A request that omits a date, a window, a name, or a valid email address, or that contains an overlapping or reversed window, is rejected with the offending field named and stores nothing."
        },
        {
          "id": "REQUEST-002",
          "title": "Submit.",
          "text": "A valid request is recorded once, acknowledged to the invitee on screen, and notified to operators within 60 seconds. The acknowledgement states the response expectation the operator configured, or, where none is configured, only that the host will follow up by email. Replaying the same nonce returns the existing request. A request reserves no time and creates no booking."
        },
        {
          "id": "REQUEST-003",
          "title": "Operator workflow.",
          "text": "Operators can list requests by submitted, handled, or closed state, open the invitee's windows rendered in both the invitee's and the host's zone, mark one handled or closed, and create a booking for an agreed time. That booking runs the ordinary `BOOK-012` path and marks the request handled."
        },
        {
          "id": "REQUEST-004",
          "title": "Routing.",
          "text": "A new request notifies every operator unless the routing policy selects a subset or suppresses email; suppressing email never suppresses the in-app record. *Policy: `availability-request.routing.v1`; default: every operator.*"
        },
        {
          "id": "REQUEST-005",
          "title": "Invitee updates.",
          "text": "Submitting sends the invitee one acknowledgement email restating the requested windows in their own zone. Marking a request handled or closed sends the invitee exactly one further email; the closed one says the host cannot offer a time, and neither is sent twice however often the state is set again."
        },
        {
          "id": "NOTIFY-001",
          "title": "Booking confirmation.",
          "text": "Within 60 seconds of confirmation Dial emails, through `mail.sender.v1`, the invitee and every operator: the event, the time in each recipient's relevant zone, the duration, the answers, and the current calendar state. The invitee's copy also carries their manage link and the `.ics` file of `CAL-009`; no operator copy contains the manage token. Where the meeting link is still pending the email says so instead of inventing one. An operator can mute booking mail — confirmations, changes, and reminders — for themselves in settings; the invitee's copy is never suppressed."
        },
        {
          "id": "NOTIFY-002",
          "title": "Link completion.",
          "text": "When a pending joining link becomes available Dial updates the managed calendar event and sends the invitee exactly one link-ready email, however many times the completion job runs."
        },
        {
          "id": "NOTIFY-003",
          "title": "Reminders.",
          "text": "A confirmed future booking schedules invitee and operator reminders carrying the event, the time in the recipient's zone, the location or join link, and, for the invitee, the manage link. A reminder is sent within 5 minutes of its scheduled offset. Rescheduling replaces the scheduled times and cancellation removes them, so no reminder is ever sent for a time the booking no longer holds. *Policy: `reminder.schedule.v1`; default: 24 hours and 1 hour before the start, scheduling only the offsets still in the future at confirmation or reschedule.*"
        },
        {
          "id": "NOTIFY-004",
          "title": "Changes.",
          "text": "A reschedule or a cancellation sends the invitee and every operator exactly one email naming who made the change, the previous time, the new time or the cancellation reason, and the current calendar state."
        },
        {
          "id": "NOTIFY-005",
          "title": "Delivery failure.",
          "text": "A transient email failure is retried up to 5 times over 1 hour. A permanent failure or a bounce is marked failed on the booking or request with a typed sanitised reason and safe remediation, and can be resent without duplicating a delivery that already succeeded. Raw provider errors are neither persisted nor rendered."
        },
        {
          "id": "NOTIFY-006",
          "title": "No mail loops.",
          "text": "Product mail is sent from a no-reply address. Mail delivered to that address is discarded and never creates a booking, a request, or a notification."
        },
        {
          "id": "NOTIFY-007",
          "title": "Connection alerts.",
          "text": "Within 60 seconds of the calendar connection becoming unhealthy under `CAL-006`, every operator is emailed once, naming the cause and the reconnect action, because public booking is failing closed meanwhile. This alert is operational and the mute of `NOTIFY-001` does not suppress it. No further alert is sent for that connection until it has recovered, and recovery is announced the same way."
        },
        {
          "id": "DATA-001",
          "title": "Export contents.",
          "text": "Supersedes `BASE-DATA-001` for OAuth credentials only. The export contains every operator, host profile, event type, question, availability rule and override, calendar-connection metadata and sync state, booking and occurrence, invitee answer and custom-field value, attendance mark, alternative request, delivery attempt, setting, and audit entry as JSON, plus every uploaded asset in its original form. Access and refresh tokens are credentials and are never exported: after an import the calendar connection is unhealthy under `CAL-006` until an operator re-authorises it."
        },
        {
          "id": "DATA-002",
          "title": "Erasure targets.",
          "text": "An operator can delete a booking or an alternative request. Deleting one removes its invitee answers, notes, attendance mark, and uploaded content, invalidates its manage token, and cancels its managed calendar event without sending the invitee a Dial email; the audit entries survive with the invitee reference anonymised, as `BASE-DATA-003` requires. Deleting every booking and request carrying an email address erases that person from the deployment, since Dial keeps no separate invitee record."
        }
      ],
      "hasReadme": false,
      "hasBaseline": true,
      "nonGoals": [
        "Multiple hosts or workspaces",
        "Roles and permissions",
        "Team round-robin or collective scheduling",
        "Group events, classes, waitlists, or resource capacity",
        "Additional invitee guests on a booking",
        "Bookings that wait for host approval before they are confirmed",
        "Payments, deposits, refunds, or booking fees",
        "Recurring booking series or scheduling polls",
        "Marketplace discovery, ratings, or invitee-submitted reviews",
        "SMS, WhatsApp, phone automation, CRM, campaigns, or marketing automation",
        "Automatic adoption of arbitrary external-calendar time or location edits",
        "AI routing or ranking",
        "Native mobile apps"
      ],
      "externals": [
        {
          "name": "google-calendar",
          "required": true,
          "requiredWhen": "",
          "reason": "A scheduler must read the host's real busy time and place accepted bookings on the calendar people already use; Cloudflare has no calendar primitive. EVENT-006 blocks publishing any event type without a healthy destination-calendar connection, and AVAIL-004 fails booking closed without a readable calendar.",
          "data": [
            "host OAuth identity",
            "calendar identifiers",
            "booking event details",
            "invitee name and email"
          ],
          "adapters": [
            "calendar.provider.v1"
          ]
        },
        {
          "name": "google-meet",
          "required": false,
          "requiredWhen": "",
          "reason": "The prototype's default location is a real Google Meet conference, which requires provider-side conference creation. EVENT-004 also lets an event type use a fixed link, a phone number, or an in-person address instead, so a deployment can run with no event type ever using Meet.",
          "data": [
            "booking correlation ID",
            "event title",
            "start and end instant"
          ],
          "adapters": [
            "calendar.provider.v1"
          ]
        },
        {
          "name": "resend",
          "required": true,
          "requiredWhen": "",
          "reason": "Cloudflare does not provide transactional email delivery or bounce webhooks; confirmations and change notices must reach external inboxes. EVENT-006 also blocks publishing any event type without a healthy mail-provider connection.",
          "data": [
            "operator and invitee email",
            "booking or request content",
            "calendar attachment"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "estimated",
        "estimatedAt": "2026-08-31",
        "currency": "USD",
        "basis": "One host, one operator, 1,000 bookings per month, the default two reminders when both offsets apply, four to six transactional emails per unchanged booking, and usage inside Google Calendar's standard quota.",
        "lowMonthly": 20,
        "typicalMonthly": 25,
        "notes": "At this basis, Resend Pro is the $20 low case and adding Cloudflare Workers Paid gives the $25 typical case. Smaller deployments can fit both free allowances. More operators, changes, requests, or retries increase mail volume. Google Calendar standard use currently has no additional charge below its quota, but Google states over-quota billing is planned later in 2026.",
        "sources": [
          "https://developers.cloudflare.com/workers/platform/pricing/",
          "https://resend.com/pricing",
          "https://developers.google.com/workspace/calendar/api/guides/quota"
        ]
      },
      "extensionPoints": [
        "operator.route.v1",
        "operator.page.v1",
        "operator.navigation.v1",
        "operator.settings-section.v1",
        "job.handler.v1",
        "cron.handler.v1",
        "queue.consumer.v1",
        "operator.management.v1",
        "booking.questions.v1",
        "availability.recommendation.v1",
        "booking.reschedule.v1",
        "booking.cancellation.v1",
        "availability-request.routing.v1",
        "reminder.schedule.v1",
        "calendar.external-deletion.v1",
        "profile.updated.v1",
        "event-type.published.v1",
        "event-type.disabled.v1",
        "event-type.deleted.v1",
        "booking.confirmed.v1",
        "booking.rescheduled.v1",
        "booking.cancelled.v1",
        "availability-request.created.v1",
        "availability-request.handled.v1",
        "availability-request.closed.v1",
        "calendar.connection-health-changed.v1",
        "delivery.failed.v1",
        "operator.navigation.after.v1",
        "operator.dashboard.after.v1",
        "event-type.header.actions.v1",
        "event-type.settings.after.v1",
        "booking.list.row-actions.v1",
        "booking.detail.sidebar.after.v1",
        "booking.detail.actions.after.v1",
        "availability-request.detail.after.v1",
        "public.profile.after.v1",
        "public.event.before-booking.v1",
        "public.confirmation.after.v1",
        "settings.calendar.after.v1",
        "calendar.provider.v1",
        "mail.sender.v1"
      ],
      "limits": {
        "status": "estimated-until-load-tested",
        "estimatedAt": "2026-08-31",
        "maxOperators": 10,
        "maxEventTypes": 100,
        "maxFocusAreas": 6,
        "maxFocusAreaChars": 40,
        "maxSocialProofEntries": 3,
        "maxSocialProofChars": 280,
        "maxBookingQuestionsPerEventType": 25,
        "maxQuestionOptionsPerQuestion": 50,
        "maxAvailabilityOverridesPerEventType": 1000,
        "maxRequestDatesPerRequest": 10,
        "maxRequestWindowsPerRequest": 20,
        "maxConflictCalendars": 10,
        "availabilityLookaheadDays": 365,
        "maxBookings": 100000,
        "maxBookingsPerMonth": 10000,
        "concurrentSlotClaims": 50,
        "publicRequestsPerIpPerMinute": 120,
        "bookingMutationsPerIpPerMinute": 10,
        "alternativeRequestsPerIpPerHour": 5,
        "webhookRequestsPerProviderPerMinute": 600,
        "maxUploadBytes": 5242880,
        "allowedUploadTypes": [
          "image/jpeg",
          "image/png",
          "image/webp",
          "image/avif"
        ]
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target-until-automated-and-manual-audit"
      },
      "landing": "assets/seeds/dial/01-home-desktop.png",
      "shots": [
        "assets/seeds/dial/01-home-desktop.png",
        "assets/seeds/dial/02-profile-desktop.png",
        "assets/seeds/dial/03-preview-desktop.png",
        "assets/seeds/dial/06-operator-dashboard-desktop.png"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/dial",
        "release": "0.1.0",
        "manifestSha256": "8c620babfb252c10d7d12722754df475ae6a9c724340acd318fcaab881192413"
      }
    },
    {
      "id": "penny",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Penny",
      "dir": "canny",
      "category": "feedback",
      "categoryLabel": "Feedback",
      "spine": {
        "id": "feedback",
        "record": "request with attributed evidence"
      },
      "replaces": [
        {
          "name": "Canny",
          "edition": null
        }
      ],
      "version": "0.0.0-bootstrap",
      "oneLiner": "A self-hosted customer-feedback decision system that connects attributed evidence, accountable product decisions, customer-safe roadmaps and…",
      "summary": "A self-hosted customer-feedback decision system that connects attributed evidence, accountable product decisions, customer-safe roadmaps and releases, and requester outcomes for one B2B software business.",
      "scope": "",
      "maturity": "ui-port-read-spine",
      "clauseCount": 85,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Public feedback portal.",
          "text": "Penny serves the deployment's public feedback portal at `/`. It identifies the workspace and gives the owner's configured one-line portal description; public feedback, roadmap, and updates follow REQ-001, ROAD-006, and REL-010. It makes no vendor, pricing, testimonial, or metric claims and exposes no operator-only data."
        },
        {
          "id": "OPS-001",
          "title": "Equal operators.",
          "text": "Every operator can see and act on every account, evidence item, request, decision, roadmap item, and release except where a policy restricts an action. There are no roles."
        },
        {
          "id": "OPS-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. The operator configured at setup can be removed only by themselves. *Policy: `operator.management.v1`; default: any operator may invite or remove any other operator.*"
        },
        {
          "id": "OPS-003",
          "title": "Overview.",
          "text": "The operator home lists decisions that are ready or review due, evidence that is unreviewed, roadmap items that are Planned or In progress, and releases published in the last 30 days with their outcome totals. Every figure links to the records it counts, and a section with no records names what is absent and links to the action that creates one."
        },
        {
          "id": "OPS-004",
          "title": "Global search.",
          "text": "Operators can search request titles and descriptions, evidence wording, decision questions and rationales, roadmap item titles and summaries, release titles and bodies, requester display names and email addresses, and account names. The first page of results returns within 500ms."
        },
        {
          "id": "EVID-001",
          "title": "Capture.",
          "text": "Every portal submission creates exactly one evidence item in the same transaction (REQ-003). An operator can also record an evidence item, whose source is one of sales call, support conversation, interview, email, or a free-text label the operator supplies."
        },
        {
          "id": "EVID-002",
          "title": "Provenance.",
          "text": "Every evidence item stores its source, its original wording, the observed time, and the operator or portal submission that created it. The submitting requester or named speaker and the account are nullable, are set only from what the recorder supplies, and are never inferred."
        },
        {
          "id": "EVID-003",
          "title": "Review lifecycle.",
          "text": "New evidence is unreviewed. An operator can link it to a request or to a candidate bet in a decision brief (DEC-002), create a request or a decision from it, or mark it reviewed with a reason, and can return any reviewed item to unreviewed. Linking alone does not mark it reviewed."
        },
        {
          "id": "EVID-004",
          "title": "Non-destructive linking.",
          "text": "One evidence item can be linked to any number of candidate bets, and each link is recorded as supporting or contradicting that bet. Linking, unlinking, tagging, merging a request, and committing a decision never alter or remove an evidence item or any of its versions; only an operator deleting it does."
        },
        {
          "id": "EVID-005",
          "title": "Account context.",
          "text": "An account has a name, an internal owner, optional commercial fields for revenue, renewal date, and health, its requesters, its evidence in observed-time order, and its linked requests and decisions. Evidence recorded against an account appears on that account, and on every request and decision the same transaction linked it to, when that transaction commits."
        },
        {
          "id": "EVID-006",
          "title": "Private commercial data.",
          "text": "No field of an account (EVID-005), no evidence source attribution, and no internal note (REQ-032) appears in a portal response, on a public page, in page metadata, in a sitemap, or in the subject or body of a requester notification."
        },
        {
          "id": "EVID-007",
          "title": "Duplicate suggestions.",
          "text": "When an operator records evidence or a requester types a request title, Penny returns within 500ms up to 5 existing requests ranked by term overlap between the typed title and stored request titles and descriptions, and names the matched terms for each. A requester is shown published requests only. Matching is lexical and deterministic; Penny never links or merges without an explicit operator or requester action."
        },
        {
          "id": "EVID-008",
          "title": "Evidence attachments.",
          "text": "An operator can attach files to an evidence item within the limits in `seed.json` under BASE-INPUT-003. An evidence attachment is readable only by an operator session and is never served from a public path, whatever the publication state of a linked request."
        },
        {
          "id": "EVID-009",
          "title": "Tags.",
          "text": "Operators create, rename, merge, and delete tags and apply them to evidence items and requests. A tag is operator-only, filterable in every operator list, survives a request merge, and is included in the export."
        },
        {
          "id": "EVID-010",
          "title": "Versions.",
          "text": "Editing an evidence item's wording writes a new version and leaves every earlier version readable to operators. The item's own ID never changes."
        },
        {
          "id": "REQ-001",
          "title": "Public portal.",
          "text": "The portal serves published requests, a request detail page with its discussion, the public roadmap, and published releases. *Policy: `request.visibility.v1`; default: any visitor may read all four without signing in.*"
        },
        {
          "id": "REQ-002",
          "title": "Requester identity.",
          "text": "Submitting, voting, commenting, following, subscribing, and recording an outcome each require either an email address verified under REQ-023 or a host-signed identity under REQ-031. A portal response shows the requester's chosen display name and never their email address or account."
        },
        {
          "id": "REQ-003",
          "title": "Submission.",
          "text": "A requester submits a title, a problem description, an optional free-text proposed solution, an optional free-text impact statement, a board (REQ-018), and optional attachments. The submission atomically creates one unreviewed evidence item and one request awaiting moderation; the request is not public until an operator publishes it. *Policy: `request.moderation.v1`; default: an operator reviews every requester submission, edit, and comment before it becomes public.*"
        },
        {
          "id": "REQ-004",
          "title": "Duplicate prevention.",
          "text": "Before a requester submits, the portal shows the suggestions from EVID-007 with a vote control and a follow control on each. Choosing one applies that vote or follow and discards the draft, creating no request, evidence item, or moderation item."
        },
        {
          "id": "REQ-005",
          "title": "Publication.",
          "text": "An operator publishes a moderated request by approving its public title, description, board, requester display name, and each attachment separately (REQ-011). The published record contains only the approved fields."
        },
        {
          "id": "REQ-006",
          "title": "Voting.",
          "text": "A requester identity has at most one vote on a request and can remove it. The vote total is the count of distinct current voters, and a vote is accepted only while the request's public status is Open, Under review, Planned, or In progress (REQ-016). *Policy: `request.vote-eligibility.v1`; default: any verified requester may vote and every vote counts once.*"
        },
        {
          "id": "REQ-007",
          "title": "Discussion.",
          "text": "Verified requesters and operators can comment on a published request. A requester comment is held under `request.moderation.v1` and becomes public only when an operator approves it; an operator comment is public when posted."
        },
        {
          "id": "REQ-008",
          "title": "Merge.",
          "text": "An operator can merge a duplicate request into the canonical request, which is the one that survives. The operation atomically moves the duplicate's evidence links, approved attachments, comments, and followers, collapses the combined votes so each identity counts once, keeps the duplicate's title and source as merge history, and answers the duplicate's public URL with a 301 to the canonical request. Merging a request that is itself a duplicate merges it into the canonical request at the end of its chain."
        },
        {
          "id": "REQ-009",
          "title": "Unmerge.",
          "text": "An operator can reverse a merge. Penny restores the duplicate request's own content, votes, comments, followers, and evidence links, leaves anything added to the canonical request after the merge on the canonical request, and records the reversal."
        },
        {
          "id": "REQ-010",
          "title": "Follow and consent.",
          "text": "A verified requester can follow and unfollow a published request. Submitting and voting present an unticked \"Email me updates\" control and grant no email permission on their own. Following, or ticking that control, stores a timestamped permission record for the verified address, which is what makes the identity eligible under REL-004."
        },
        {
          "id": "REQ-011",
          "title": "Portal attachments.",
          "text": "A file submitted with a request is operator-only until an operator approves that file for publication; rejecting a file records a reason for operators and does not reject the request. While both the parent request and the file are published, `GET /api/public/attachments/:id` serves it with its declared content type and, for the image types in `seed.json → limits.uploadTypes`, an inline disposition so it renders on the request page; **this supersedes BASE-INPUT-003's download disposition for published image attachments only.** In every other state that path returns 404."
        },
        {
          "id": "REQ-012",
          "title": "Portal search and browse.",
          "text": "Visitors can search published request titles and descriptions, browse one board at a time, filter by public status, and sort by vote total or by most recent public activity, meaning the request's latest published comment, status update, or vote. The first page returns within 500ms, and no result, count, or facet includes an internal board, an unpublished request, or a held comment."
        },
        {
          "id": "REQ-013",
          "title": "Submission idempotency.",
          "text": "A public submission accepts an idempotency key. Repeating the same submission with that key returns the original request and evidence result and creates no second record of any kind; the same key with a different body returns 409. Keys are retained for 30 days and hold no personal data."
        },
        {
          "id": "REQ-014",
          "title": "Identity failure.",
          "text": "A host-signed identity is expired when its signed issue time is more than 24 hours old or more than 5 minutes in the future. An identity that is expired, or whose signature does not verify, is rejected for every identity-required action, changes no data, and reveals no account or identity match; the visitor is offered email verification (REQ-023) instead."
        },
        {
          "id": "REQ-015",
          "title": "Status updates.",
          "text": "An operator can publish a status update on a published request. It is rendered distinctly from comments, can be pinned above them, enters the request history, and notifies followers under NOTIFY-001."
        },
        {
          "id": "REQ-016",
          "title": "Public status.",
          "text": "A published request has exactly one public status: Open, Under review, Planned, In progress, Shipped, or Declined. A declined request is Declined; otherwise a request with an approved roadmap mapping takes the public status its mapped roadmap item projects under ROAD-003, and a request without one is Open."
        },
        {
          "id": "REQ-017",
          "title": "Decline.",
          "text": "An operator can decline a published request. A decline without a public reason is rejected. Declining sets the public status to Declined, adds the reason to the request history as a status update (REQ-015), and refuses new votes and requester comments while leaving existing ones visible and counted. Reversing a decline restores the request's previous public status."
        },
        {
          "id": "REQ-018",
          "title": "Boards.",
          "text": "Every request belongs to exactly one board. Operators create, rename, reorder, and archive boards in settings and can move a request to another board, which keeps its votes, comments, followers, and evidence links. An archived board leaves portal navigation, keeps its published requests reachable at their URLs, and refuses a new submission with 409."
        },
        {
          "id": "REQ-019",
          "title": "Internal boards.",
          "text": "Each board is public or internal. A request on an internal board never appears in a portal response, on the public roadmap, in a release, or in a requester notification, whatever its own publication state. Moving a request to an internal board withdraws it from the portal within 5 seconds and its public URL returns 404."
        },
        {
          "id": "REQ-020",
          "title": "Requester's own content.",
          "text": "A requester can edit the title and description of their own request and the text of their own comment. An edit to already-public text re-enters moderation under `request.moderation.v1`, and the previously approved text stays public until the edit is approved or rejected."
        },
        {
          "id": "REQ-021",
          "title": "Requester withdrawal.",
          "text": "A requester can withdraw their own request. A withdrawn request leaves moderation or the portal, its public URL returns 410, and its votes stop counting. Its evidence item survives under EVID-004, marked withdrawn."
        },
        {
          "id": "REQ-022",
          "title": "Blocking a requester.",
          "text": "An operator can block a requester identity. A blocked identity's submissions, votes, comments, follows, and outcome responses are refused with 403; their published requests and comments are hidden from the portal but kept for operators; and their votes stop counting toward totals. Unblocking restores all of it."
        },
        {
          "id": "REQ-023",
          "title": "Email verification.",
          "text": "A requester verifies an email address by following a single-use link sent to it; the link expires 30 minutes after it is sent and fails after one use. The verified session is a browser-local token created at the requester's first interaction and expiring after 30 days without use, so no portal surface sets a cookie. An identity-required action attempted before verification is held for 24 hours, applied on the first verification of that address, and otherwise discarded, and it creates no vote, public record, or notification while held."
        },
        {
          "id": "REQ-024",
          "title": "Requester identity merge.",
          "text": "An operator can merge two requester identities. The survivor keeps both verified addresses and all requests, comments, follows, permission records, and suppression state; votes for the same request collapse to one; and the merge is recorded. A merge creates no new vote and no new permission record."
        },
        {
          "id": "REQ-025",
          "title": "Requester activity.",
          "text": "A verified requester can see their own requests with their current moderation state and public status, their votes, their follows, and their own comments including held and rejected ones, and can unfollow, mute, or change their update subscription (REL-012) from that view."
        },
        {
          "id": "REQ-026",
          "title": "Demand attribution.",
          "text": "For any request, operators can see every current voter's display name, verified address, and linked account, and the vote total broken down by account. The operator view shows the vote total, the linked-evidence count, and the distinct-account count as three separate figures and combines them into no score. None of it appears in a portal response."
        },
        {
          "id": "REQ-027",
          "title": "Portal embedding.",
          "text": "The portal can be embedded in an iframe on the origins in `publicOriginAllowlist` under BASE-PUBLIC-002; a request to frame it from any other origin is refused."
        },
        {
          "id": "REQ-028",
          "title": "Request queue.",
          "text": "Operators can list every request, including unpublished ones, filtered by moderation state, board, public status, tag, linked decision, and account, and sorted by vote total, most recent activity, or age. The first page returns within 500ms."
        },
        {
          "id": "REQ-029",
          "title": "Operator-authored request.",
          "text": "An operator can create a request from one or more evidence items. It skips moderation, records the operator as its author, links the evidence it was created from, and becomes public only through REQ-005."
        },
        {
          "id": "REQ-030",
          "title": "Moderation verdicts.",
          "text": "An operator can approve, edit and then approve, or reject with a reason any request or comment awaiting moderation. A rejected item never becomes public, counts toward no public total, stays visible with its reason to its author and to operators, and notifies its author only when the operator chooses to."
        },
        {
          "id": "REQ-031",
          "title": "Host identity claims.",
          "text": "A host-signed identity may assert a display name, an email address, and an account identifier and name. A valid signature marks that address verified without a further challenge and links the requester to the matching or newly created account; any asserted commercial field is stored operator-only under EVID-006."
        },
        {
          "id": "REQ-032",
          "title": "Internal notes.",
          "text": "Operators can add a note to any request. A note is rendered distinctly from an operator comment and never appears in a portal response, page metadata, a release, or a notification."
        },
        {
          "id": "DEC-001",
          "title": "States and queue.",
          "text": "A decision is draft until its brief has a question, an owner, at least one candidate bet, and at least one linked evidence item, at which point it is ready; committing makes it committed, and DEC-007 makes a committed decision review due. The decision queue lists every decision in each state with its owner, last action, review date, and linked-evidence count, and a decision changes row rather than leaving the queue when its state changes."
        },
        {
          "id": "DEC-002",
          "title": "Brief.",
          "text": "A candidate bet is one named option a decision could commit to, and a bet has at most one internal roadmap item. A decision brief holds a question, an accountable owner, its linked evidence and accounts, its candidate bets with the counter-evidence and assumptions for each, a confidence level of low, medium, or high, a review date, and the requesters currently eligible under REL-004 through its mapped requests."
        },
        {
          "id": "DEC-003",
          "title": "Comparable bets.",
          "text": "Every candidate bet in a brief shows the same dimensions: affected requesters and accounts, the count and most recent observed time of its supporting evidence, its contradicting evidence, urgency, delivery shape, and an observable success measure. Penny stores and displays no combined score and no automatic ranking of the bets."
        },
        {
          "id": "DEC-004",
          "title": "Commitment.",
          "text": "Committing requires a selected bet, a rationale, an owner, a confidence level, and a review date, and committing a brief with fewer than two candidate bets additionally requires a recorded reason why no viable alternative exists. The operation records the declined bets and an evidence snapshot made of the evidence IDs, evidence-version IDs, and DEC-003 dimensions as they stood at that moment, without altering the evidence itself. *Policy: `decision.commitment.v1`; default: any operator may commit.*"
        },
        {
          "id": "DEC-005",
          "title": "Commitment effects.",
          "text": "Committing atomically marks the decision committed, creates or updates the selected bet's internal roadmap item as Planned, leaves the declined bets' roadmap items unchanged, and links the decision to that roadmap item. Commitment writes no public field itself; whether the new status reaches the portal is decided entirely by ROAD-004 and ROAD-005."
        },
        {
          "id": "DEC-006",
          "title": "Revision.",
          "text": "A later change of selected bet or rationale under `decision.commitment.v1` supersedes the prior commitment as a new decision revision, and the original rationale, evidence snapshot, and declined bets stay readable. A revision that changes the selected bet returns the superseded bet's roadmap item to Exploring and, where that item was published, withdraws its approval under ROAD-004 and prepares a status update draft under ROAD-007."
        },
        {
          "id": "DEC-007",
          "title": "Review due.",
          "text": "A review is complete when an operator records a review note and either a new review date or a revision (DEC-006). A committed decision whose review date has passed in `workspaceTimezone` without a complete review becomes review due within 1 hour and its owner is notified under NOTIFY-003. Becoming review due changes no roadmap status, no public status, and no selected bet."
        },
        {
          "id": "DEC-008",
          "title": "Outcome evidence.",
          "text": "A requester outcome (REL-008) and any evidence recorded against a linked release appear on the decision as post-release evidence within 60 seconds and do not alter the evidence snapshot that supported the commitment."
        },
        {
          "id": "ROAD-001",
          "title": "Internal states.",
          "text": "A roadmap item is Exploring, Planned, In progress, Shipped, or Dropped. A status change records the actor, time, and reason and adds an activity entry to the linked decision without changing that decision's state. Moving backward is allowed and is recorded as a further transition."
        },
        {
          "id": "ROAD-002",
          "title": "Internal record.",
          "text": "A roadmap item holds its owner, target or release window, linked decision and evidence, account impact, confidence, delivery notes, and its customer-safe title, summary, and status."
        },
        {
          "id": "ROAD-003",
          "title": "Public projection.",
          "text": "An approved roadmap item's public status follows its internal status within 5 seconds of a transition: Exploring is shown as Under review; Planned, In progress, and Shipped keep their labels; a Dropped item is withdrawn from the public roadmap. The projection exposes only the approved title, summary, board, and public status; account names, revenue, internal dates, confidence, rationale, evidence, declined bets, and delivery notes are never projected."
        },
        {
          "id": "ROAD-004",
          "title": "Approval is publication.",
          "text": "A roadmap item and each of its request mappings are internal until an operator approves their customer-safe fields, and that approval alone is what puts them on the public roadmap and on the mapped requests. A published request has at most one approved mapping; approving a second is rejected. Withdrawing approval removes the projection from the portal within 5 seconds, makes its public roadmap URL return 404, and deletes no internal record. *Policy: `roadmap.publication.v1`; default: an operator approves every public mapping explicitly.*"
        },
        {
          "id": "ROAD-005",
          "title": "Commitment mapping.",
          "text": "The roadmap item DEC-005 creates or updates is internal unless it already carries an approval under ROAD-004. An already-approved item projects its new Planned status under ROAD-003; an unapproved item and its mappings stay private, and commitment never grants an approval."
        },
        {
          "id": "ROAD-006",
          "title": "Public roadmap.",
          "text": "The public roadmap groups approved items under Under review, Planned, In progress, and Shipped and links each to its public request or release. It exposes no unapproved item and no count that includes one."
        },
        {
          "id": "ROAD-007",
          "title": "Status update draft.",
          "text": "Every status change of a roadmap item that carries an approval under ROAD-004, and every request decline under REQ-017, prepares a draft customer-safe update naming the new public status and the problem it addresses. The draft carries no internal rationale, commercial context, declined bet, or outcome prompt, and is invisible on the portal until an operator publishes it."
        },
        {
          "id": "ROAD-008",
          "title": "Status update publication.",
          "text": "Publishing that draft adds it to the history of every approved mapped request and queues one notification per update version for each requester eligible under REL-004 who follows one of those requests, deduplicated by identity and subject to the coalescing in NOTIFY-002. Republishing the same version queues nothing further."
        },
        {
          "id": "REL-001",
          "title": "Draft and targets.",
          "text": "A release draft has one or more targets, each linking one roadmap item in any state to its decision and its approved public request mappings. The draft also holds a customer-safe title, a type of New, Improved, or Fixed, one or more subject tags drawn from EVID-009, a body, a publication date, an optional setup or usage link, an author, and the wording of the outcome prompt shown with the REL-008 response."
        },
        {
          "id": "REL-002",
          "title": "Publication.",
          "text": "An operator can publish a draft only when it has a title, a body, at least one target, and every linked roadmap item at Shipped; publishing with any linked item in another state is rejected. Publication makes one versioned update visible on the portal and durably records the complete recipient snapshot (REL-004) before it queues any delivery."
        },
        {
          "id": "REL-003",
          "title": "Published wording.",
          "text": "Published release wording is never overwritten. A correction creates a new version, keeps the earlier version readable by operators, and marks which version the portal shows. A correction changes the portal only; sending it to recipients requires an operator to send it as a new delivery version."
        },
        {
          "id": "REL-004",
          "title": "Recipient eligibility and snapshot.",
          "text": "A recipient is eligible when they hold a verified address and a current permission record, and each eligible identity appears exactly once in the snapshot however many target contexts it has. At publication Penny snapshots every release target and each recipient's linked request or evidence context, and a later merge, relink, or preference change does not alter that snapshot. *Policy: `release.recipient-eligibility.v1`; default: a permission record comes only from following a request (REQ-010) or subscribing to updates (REL-012); voting alone and an operator-entered address grant nothing.*"
        },
        {
          "id": "REL-005",
          "title": "Customer-safe delivery.",
          "text": "Release email and release portal content carry only the published release fields and the recipient's own request context. Recipient lists, other requesters, account context, commercial impact, decision rationale, confidence, and declined bets never appear in either."
        },
        {
          "id": "REL-006",
          "title": "Acceptance and retry.",
          "text": "Publication queues each eligible delivery within 60 seconds of its permitted send time (NOTIFY-002). A failure returned before provider acceptance is retried up to 5 times with exponential backoff over 1 hour and then marked failed. After acceptance Penny never resends automatically. A result the provider leaves ambiguous is marked unknown, shown to operators, and retried under its delivery key (REL-007); a delivery still unknown after those retries is marked failed for BASE-OPS-004. The published update stays visible on the portal in every failure state."
        },
        {
          "id": "REL-007",
          "title": "Idempotent delivery and provider events.",
          "text": "Every release version and recipient has one durable delivery key that `mail.sender.v1` passes to the provider as an idempotency key, and the accepted provider message ID is stored against it. `/api/public/mail/events` accepts only signed provider callbacks and deduplicates their event IDs, which are retained for 30 days."
        },
        {
          "id": "REL-008",
          "title": "Outcome response.",
          "text": "A requester in a release's publication snapshot can answer Adopted, Partly adopted, or Not yet for each of its targets, whether or not they have muted or unsubscribed from email. Penny stores one current response per release, requester, and target; changing it records a revision and updates the aggregates without exposing the requester on any public surface. Repeating the same response records no revision."
        },
        {
          "id": "REL-009",
          "title": "Outcome coverage.",
          "text": "Operators can see, per target and for the whole release, the count of each response and the coverage, meaning current responses divided by snapshot recipients, and can see the individual responses. Each current response links through its snapshotted target and recipient context back to the applicable request or evidence, roadmap item, and decision."
        },
        {
          "id": "REL-010",
          "title": "Empty feed.",
          "text": "With no release published, the public updates surface states that there are no updates yet and renders no draft, example, or placeholder entry."
        },
        {
          "id": "REL-011",
          "title": "Bounces and complaints.",
          "text": "A hard bounce or a spam complaint suppresses the address: no further Penny email reaches it until the requester verifies it again, and its permission records are marked suppressed rather than deleted. A soft bounce marks that one delivery failed and suppresses nothing. Suppression removes no vote, request, comment, or portal access."
        },
        {
          "id": "REL-012",
          "title": "Update subscription.",
          "text": "A verified requester can subscribe to every published release from the public updates surface and unsubscribe there or from any notification's unsubscribe link. Subscribing stores a timestamped permission record for that address; unsubscribing withdraws it without removing votes, requests, or follows."
        },
        {
          "id": "REL-013",
          "title": "Delivery states.",
          "text": "A delivery moves through queued, then accepted, then delivered, and can terminate at failed, suppressed, or unknown. An out-of-order provider event never moves a delivery backward along that order and never counts a result twice."
        },
        {
          "id": "NOTIFY-001",
          "title": "Requester updates.",
          "text": "A follower of a published request is notified when an operator publishes a status update on it (REQ-015), declines it (REQ-017), or publishes a status update mapped to it (ROAD-008), unless they have muted that request or their address is suppressed. An internal roadmap transition with no published update notifies nobody, and republishing the same update version notifies nobody again."
        },
        {
          "id": "NOTIFY-002",
          "title": "Preferences, timing, and unsubscribe.",
          "text": "A notification's permitted send time is the first moment after it is queued at which the requester's preferences and the quiet hours below allow it. Every requester notification carries a one-click unsubscribe link that works without signing in, and a requester can mute one request or all requester email; the preference is checked immediately before each send and removes no vote, evidence, or portal access. *Policy: `requester.notification.v1`; default: hold requester email from 20:00 inclusive to 08:00 exclusive in `workspaceTimezone`, and coalesce updates for the same request queued within 1 hour into one message.*"
        },
        {
          "id": "NOTIFY-003",
          "title": "Decision review.",
          "text": "A decision owner receives one email and one in-app notification, which stays in their notification list until dismissed, the first time a decision becomes review due. Operator email carries no quiet hours and queues within 60 seconds of the decision becoming review due. No repeat is sent until the review date changes or the operator completes the review and schedules another."
        },
        {
          "id": "NOTIFY-004",
          "title": "No mail loops.",
          "text": "Notifications are sent from a no-reply address. Penny exposes no inbound-mail route, so a reply can create no evidence item, comment, request, or other record."
        },
        {
          "id": "NOTIFY-005",
          "title": "Notification failure.",
          "text": "Requester status-update email and operator decision-review email each use a durable recipient-and-event key and retry, suppress, and expose failure exactly as REL-006, REL-011, and REL-013 describe. The in-app or portal record of the same event stays available when the email fails."
        },
        {
          "id": "DATA-001",
          "title": "Export contents.",
          "text": "The export contains every durable record as JSON, including every operator, requester, identity merge, account, board, tag, request, moderation decision and reason, merge history, vote, comment, note, follower, block, evidence item and version, decision and revision, roadmap item and transition, release, release version and target, delivery with its key and provider message ID, outcome and revision, subscription, suppressed address, notification and preference, custom field, and audit entry, plus every attachment as its original file."
        },
        {
          "id": "DATA-002",
          "title": "Public/private boundary.",
          "text": "No operator-only field name and no operator-only value appears anywhere in a public response payload, including in fields the portal does not render."
        },
        {
          "id": "DATA-003",
          "title": "Erasure outranks history.",
          "text": "Where a clause here says a record is preserved, versioned, immutable, or never overwritten, erasure under BASE-DATA-003 takes precedence: the record is deleted or anonymised and the surviving audit entry keeps an anonymised reference. Unmerging (REQ-009) and revision history never recreate erased data."
        },
        {
          "id": "DATA-004",
          "title": "Custom fields.",
          "text": "Operators define custom fields in settings on each entity listed in `seed.json → customFields.entities`, and can read and edit their values wherever that entity is shown. A custom field is operator-only and is never projected to a public surface."
        }
      ],
      "hasReadme": true,
      "hasBaseline": true,
      "nonGoals": [
        "Multiple workspaces, businesses, or tenants",
        "Granular roles, permissions, and approval chains",
        "A CRM, support inbox, survey, NPS, community chat, or general-purpose forum",
        "Anonymous or unverified voting",
        "AI classification, summarisation, scoring, prioritisation, or automatic merging",
        "A combined priority score or automatic ranking of requests, bets, or roadmap items",
        "Automated CRM, support, review-site, or call ingestion",
        "Importing feedback from another feedback tool",
        "A script-tag feedback widget or in-app capture SDK beyond the embeddable portal in REQ-027",
        "Bidirectional Jira, Linear, or other delivery-tracker synchronisation",
        "Portfolio capacity planning, Gantt charts, sprint management, and public delivery estimates",
        "Account-specific public roadmaps",
        "Native mobile applications"
      ],
      "externals": [
        {
          "name": "transactional-email",
          "required": true,
          "requiredWhen": "",
          "reason": "Operator magic-link sign-in (BASE-ACCESS-001), requester email verification (REQ-023), decision-review alerts (NOTIFY-003), and opted-in requester status and release notifications must reach addresses outside the deployment; no owned Cloudflare primitive delivers mail. The adapter must accept a caller-supplied idempotency key, enforced at the provider, so REL-006 can retry an ambiguous send without duplicating it, and must deliver signed delivery, bounce, and complaint callbacks so REL-007, REL-011, and REL-013 can update delivery state.",
          "data": [
            "recipient email address",
            "single-use sign-in or verification link, or customer-safe notification content",
            "delivery key for bounce and retry correlation"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "estimate-only",
        "basis": "Workers Free is the default Cloudflare plan; Workers Paid starts at USD 5 per account-month. The current read-only port dispatches no Queue deliveries. For the future release-delivery limit, a normal Queue message at or below 64 KB is at least three operations (write, read, delete): 3,334 successful deliveries consume at least 10,002 operations and exceed Workers Free's 10,000 Queue operations per account per UTC day. A maximum 50,000-recipient release needs at least 150,000 Queue operations before retries, dead-letter handling, or messages above 64 KB, so it genuinely requires Workers Paid. Workers Free D1 includes 5 million rows read/day, 100,000 rows written/day, and 5 GB total storage; reaching a Free D1 limit makes D1 return errors until the UTC reset or an upgrade. R2 Standard includes 10 GB-month storage, 1 million Class A and 10 million Class B operations/month; using R2 requires a billing-enabled Cloudflare account even while usage remains within those included allowances. The R2 free tier does not apply to Infrequent Access storage, which also has retrieval fees and a 30-day minimum storage duration. Cron Triggers are included; this port does not currently use Durable Objects. R2 and Workers do not charge Internet egress under the documented plans. Transactional email remains a separate provider and message-volume cost.",
        "monthlyUsd": {
          "cloudflare": "Workers Free by default; Workers Paid starts at 5, then Queue, D1, R2, and Worker usage above included allowances are usage-dependent",
          "transactionalEmail": "provider and message-volume dependent"
        },
        "excluded": [
          "domain registration",
          "operator implementation time"
        ]
      },
      "extensionPoints": [
        "app.page.v1",
        "app.navigation.v1",
        "app.settings-section.v1",
        "job.consumer.v1",
        "cron.task.v1",
        "operator.management.v1",
        "request.moderation.v1",
        "request.visibility.v1",
        "request.vote-eligibility.v1",
        "decision.commitment.v1",
        "roadmap.publication.v1",
        "release.recipient-eligibility.v1",
        "requester.notification.v1",
        "evidence.created.v1",
        "evidence.linked.v1",
        "request.created.v1",
        "request.moderated.v1",
        "request.published.v1",
        "request.declined.v1",
        "request.merged.v1",
        "request.unmerged.v1",
        "decision.committed.v1",
        "decision.revised.v1",
        "decision.review-due.v1",
        "roadmap.status-changed.v1",
        "roadmap.status-update-published.v1",
        "release.published.v1",
        "release.delivery-failed.v1",
        "release.outcome-recorded.v1",
        "overview.after.v1",
        "evidence.list.after.v1",
        "evidence.detail.after.v1",
        "account.detail.after.v1",
        "request.detail.after.v1",
        "decision.brief.after.v1",
        "decision.alternatives.after.v1",
        "roadmap.item.after.v1",
        "release.detail.after.v1",
        "portal.request.detail.after.v1",
        "mail.sender.v1"
      ],
      "limits": {
        "provenance": "All values are estimates for backend design and must be replaced by load-test results before the first release. Latency that defines a feature lives in its contract clause, not here.",
        "status": "estimated-not-tested",
        "operators": 20,
        "boards": 100,
        "tags": 500,
        "accounts": 25000,
        "requesters": 250000,
        "evidenceItems": 1000000,
        "publicRequests": 100000,
        "votes": 2000000,
        "comments": 1000000,
        "decisions": 25000,
        "roadmapItems": 25000,
        "releases": 10000,
        "recipientsPerRelease": 50000,
        "publicReadRequestsPerMinutePerIp": 120,
        "publicWriteRequestsPerMinutePerIp": 30,
        "publicWritesPerMinutePerIdentity": 20,
        "identityActionsPerMinutePerIp": 10,
        "identityVerificationsPerHourPerEmail": 5,
        "magicLinkRequestsPerHourPerEmail": 5,
        "mailEventsPerMinutePerIp": 600,
        "requestSubmissionsPerHourPerIdentity": 10,
        "uploadBytesPerFile": 10485760,
        "uploadBytesPerRequest": 26214400,
        "uploadTypes": [
          "image/png",
          "image/jpeg",
          "image/gif",
          "application/pdf"
        ]
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target-not-yet-verified"
      },
      "landing": "assets/seeds/penny/01-landing.jpg",
      "shots": [
        "assets/seeds/penny/01-landing.jpg",
        "assets/seeds/penny/02-public-roadmap.jpg",
        "assets/seeds/penny/04-operator-overview.jpg",
        "assets/seeds/penny/05-operator-decision.jpg"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/penny",
        "release": "0.0.0-bootstrap",
        "manifestSha256": "3e83a43a2c3ebf8903868971198ba2383028544d02ccffdaf0b57c57751f6900"
      }
    },
    {
      "id": "beacon",
      "authoring": {
        "mode": "edition-owned",
        "agentSource": "AGENTS.md",
        "id": "runeditrun/beacon",
        "parent": {
          "repository": "https://github.com/runeditrun/support-beacon.git",
          "revision": "0efb6d0c0b68ca7d4c1b80e31768126ecda132e4"
        },
        "toolchain": {
          "id": "runeditrun/edition-authoring",
          "version": "1.1.0",
          "sha256": "a13d7d0bcf174618923ca5d1134575c149ea6b57c89d1640c03c1ebf091ab372"
        },
        "components": [
          {
            "id": "runeditrun/base",
            "role": "foundation",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/deployment/health.mjs",
                "scripts/deployment/base-secret-json.mjs",
                "schema/seed-spec/SEED.schema.json",
                "schema/seed-spec/json-schema.mjs",
                "schema/seed-spec/validate.mjs",
                "scripts/operations.mjs"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "bc27ecf55693a68bfa7e8a6aba9fe3ce8883bcfe",
                "path": ".",
                "paths": [
                  "deployment/health.mjs",
                  "deployment/secret-json.mjs",
                  "schema/SEED.schema.json",
                  "schema/json-schema.mjs",
                  "schema/validate.mjs",
                  "operations/operations.mjs"
                ]
              },
              "mappings": [
                {
                  "path": "scripts/deployment/health.mjs",
                  "originPath": "deployment/health.mjs"
                },
                {
                  "path": "scripts/deployment/base-secret-json.mjs",
                  "originPath": "deployment/secret-json.mjs"
                },
                {
                  "path": "schema/seed-spec/SEED.schema.json",
                  "originPath": "schema/SEED.schema.json"
                },
                {
                  "path": "schema/seed-spec/json-schema.mjs",
                  "originPath": "schema/json-schema.mjs"
                },
                {
                  "path": "schema/seed-spec/validate.mjs",
                  "originPath": "schema/validate.mjs"
                },
                {
                  "path": "scripts/operations.mjs",
                  "originPath": "operations/operations.mjs"
                }
              ]
            }
          },
          {
            "id": "runeditrun/support",
            "role": "family",
            "source": {
              "kind": "local",
              "paths": [
                "src/family/support",
                "composition/families/support/README.md",
                "composition/families/support/LICENSE",
                "tests/composition/support",
                "scripts/test-composition.mjs"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/support.git",
                "revision": "71dea496cd4e2862295b1a1e82afa6838df8f598",
                "path": ".",
                "paths": [
                  "core/src",
                  "core/README.md",
                  "core/LICENSE",
                  "core/tests",
                  "core/test-components.mjs"
                ]
              },
              "mappings": [
                {
                  "path": "src/family/support",
                  "originPath": "core/src"
                },
                {
                  "path": "composition/families/support/README.md",
                  "originPath": "core/README.md"
                },
                {
                  "path": "composition/families/support/LICENSE",
                  "originPath": "core/LICENSE"
                },
                {
                  "path": "tests/composition/support",
                  "originPath": "core/tests"
                },
                {
                  "path": "scripts/test-composition.mjs",
                  "originPath": "core/test-components.mjs"
                }
              ]
            }
          },
          {
            "id": "runeditrun/support-intake",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/support-intake",
                "composition/modules/support-intake/README.md",
                "composition/modules/support-intake/LICENSE",
                "tests/composition/support-intake"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/support.git",
                "revision": "71dea496cd4e2862295b1a1e82afa6838df8f598",
                "path": ".",
                "paths": [
                  "modules/support-intake/src",
                  "modules/support-intake/README.md",
                  "modules/support-intake/LICENSE",
                  "modules/support-intake/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/support-intake",
                  "originPath": "modules/support-intake/src"
                },
                {
                  "path": "composition/modules/support-intake/README.md",
                  "originPath": "modules/support-intake/README.md"
                },
                {
                  "path": "composition/modules/support-intake/LICENSE",
                  "originPath": "modules/support-intake/LICENSE"
                },
                {
                  "path": "tests/composition/support-intake",
                  "originPath": "modules/support-intake/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/support-email",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/support-email",
                "composition/modules/support-email/README.md",
                "composition/modules/support-email/LICENSE",
                "tests/composition/support-email"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/support.git",
                "revision": "71dea496cd4e2862295b1a1e82afa6838df8f598",
                "path": ".",
                "paths": [
                  "modules/support-email/src",
                  "modules/support-email/README.md",
                  "modules/support-email/LICENSE",
                  "modules/support-email/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/support-email",
                  "originPath": "modules/support-email/src"
                },
                {
                  "path": "composition/modules/support-email/README.md",
                  "originPath": "modules/support-email/README.md"
                },
                {
                  "path": "composition/modules/support-email/LICENSE",
                  "originPath": "modules/support-email/LICENSE"
                },
                {
                  "path": "tests/composition/support-email",
                  "originPath": "modules/support-email/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/sendgrid-email",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/sendgrid-email",
                "composition/modules/sendgrid-email/README.md",
                "composition/modules/sendgrid-email/LICENSE",
                "tests/composition/sendgrid-email"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "bc27ecf55693a68bfa7e8a6aba9fe3ce8883bcfe",
                "path": ".",
                "paths": [
                  "modules/sendgrid-email/src",
                  "modules/sendgrid-email/README.md",
                  "modules/sendgrid-email/LICENSE",
                  "modules/sendgrid-email/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/sendgrid-email",
                  "originPath": "modules/sendgrid-email/src"
                },
                {
                  "path": "composition/modules/sendgrid-email/README.md",
                  "originPath": "modules/sendgrid-email/README.md"
                },
                {
                  "path": "composition/modules/sendgrid-email/LICENSE",
                  "originPath": "modules/sendgrid-email/LICENSE"
                },
                {
                  "path": "tests/composition/sendgrid-email",
                  "originPath": "modules/sendgrid-email/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/beacon",
            "role": "edition",
            "source": {
              "kind": "local",
              "paths": [
                "src/core",
                "src/ext",
                "src/routes",
                "migrations"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/support.git",
                "revision": "71dea496cd4e2862295b1a1e82afa6838df8f598",
                "path": ".",
                "paths": [
                  "editions/beacon/src/core",
                  "editions/beacon/src/ext",
                  "editions/beacon/src/routes",
                  "editions/beacon/migrations"
                ]
              },
              "mappings": [
                {
                  "path": "src/core",
                  "originPath": "editions/beacon/src/core"
                },
                {
                  "path": "src/ext",
                  "originPath": "editions/beacon/src/ext"
                },
                {
                  "path": "src/routes",
                  "originPath": "editions/beacon/src/routes"
                },
                {
                  "path": "migrations",
                  "originPath": "editions/beacon/migrations"
                }
              ]
            }
          },
          {
            "id": "runeditrun/onboarding",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/base-onboarding"
              ]
            }
          }
        ],
        "manifestSha256": "0eb58b5abc957d28a124d40e21983dc46170ec7b839cd6f438931eb3e450b76f",
        "lockSha256": "e0c245f5148a093822d78a9617130019dcc0b3606f896569f325dd351e8b852b"
      },
      "name": "Beacon",
      "dir": "crisp",
      "category": "support",
      "categoryLabel": "Support",
      "spine": {
        "id": "conversation-desk",
        "record": "A conversation between a customer and a team, with separately raised cases as the work records over it."
      },
      "composition": {
        "base": "base",
        "family": "support",
        "edition": "beacon",
        "modules": [
          "support-intake",
          "support-email",
          "sendgrid-email"
        ]
      },
      "replaces": [
        {
          "name": "Intercom",
          "edition": null
        }
      ],
      "version": "0.2.0",
      "oneLiner": "A self-hosted conversation desk for a business's customer conversations and separately raised cases.",
      "summary": "A self-hosted conversation desk for a business's customer conversations and separately raised cases.",
      "scope": "",
      "maturity": "Stage 2 partial development: HOME-001 and CASE-011 have accepted full-clause evidence; D1-backed channel, notification, customer, receipt, Start, and widget seams have bounded implementation evidence. WIDGET-001–005 client/configuration/contact verification and all other product clauses remain pending full-clause acceptance.",
      "clauseCount": 176,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Owner front door.",
          "text": "At `/`, Beacon renders the workspace name and one owner-configured description from `src/ext/config.ts`, using the configured theme, with a link to `/signin`. It renders no customer or case data, vendor pricing, feature claims, testimonials, or operational metrics. The configurable “Powered by Beacon · runeditrun.com” credit defaults on; when enabled it is a plain link and makes no request before an operator chooses it."
        },
        {
          "id": "OPS-001",
          "title": "Granted workspace access.",
          "text": "A signed-in member can read or operate only the inboxes, conversations, cases, and administration grants assigned to them. Every permitted action records the acting member, time, and authorising grant."
        },
        {
          "id": "OPS-002",
          "title": "Workspace administration.",
          "text": "A member with workspace-administration grant can configure workspace identity, teammates, teams, channel settings, category configuration, working schedule, and public-channel presentation. Removing the final active workspace administrator is refused with that reason."
        },
        {
          "id": "OPS-003",
          "title": "Approval eligibility.",
          "text": "A member may approve or submit a manual resolution only when they hold the non-bypassable decide grant for that case type. The decision records the signed-in member and grant. *Policy: `approval.eligibility.v2`; default: permit a qualified preparer to approve their own decision; the policy may impose an additional reviewer condition or forbid self-approval but cannot admit a member without the decide grant.*"
        },
        {
          "id": "OPS-004",
          "title": "Team inbox membership.",
          "text": "A routing administrator can create inboxes, give members inbox membership, and select a routing mode. The recorded selection and every assignment preserve the actor and time."
        },
        {
          "id": "OPS-005",
          "title": "Public sign-in boundary.",
          "text": "Customer and public paths disclose no operator, conversation, case, Tracker, evidence, or approval data until their own route-level identity check succeeds."
        },
        {
          "id": "OPS-006",
          "title": "Conversation-scoped send grant.",
          "text": "A member may review, approve, or manually resolve a conversation customer send, whether or not that conversation has linked cases, only when they hold the non-bypassable conversation-send grant for its inbox. This grant cannot authorise an external-provider mutation, case action, command, or receipt."
        },
        {
          "id": "CHAN-001",
          "title": "Inbound email.",
          "text": "A signed, configured inbound-mail event becomes one conversation message within 60 seconds, preserving sender, recipients, subject, text and sanitised HTML bodies, attachments, provider message ID, and threading headers."
        },
        {
          "id": "CHAN-002",
          "title": "Strict email threading.",
          "text": "An inbound reply joins a conversation only when its `In-Reply-To` or `References` header names a stored message of that conversation, or its signed Beacon reply token names that conversation."
        },
        {
          "id": "CHAN-003",
          "title": "Ambiguous email.",
          "text": "An inbound email that CHAN-002 does not prove belongs to a conversation starts a new conversation. When its sender is a verified contact point of a known customer, the new conversation links that customer and is flagged as possibly related to that customer's most recent non-completed case. Sender or subject similarity alone never joins it to an existing conversation."
        },
        {
          "id": "CHAN-004",
          "title": "Inbound deduplication.",
          "text": "Repeated delivery of one authenticated provider message ID creates one message and one set of downstream events."
        },
        {
          "id": "CHAN-005",
          "title": "Reviewed outbound email.",
          "text": "A signed-in member may submit an email only from a reviewed case outcome send under ACT-013 or a reviewed conversation-scoped send under CONV-014, whether or not that conversation has linked cases. It is submitted to `mail.sender.v1` within 60 seconds with stable delivery ID, correct threading headers, and attachments within declared limits. An email recipient is a verified contact under CUST-009."
        },
        {
          "id": "CHAN-006",
          "title": "First-party web-chat inbound.",
          "text": "A visitor message appends to that visitor's open conversation or starts one and appears to permitted operators within 2 seconds. A visitor has at most one open conversation."
        },
        {
          "id": "CHAN-007",
          "title": "Reviewed web-chat outbound.",
          "text": "A signed-in member may submit web chat only from a reviewed case outcome send under ACT-013 or a reviewed conversation-scoped send under CONV-014, whether or not that conversation has linked cases. It reaches the authenticated visitor session or token that owns the conversation within two seconds. A disconnected visitor sees it on reconnect, and a customer with a verified email contact receives email fallback within 60 seconds only when the reviewed send permits it."
        },
        {
          "id": "CHAN-008",
          "title": "Attachment access.",
          "text": "An accepted attachment keeps its original bytes and declared filename and is readable only by an authorised member, the visitor session that owns its conversation, and an approved delivery to that conversation's verified customer contact. It reaches no other recipient."
        },
        {
          "id": "CHAN-009",
          "title": "Message delivery truth.",
          "text": "Every outbound customer message records queued, sent, delivered, or failed, its attempts, and a redacted provider outcome. For web chat, delivered means the visitor session acknowledged it. A transient submission failure receives at most five attempts over one hour using the same delivery ID; permanent rejection or later bounce is failed and not resubmitted."
        },
        {
          "id": "CHAN-010",
          "title": "Automatic mail.",
          "text": "Automatic, bulk, list, and no-reply mail is preserved on its conversation but does not reopen it, create a case, alert a member, or invoke AI."
        },
        {
          "id": "CHAN-011",
          "title": "Authenticated ingress.",
          "text": "Mail webhooks authenticate through `mail.inbound.v1` and external-provider webhooks through `operations.provider.v1` before any deduplication or mutation. An absent or invalid signature creates no message, event, action transition, or AI request and is recorded as a rejected delivery."
        },
        {
          "id": "CHAN-012",
          "title": "Chat deduplication.",
          "text": "Each web-chat submission has a visitor-scoped client message ID. A retry returns its stored message and creates no duplicate message, case, alert, or AI request."
        },
        {
          "id": "CHAN-013",
          "title": "Rejected attachments.",
          "text": "An oversized, disallowed, or malformed attachment stores no bytes and appears as a rejected placeholder naming its filename and reason while the rest of the message is preserved."
        },
        {
          "id": "CHAN-014",
          "title": "Chat presence.",
          "text": "While web chat is connected, a typing signal from either side reaches the other within two seconds, carries no message text, and clears after 10 seconds without a keystroke. The visitor sees whether Beacon accepted or rejected each own message, and can retry a rejected message under CHAN-012 using its original client message ID."
        },
        {
          "id": "CHAN-015",
          "title": "Operator-created conversation.",
          "text": "A permitted member can begin an email or first-party web-chat conversation with a verified customer contact, optionally linking an existing case. It creates no case merely because it is sent or replied to; a member raises any case explicitly under CASE-002."
        },
        {
          "id": "CHAN-016",
          "title": "Channel scope.",
          "text": "Beacon's 1.0 customer channels are verified email and first-party web chat. SMS and WhatsApp are deferred adapter families; their absence from 1.0 is not an assertion that an incumbent menu lacks them."
        },
        {
          "id": "OUT-001",
          "title": "Outbound message preparation.",
          "text": "A permitted member can prepare a one-off reviewed outbound email or first-party web-chat message, naming its audience, channel, content, and purpose. Preparation sends nothing and creates no live autonomous campaign."
        },
        {
          "id": "OUT-002",
          "title": "Audience and schedule.",
          "text": "A reviewed outbound audience rule records its version and evaluation time but only prepares candidates. Before review Beacon resolves a fixed target snapshot. Each target follows exactly one path: a case-specific target has its own case, ACT-010 decision snapshot, JUDG-004 review, approval or manual resolution, delivery record, and receipt where applicable; a conversation-scoped target uses its own CONV-014 immutable per-recipient reviewed-send snapshot, whether or not that conversation has linked cases, and its delivery creates no case, action, command, or receipt. A changed audience rule or membership may create a new fixed target snapshot only before that target is reviewed; it never expands an already reviewed decision or send snapshot."
        },
        {
          "id": "OUT-003",
          "title": "Conditional content.",
          "text": "A reviewed outbound decision may prepare conditional customer-safe content with a named condition and fallback. Each delivered target records the resolved branch; a fallback never fabricates a missing customer attribute."
        },
        {
          "id": "OUT-004",
          "title": "Direct delivery.",
          "text": "A reviewed outbound message is delivered only through CHAN-005 or CHAN-007 after its customer-level review decision. It records delivery truth under CHAN-009 and does not become an autonomous send because it was scheduled."
        },
        {
          "id": "OUT-005",
          "title": "Series preparation.",
          "text": "A member can prepare a multi-step outbound Series for review. A Series cannot activate, send a step, call a provider, or bypass per-target customer-send approval until separately declared and implemented in a later contract release."
        },
        {
          "id": "WIDGET-001",
          "title": "Embed.",
          "text": "A host page embeds first-party web chat with one asynchronous script tag carrying a workspace key and nothing else. The first script is under 50KB gzipped, does not block host rendering, and every later asset comes from the Beacon deployment."
        },
        {
          "id": "WIDGET-002",
          "title": "Visitor token.",
          "text": "At first interaction Beacon issues an opaque browser-stored visitor token that restores only that visitor's own conversation history on later loads and grants no operator or customer credential."
        },
        {
          "id": "WIDGET-003",
          "title": "Allowed origin.",
          "text": "Every widget request is checked against configured host origins. An unlisted origin receives no conversation, customer, availability, or operator data."
        },
        {
          "id": "WIDGET-004",
          "title": "Availability and offline capture.",
          "text": "The widget reports operators present only when a permitted operator has had the app focused within five minutes and the current time falls inside the configured working schedule; otherwise it shows the configured away message and expected first-reply time. The composer accepts messages in both states. An offline message remains a conversation message, asks the visitor for an email contact point to verify under CUST-009, and creates no case until a member chooses case type and reason under CASE-002. The availability request carries workspace key and no visitor token, so it sends no visitor data before first interaction."
        },
        {
          "id": "WIDGET-005",
          "title": "Appearance.",
          "text": "Launcher label, accent colour, corner position, welcome message, and away message are workspace settings applied on the next widget load without changing the host embed."
        },
        {
          "id": "WIDGET-006",
          "title": "Public previews.",
          "text": "Messenger and Help Center configuration may show mobile-sized previews. Such a preview is not a native mobile operator application."
        },
        {
          "id": "CONV-001",
          "title": "Lifecycle.",
          "text": "A conversation is open, snoozed until a recorded time, or closed. A non-automatic customer message reopens a closed or snoozed conversation unless CUST-008 blocks the customer, without altering any linked case, action, or receipt."
        },
        {
          "id": "CONV-002",
          "title": "Transcript.",
          "text": "An authorised member can prepare a transcript for a verified contact of that conversation's customer. It contains customer-visible messages and times only, never a private note, evidence, decision, or rationale."
        },
        {
          "id": "CONV-003",
          "title": "Primary record and opaque identity.",
          "text": "A conversation is the customer communication timeline with an opaque internal conversationId; it is not publicly addressed. Its messages, internal notes, channel identity, customer context, assignment, state, and merge history remain on the conversation."
        },
        {
          "id": "CONV-004",
          "title": "Queues and assignment.",
          "text": "A permitted member can assign, reassign, or unassign a conversation within a granted inbox and can select a queue view. *Policy: `conversation.assignment.v1`; default: manual assignment, initially unassigned.*"
        },
        {
          "id": "CONV-005",
          "title": "Priority and snooze.",
          "text": "A member can set a conversation priority and snooze it to a recorded time. Snoozing removes it from active views until the time expires or CONV-001 reopens it; each change is audited."
        },
        {
          "id": "CONV-006",
          "title": "Customer context.",
          "text": "The conversation detail presents its verified customer, relevant contacts, company relationships, other conversations, and linked cases only to a member authorised for those records."
        },
        {
          "id": "CONV-007",
          "title": "Replies and internal notes.",
          "text": "A member can draft a customer reply or add an internal note. Notes are visually distinct, never leave the deployment, never appear in a customer route, and never enter a customer-facing AI output."
        },
        {
          "id": "CONV-008",
          "title": "AI reply aids.",
          "text": "A member may request a cited rewrite, macro-assisted draft, summary, or reply draft. It enters a review surface, is labelled AI where applicable, and cannot send itself."
        },
        {
          "id": "CONV-009",
          "title": "Search and commands.",
          "text": "Permitted members can search and filter conversations and use explicit commands such as assignment, snooze, case creation, and merge. A command records its actor and does not infer a case or external action."
        },
        {
          "id": "CONV-010",
          "title": "Conversation merge.",
          "text": "A member with merge grant may merge conversations only when both link the same verified customer. The member selects a canonical conversation and records a reason. The merge emits `conversation.merged.v1` naming former and canonical conversation, member, time, and reason. The losing conversation becomes a read-only merge pointer; its messages, notes, source identity, and audit history remain readable only through their original customer authorisation boundary. Every case whose originating conversation becomes non-canonical retains that origin and receives an immutable origin-merge-link audit event naming case, former origin, canonical conversation, member, time, and reason. Cases remain separate and are neither merged nor rewritten."
        },
        {
          "id": "CONV-011",
          "title": "Workbench.",
          "text": "The operator Inbox presents queue navigation, a conversation list, selected timeline, composer, and customer/context panel as one conversation workbench."
        },
        {
          "id": "CONV-012",
          "title": "Global destinations.",
          "text": "The operator application exposes distinct destinations for Inbox, AI preparation, knowledge, reports, outbound, and contacts. Navigation changes no work state."
        },
        {
          "id": "CONV-013",
          "title": "Linked-case detail.",
          "text": "A conversation detail lists its originating and additional linked cases, including Customer, Back-office, and Tracker cases available to that member; a link does not turn the conversation into a case."
        },
        {
          "id": "CONV-014",
          "title": "Conversation-scoped reviewed send.",
          "text": "A conversation may remain at zero cases and may also have linked cases. In either situation a signed-in member holding its conversation-send grant may create an immutable per-recipient reviewed-send snapshot. The snapshot names the conversation, authenticated visitor session/token or verified email recipient, channel, content, actor, review/approval or manual-resolution member, time, and stable delivery ID. It is sent once through CHAN-005 or CHAN-007, records delivery truth under CHAN-009, creates or mutates no case, provider command, case action, or receipt, and a changed recipient or content requires a new snapshot. Email requires CUST-009 verified contact; first-party web chat may target the authenticated visitor session/token that owns the conversation."
        },
        {
          "id": "CASE-001",
          "title": "Separate records.",
          "text": "A case is separately created accountable work over one required originating conversation. A new conversation starts with zero cases; a conversation may have zero or many cases. A case owns its type, lifecycle, evidence, decision, action, and receipt history."
        },
        {
          "id": "CASE-002",
          "title": "Creation.",
          "text": "A permitted member creates a case from a conversation by selecting Customer, Back-office, or Tracker type and recording a reason. Inbound email, operator-created conversation, widget offline capture, or a customer reply never creates a case automatically."
        },
        {
          "id": "CASE-003",
          "title": "Lifecycle.",
          "text": "A case is in exactly one of working, waiting, awaiting_approval, returned, declined, executing, action_failed, action_indeterminate, awaiting_message, notification_failed, or completed. Every transition preserves actor or feature, time, and reason. Assignment, priority, escalation, tags, and due time are independent fields that no transition clears."
        },
        {
          "id": "CASE-004",
          "title": "Resolution queues.",
          "text": "Every non-completed case appears in exactly one primary queue, selected in this order: an escalation flag or action_failed, action_indeterminate, or notification_failed state is Escalated; awaiting_message is Ready to send; waiting or executing is Waiting; any other non-completed case is Needs a decision. A completed case appears in no primary queue and remains searchable and reportable."
        },
        {
          "id": "CASE-005",
          "title": "Assignment.",
          "text": "A permitted member can assign and reassign a case within its permitted case scope. *Policy: `case.assignment.v1`; default: manual assignment, initially unassigned.*"
        },
        {
          "id": "CASE-006",
          "title": "Priority.",
          "text": "Each case has low, medium, or high priority and starts at medium. A member can change it, and its recorded reason says whether its current value came from a member or AI-005 suggestion."
        },
        {
          "id": "CASE-007",
          "title": "Due time.",
          "text": "Every non-completed case stores a UTC due instant computed from configured IANA workspace timezone, weekly working schedule, and holiday dates, and is labelled due later, due today, or overdue in that timezone. A later schedule change affects only new calculations. *Policy: `case.due-time.v1`; default: four configured working hours from creation.*"
        },
        {
          "id": "CASE-008",
          "title": "Replies and notes.",
          "text": "Case activity may show prepared customer messages and internal notes. A note remains private under CONV-007, and a prepared customer message still requires JUDG-004 and ACT-013 before it is sent."
        },
        {
          "id": "CASE-009",
          "title": "Waiting.",
          "text": "Moving a case to waiting records a reason and wake time. It returns to working within 60 seconds of wake or immediately on a non-automatic customer message, and each return produces exactly one operator alert under NOTIFY-002."
        },
        {
          "id": "CASE-010",
          "title": "Explicit completion.",
          "text": "Approval and provider acceptance never complete a case. An executable-action case completes only after the authoritative provider reports that action succeeded and its required reviewed customer message is recorded sent. A documented no-action resolution may complete a case only when it has no pending, indeterminate, or succeeded action; a failed action remains visible and needs either replacement or the documented permitted no-action resolution. Inactivity and conversation close never complete a case."
        },
        {
          "id": "CASE-011",
          "title": "Follow-up after completion.",
          "text": "A member can create a linked follow-up case after completion. No later customer message creates a follow-up automatically; previous decisions, actions, and receipts remain immutable."
        },
        {
          "id": "CASE-012",
          "title": "Case merge.",
          "text": "A permitted member may merge two non-completed cases only when they belong to the same customer and have compatible visibility scope; a Tracker case cannot merge with a Customer or Back-office case. The member names a canonical case and records a reason. The losing case becomes a read-only pointer, retains its notes, evidence, decision versions, event history, originating conversation, and links, accepts no new proposal, decision, action, or case link, and cannot expose its private history through the canonical case. The merge emits `case.merged.v1` naming both cases, canonical case, member, time, and reason. Case merge is separate from CONV-010 and never erases a source conversation."
        },
        {
          "id": "CASE-013",
          "title": "Views.",
          "text": "Members can list cases by primary queue, lifecycle state, channel, assignee, priority, due state, and tag, sorted by last activity or due time in pages of the size declared in seed.json limits."
        },
        {
          "id": "CASE-014",
          "title": "Search.",
          "text": "Full-text search over customer names and contact points, message bodies, case subjects and identifiers, evidence labels, and receipt references returns authorised matching cases within 500ms."
        },
        {
          "id": "CASE-015",
          "title": "Unread.",
          "text": "A case is unread for a member until that member opens it after the latest customer message in its conversation. Every authorised case view shows that member's unread count."
        },
        {
          "id": "CASE-016",
          "title": "Escalation.",
          "text": "A permitted member can set or clear a durable escalation flag with reason. The flag routes the case to Escalated without changing lifecycle state, assignee, or due time, and alerts the members named by notification.routing.v1."
        },
        {
          "id": "CASE-017",
          "title": "Tags.",
          "text": "A permitted member can add and remove named case tags. Tags are filterable under CASE-013, reportable under ANALYTICS-003, and included in an authorised operator export."
        },
        {
          "id": "CASE-018",
          "title": "Additive conversation links.",
          "text": "A case retains its origin conversation and may receive additional audited related-conversation links. A link is additive: it does not merge conversations, customers, identities, notes, messages, or case histories, and it carries the member, reason, and time."
        },
        {
          "id": "CASE-019",
          "title": "Customer case.",
          "text": "A Customer case is customer shared and reply capable. It has a stable short public case number; its public route exposes only customer-safe material authorised for that case."
        },
        {
          "id": "CASE-020",
          "title": "Back-office case.",
          "text": "A Back-office case is private by default in 1.0. A later customer-exposure capability requires a new contract clause, release change, permissions model, and customer-safe disclosure rules."
        },
        {
          "id": "CASE-021",
          "title": "Tracker privacy.",
          "text": "A Tracker case has no public number, public route, customer search result, customer export entry, or customer notification. No setting, policy, extension, import flag, edition, or role grant can make it customer shared."
        },
        {
          "id": "CASE-022",
          "title": "Tracker aggregation and action boundary.",
          "text": "A Tracker may aggregate related conversations and linked cases for a widespread issue using CASE-018. It never merges customer identities and cannot itself authorise a customer send or provider action. Each customer-specific target has a separate linked Customer or Back-office case with its own origin, target, decision, approval, command, outcome, and receipt."
        },
        {
          "id": "CASE-023",
          "title": "Category configuration.",
          "text": "A routing administrator can configure case states and attributes by case type. Category configuration cannot relax CASE-019, CASE-020, CASE-021, CASE-022, JUDG-004, or provider-outcome rules."
        },
        {
          "id": "CUST-001",
          "title": "Customer and visitor records.",
          "text": "An inbound message from a verified email contact links the unique customer that owns that contact, or creates one customer when no such verified association exists. A web-chat visitor without a verified contact remains an anonymous visitor record; obtaining a verified contact can link it to that customer."
        },
        {
          "id": "CUST-002",
          "title": "Customer profile.",
          "text": "A customer holds name, verified contact points, preferred contact method, relationships, permitted notes, tags, enabled custom fields, conversations, cases, action outcomes, receipts, and audited activity. A permitted member can edit profile fields, and activity records each edit with actor, time, and before/after value."
        },
        {
          "id": "CUST-003",
          "title": "Contact matching.",
          "text": "One verified case-insensitive email contact belongs to exactly one customer, and every inbound conversation from that verified contact links to that customer. A customer-entered unverified contact value neither merges customers nor authorises disclosure."
        },
        {
          "id": "CUST-004",
          "title": "Signed web identity.",
          "text": "A host may pass a signed customer identity. An absent or invalid signature leaves the visitor anonymous and exposes no customer history; a valid identity links only the named customer."
        },
        {
          "id": "CUST-005",
          "title": "Consequential identity basis.",
          "text": "A customer-specific decision records the verified contact, signed identity, or operator-recorded verification tying its target to the customer, including method and time. Approval refuses without this basis."
        },
        {
          "id": "CUST-006",
          "title": "People, companies, and history.",
          "text": "A permitted member can view people, companies, and their relationships. A customer history lists that customer's conversations, cases, action outcomes, receipts, and activity in chronological order without changing primary-record ownership."
        },
        {
          "id": "CUST-007",
          "title": "Customer merge.",
          "text": "A permitted member can merge two customers only after reviewing both identities and histories. The canonical customer receives every linked conversation, case, action outcome, receipt, and permitted profile record; the losing customer becomes an auditable pointer preserving source lineage and both identities. The merge never exposes Tracker content to a customer."
        },
        {
          "id": "CUST-008",
          "title": "Customer block.",
          "text": "A permitted operator can block or unblock a customer. While blocked, new messages are preserved but create no alert, AI request, automatic state change, or case."
        },
        {
          "id": "CUST-009",
          "title": "Verified contact points.",
          "text": "A contact becomes verified only through inbound mail, a valid signed identity, or its owner opening a one-time confirmation link. It records method, time, and actor; unverified contacts receive no case content, receipt link, transcript, or customer delivery."
        },
        {
          "id": "CUST-010",
          "title": "Import and segments.",
          "text": "An operator can import and manage customers, companies, tags, and segments only through IMPORT-001 and declared contact rules; imports cannot infer customer identity from Tracker aggregation."
        },
        {
          "id": "EVID-001",
          "title": "Evidence records.",
          "text": "Each evidence record names type, source system, immutable source identifier, retrieval time, verification state, and requesting case. In every operator presentation card numbers, bank-account numbers, and provider secrets show only their last four characters where shown at all."
        },
        {
          "id": "EVID-002",
          "title": "Source truth.",
          "text": "Provider data and uploaded originals remain distinguishable from AI extraction or summary. AI output alone never verifies evidence."
        },
        {
          "id": "EVID-003",
          "title": "Collection failure.",
          "text": "Missing, not-found, or errored evidence is shown unavailable with reason and attempt time and is never replaced by generated content. Required unavailable evidence blocks a decision."
        },
        {
          "id": "EVID-004",
          "title": "Decision snapshot evidence.",
          "text": "A decision snapshot records the customer identity basis, action target and parameters, evidence IDs and verification states, exact knowledge or policy revision and section, proposed customer text, preparer, and rationale reviewed."
        },
        {
          "id": "EVID-005",
          "title": "Revision invalidation.",
          "text": "Changing cited evidence, knowledge, target, parameter, action, or customer message creates a new decision version and invalidates any pending approval of the superseded version."
        },
        {
          "id": "EVID-006",
          "title": "Historical integrity.",
          "text": "Evidence and knowledge revisions cited by a decision remain readable and unchanged after live sources change; a receipt renders from cited versions, subject to DATA-002 anonymisation."
        },
        {
          "id": "EVID-007",
          "title": "Evidence pack.",
          "text": "An authorised operator can export a case's permitted messages, evidence metadata and originals, decision versions, outcomes, and receipt as a portable archive without Tracker disclosure to a customer."
        },
        {
          "id": "EVID-008",
          "title": "Required evidence.",
          "text": "Each declared executable operation names the verified evidence required before approval. *Policy: `evidence.requirements.v2`; default: the operation declaration's verified evidence and current identity basis, while no-action requires a documented reason.*"
        },
        {
          "id": "KNOW-001",
          "title": "Knowledge library.",
          "text": "Operators can create, revise, organise, archive, and search structured articles and guidance. A revision has draft, in-review, published, or outdated state and immutable history."
        },
        {
          "id": "KNOW-002",
          "title": "Policy at event time.",
          "text": "An operational recommendation or decision selects the published knowledge or policy revision effective at the cited transaction or event time, or at case creation when no transaction/event exists, and freezes that exact revision and section into EVID-004 and ACT-010."
        },
        {
          "id": "KNOW-003",
          "title": "Private material boundary.",
          "text": "Internal notes, customer messages, account data, security reports, and one-off concessions become reusable knowledge only through an operator-authored saved revision."
        },
        {
          "id": "KNOW-004",
          "title": "Reply templates.",
          "text": "Operators can create, edit, retire, and insert named templates. Insertion fills a review editor and sends nothing."
        },
        {
          "id": "KNOW-005",
          "title": "Public Help Center.",
          "text": "Published public articles may appear in a Help Center with customer-safe routes and revision history. It never exposes private notes, Back-office content, Tracker content, decision evidence, or drafts."
        },
        {
          "id": "KNOW-006",
          "title": "Source hub.",
          "text": "An operator can declare a knowledge source and see enabled, ingesting, stale, failed, or disabled state with the source boundary and last retrieval."
        },
        {
          "id": "KNOW-007",
          "title": "Content suggestions.",
          "text": "AI may prepare a knowledge-content suggestion for operator review. It is not published or used by an assistant until an operator saves and publishes a revision."
        },
        {
          "id": "KNOW-008",
          "title": "Library health.",
          "text": "The library identifies stale or failed source material and published revisions needing review; it labels unavailable inputs rather than inventing freshness."
        },
        {
          "id": "AI-001",
          "title": "Requested preparation.",
          "text": "A member can request a summary, classification, extraction, recommendation, reply draft, or content suggestion. The result is labelled AI, linked to its model call, and neither sent nor acted on."
        },
        {
          "id": "AI-002",
          "title": "Grounding.",
          "text": "Every AI output names the conversation, evidence, customer fields, and knowledge revisions it read. An uncited requested fact is returned as missing, not invented."
        },
        {
          "id": "AI-003",
          "title": "Recommendation contents.",
          "text": "An actionable recommendation states the proposed operation, target, customer consequence, rationale, confidence, missing evidence, and editable customer text where a message may be needed."
        },
        {
          "id": "AI-004",
          "title": "No AI authority.",
          "text": "AI cannot send a customer message, approve, execute, merge, complete, verify evidence, change a customer, change knowledge, or change a permission."
        },
        {
          "id": "AI-005",
          "title": "Triage.",
          "text": "When `ai.provider.v1` is configured, within 10 seconds of each non-automatic customer message Beacon records exactly one of: a labelled intent, priority suggestion, and EVID-008 checklist; an explicit no-match that changes no case field; or the AI-008 failure record. A member reviews any suggested queue or priority before it changes work state."
        },
        {
          "id": "AI-006",
          "title": "Mandatory review.",
          "text": "A case meeting a mandatory-review trigger is labelled Mandatory review and receives no recommendation until a member asks. On another case whose AI-005 intent names a resolution type, Beacon may prepare one only after required EVID-008 evidence is complete. Neither path lets AI act. *Policy: `ai.review-boundary.v1`; default: no CUST-005 identity basis, a customer request for a human, or an AI intent of security, legal, or account access; an override may add triggers but cannot remove these triggers or the JUDG-004 gate.*"
        },
        {
          "id": "AI-007",
          "title": "Instructions.",
          "text": "Drafts and recommendations follow buyer-authored instructions that never override evidence, policy, source boundary, approval, action, or privacy constraints. *Policy: `ai.instructions.v1`; default: answer concisely from cited case and knowledge sources only.*"
        },
        {
          "id": "AI-008",
          "title": "Provider failure.",
          "text": "With `ai.provider.v1` unconfigured, AI controls are visibly unavailable and triage is skipped. When a call times out or fails, the member sees the recorded error, the case stays usable, no decision is created, and no other case transition happens."
        },
        {
          "id": "AI-009",
          "title": "Cost.",
          "text": "Every AI call records provider, model, feature, input and output tokens, latency, and estimated cost, and operators can inspect totals by day and feature."
        },
        {
          "id": "AI-010",
          "title": "Proposal boundary.",
          "text": "A recommendation becomes a decision only when a signed-in member saves a reviewed immutable ACT-010 decision snapshot. A recommendation identifier is refused wherever a decision version is required, and accepting it records the member who accepted it."
        },
        {
          "id": "AI-011",
          "title": "Untrusted sources.",
          "text": "Customer messages, attachments, provider fields, retrieved evidence, and knowledge content are data, never instructions. They cannot widen source access, action permissions, or fields sent through `ai.provider.v1`."
        },
        {
          "id": "AI-012",
          "title": "Customer language.",
          "text": "A customer record stores the language of their messages, detected on first contact and editable by a member. A member sees a labelled translation of a message outside the configured workspace language before deciding; an AI-drafted customer message is written in the customer language. Without `ai.provider.v1` the language is member-set and no translation is shown."
        },
        {
          "id": "AI-013",
          "title": "Test set and review.",
          "text": "Operators can add manual questions, generated questions, or imported CSV questions to an AI test set, and review source, expected answer, rating, and notes before any deployment preparation."
        },
        {
          "id": "AI-014",
          "title": "Deployment preparation.",
          "text": "Operators can prepare an AI deployment audience, channel, escalation path, and version for review. Preparation is disabled for live sending or external action until JUDG-004, ACT-011, and channel approvals are satisfied."
        },
        {
          "id": "AI-015",
          "title": "Performance analysis.",
          "text": "Operators can inspect labelled AI preparation performance and zero-involvement conversations with source and date range; no success-looking chart proves delivery or correctness."
        },
        {
          "id": "AI-016",
          "title": "Usage guard.",
          "text": "A workspace can set usage alerts and a hard limit. At its limit, AI preparation stops and conversations route to permitted teammates; no existing customer message or provider outcome is fabricated."
        },
        {
          "id": "AI-017",
          "title": "Workflow preparation.",
          "text": "Operators can prepare an internal reviewable workflow. It cannot activate an autonomous customer send, external provider mutation, or an extension bypass of JUDG-004."
        },
        {
          "id": "AI-018",
          "title": "Deferred Fin navigation.",
          "text": "Fin Tasks, Fin Suggestions, and Simple automations are known visible navigation labels whose working workflows are unobserved. They are not 1.0 capability promises and do not appear as active edition functionality."
        },
        {
          "id": "JUDG-001",
          "title": "Eligible recommendation.",
          "text": "A case may have one current recommendation naming a permitted operation or escalation, rationale, customer consequence, risk, confidence, and cited facts. A recommendation is eligible only with its declared evidence and published knowledge."
        },
        {
          "id": "JUDG-002",
          "title": "Evidence provenance.",
          "text": "Every recommendation fact and citation preserves source identifier, captured value, source version, and retrieval time, viewable by the reviewer."
        },
        {
          "id": "JUDG-003",
          "title": "Published policy.",
          "text": "An executable recommendation names the exact published knowledge revision and section it applies."
        },
        {
          "id": "JUDG-004",
          "title": "Human gate.",
          "text": "Beacon sends no customer message until a signed-in member holding either the conversation-send grant for a CONV-014 conversation-scoped send, whether or not that conversation has linked cases, or the case-type decide grant for a case decision approves the reviewed send or submits a manual resolution. Beacon makes no external-provider mutation until a signed-in member holding the case-type decide grant approves the reviewed case decision or submits a manual resolution. No policy, setting, extension, importer, workflow, or edition removes either requirement."
        },
        {
          "id": "JUDG-005",
          "title": "Manual resolution.",
          "text": "A signed-in eligible member can select a different permitted operation, no-action resolution, or escalation, record an internal reason, and submit a reviewed manual decision. The original recommendation and evidence remain beside it."
        },
        {
          "id": "JUDG-006",
          "title": "Freshness gate.",
          "text": "If any reviewed decision field, evidence, identity basis, knowledge revision, target, or provider preflight condition changes before approval, approval is refused and the affected fields are named."
        },
        {
          "id": "JUDG-007",
          "title": "Insufficient evidence.",
          "text": "A case with missing, conflicting, or ineligible evidence shows its reason and offers only investigation, escalation, or a documented no-action resolution. It presents no executable AI recommendation."
        },
        {
          "id": "JUDG-008",
          "title": "AI failure boundary.",
          "text": "AI failure retains manual investigation and existing approved work but produces no silent substitute evidence, classification, recommendation, or customer send."
        },
        {
          "id": "JUDG-009",
          "title": "Instruction boundary.",
          "text": "Buyer guidance can alter preparation wording or permitted source selection only through AI-007; it cannot relax JUDG-002, JUDG-004, JUDG-006, or EVID-008."
        },
        {
          "id": "JUDG-010",
          "title": "Reversibility.",
          "text": "A recommendation or manual decision states whether its declared provider operation is reversible and names the reversal operation where one exists. An irreversible operation is labelled wherever it can be approved."
        },
        {
          "id": "JUDG-011",
          "title": "Named review.",
          "text": "The review surface shows the case type, customer identity basis, action target, evidence, knowledge, proposed text, decision version, and approving member so a reviewer can reject or return it before action."
        },
        {
          "id": "APPR-001",
          "title": "Approval request.",
          "text": "A request is created only for a stored current decision version meeting EVID-008 and CUST-005. It names the case, target, operation, requester, reviewers, and requested time."
        },
        {
          "id": "APPR-002",
          "title": "Approval queue.",
          "text": "Approval requests are listed by case lifecycle state—awaiting_approval, returned, declined, executing, action_failed, action_indeterminate, awaiting_message, notification_failed, or completed—with due state and assignee; every row links to the exact decision version reviewed."
        },
        {
          "id": "APPR-003",
          "title": "Review.",
          "text": "The eligible reviewer sees the complete EVID-004 snapshot, including exact policy section, and can open every cited source before deciding. The surface identifies preparer, reviewer, and decision version."
        },
        {
          "id": "APPR-004",
          "title": "Decisions.",
          "text": "An eligible reviewer can approve, return for changes, or decline. Return and decline require a reason, and every decision records the deciding member and time."
        },
        {
          "id": "APPR-005",
          "title": "Return.",
          "text": "Returning a decision moves the case to returned, preserves the reviewed snapshot and reason, and requires a new decision version before approval can be requested again."
        },
        {
          "id": "APPR-006",
          "title": "Decline.",
          "text": "Declining a decision preserves the reviewed snapshot and reason, moves the case to declined, sends no provider command or customer message, and leaves the case for a member to re-propose or complete only under CASE-010."
        },
        {
          "id": "APPR-007",
          "title": "Single decision.",
          "text": "One decision version accepts one terminal decision. A repeated click or request returns the stored decision and creates no further action."
        },
        {
          "id": "APPR-008",
          "title": "Approval command.",
          "text": "Once APPR-009 succeeds for an executable provider operation, approval atomically stores the decision, moves the case to executing, and creates exactly one durable provider command for that decision. A documented no-action decision atomically stores its decision and creates no provider command. Neither is provider execution, customer delivery, or case completion."
        },
        {
          "id": "APPR-009",
          "title": "Approval preconditions.",
          "text": "Immediately before approval Beacon refetches and revalidates case state, current decision version, case-type decide grant, policy eligibility, CUST-005 identity basis, and cited evidence versions and verification states. An executable provider operation requires ACT-009 eligibility and ACT-011 provider preflight. A documented no-action requires CASE-010 guards and EVID-008 evidence, but no provider state or provider preflight. An unavailable required authoritative state or mismatch returns a conflict, audits the rejected attempt, and stores neither decision nor command."
        },
        {
          "id": "ACT-001",
          "title": "Action adapter.",
          "text": "A declared external operation executes through `operations.provider.v1` with target, parameters, reason, and Beacon idempotency key. A concrete operation is executable only when ACT-009 and ACT-011 admit it."
        },
        {
          "id": "ACT-002",
          "title": "Idempotency.",
          "text": "Repeated approval clicks, queue retries, Worker restarts, and duplicate provider webhooks refer to the same durable action and never issue a second logical action."
        },
        {
          "id": "ACT-003",
          "title": "Outcome states.",
          "text": "An action is in exactly one of pending, succeeded, failed, or indeterminate and records redacted provider request and outcome fields, provider event IDs, and reconciliation attempts."
        },
        {
          "id": "ACT-004",
          "title": "Indeterminate result.",
          "text": "A timeout, lost connection, or ambiguous provider response makes the action indeterminate and blocks further mutation on its target until ACT-005 establishes whether the original command took effect."
        },
        {
          "id": "ACT-005",
          "title": "Reconciliation.",
          "text": "Provider events and scheduled status checks update an action idempotently. The scheduled check runs at least every 15 minutes on a pending or indeterminate action for up to seven days; a late authoritative success or failure updates that same action and case. An action unresolved after seven days becomes indeterminate if pending, stops being checked, and alerts the responsible member and assignee for manual reconciliation."
        },
        {
          "id": "ACT-006",
          "title": "Completion.",
          "text": "Approval and provider acceptance do not complete a case. An executable-action case reaches completed only after operations.provider.v1 reports the action succeeded and its required reviewed customer message is recorded sent. Failed or indeterminate actions cannot complete a case; documented no-action completion remains governed by CASE-010."
        },
        {
          "id": "ACT-007",
          "title": "Failed action.",
          "text": "A definitively failed action retains its decision and can be replaced only by a new decision that records it and carries a new idempotency key. An indeterminate action blocks replacement until reconciled."
        },
        {
          "id": "ACT-008",
          "title": "Provider claims.",
          "text": "Beacon shows an arrival estimate, destination confirmation, external reference, or outcome detail only when the authoritative provider supplied it, and labels unavailable information as pending or unknown."
        },
        {
          "id": "ACT-009",
          "title": "Action eligibility.",
          "text": "An operation must match EVID-008 required evidence, current identity basis, target, parameter constraints, and authoritative provider conditions; a change refuses approval or command submission."
        },
        {
          "id": "ACT-010",
          "title": "Immutable decision snapshot.",
          "text": "A submitted decision snapshots exactly the case, customer identity basis, actor, operation, parameters, action target, customer consequence, evidence IDs and states, exact knowledge or policy revision and section, and submission time. It cannot be edited or cancelled."
        },
        {
          "id": "ACT-011",
          "title": "Immediate provider preflight.",
          "text": "Immediately before command submission, Beacon reads current authoritative provider state and refuses unless target, customer identity, operation, parameters, and declared conflict conditions still match the decision. The command carries the current provider version or equivalent precondition."
        },
        {
          "id": "ACT-012",
          "title": "Execution progress.",
          "text": "A decision shows validation, provider execution, outcome recording, and customer-draft preparation, with start and completion times for each completed step."
        },
        {
          "id": "ACT-013",
          "title": "Customer-message preparation and send.",
          "text": "A confirmed or documented outcome can prepare customer text stating only what authoritative outcome permits and labelling uncertainty. It never sends itself. A signed-in member reviews and approves or manually resolves the send under JUDG-004; only then may CHAN-005 or CHAN-007 deliver it."
        },
        {
          "id": "ACT-014",
          "title": "One in-flight target.",
          "text": "At most one decision for a case and action target is validating or executing at a time. A second approval returns the existing state rather than starting another attempt."
        },
        {
          "id": "ACT-015",
          "title": "Conditional mutation.",
          "text": "An operation whose adapter cannot provide a current-version or equivalent conditional mutation is never executable; it is offered only as escalation or manual investigation."
        },
        {
          "id": "ACT-016",
          "title": "Execution pause.",
          "text": "An authorised member can pause external execution for the deployment. Intake, review, and approvals remain available, but no provider command is sent; resumption revalidates each command under ACT-011."
        },
        {
          "id": "ACT-017",
          "title": "Provider outage.",
          "text": "When an operations provider cannot read or preflight, Beacon names the outage, refuses provider-action approval, and preserves intake, manual investigation, and no-action resolution. It never queues an unpreflighted mutation as if ready."
        },
        {
          "id": "ACT-018",
          "title": "Reversal.",
          "text": "A provider-declared reversible action is reversed only by a new case and new decision naming the original decision. Original snapshots and receipts remain immutable and linked."
        },
        {
          "id": "ACT-019",
          "title": "Declared operation boundary.",
          "text": "An executable operation declares its provider adapter, target shape, identity precondition, required evidence, reversibility, and receipt shape. Beacon does not invent a generic provider adapter or operation from a UI field."
        },
        {
          "id": "RCPT-001",
          "title": "Receipt creation.",
          "text": "A receipt is created only when CASE-010 permits completed resolution. It is immutable and holds customer and public-case references where applicable, completion basis, acting member and time, exact evidence and knowledge/policy IDs, declared action parameters including amount and currency when financial, action target, authoritative outcome, required customer-message body and delivery state, and chronological activity. A pending or failed delivery remains an operational action view, not a completed-resolution receipt; a documented no-action receipt names its reason."
        },
        {
          "id": "RCPT-002",
          "title": "Action record before completion.",
          "text": "Before CASE-010 completion an authorised member sees an action or outcome-progress record labelled approved, executing, pending, succeeded, failed, indeterminate, or reconciled as applicable. It is not a completed-resolution receipt. A completed receipt is created only under RCPT-001 and may receive append-only amendments under RCPT-004; approval, submission, and provider acceptance are never success."
        },
        {
          "id": "RCPT-003",
          "title": "Customer receipt access.",
          "text": "An authorised member can view and download a receipt from its case and the customer history. A Customer-case customer receives one signed link to one redacted receipt that expires 30 days after issue and then returns an expired page with no receipt content. The receipt excludes internal notes, Back-office material, Tracker material, private evidence, and operator rationale."
        },
        {
          "id": "RCPT-004",
          "title": "Amendments.",
          "text": "Later events never rewrite a receipt snapshot. A late provider or delivery event creates an append-only amendment presented beside the original."
        },
        {
          "id": "RCPT-005",
          "title": "Per-target ownership.",
          "text": "A receipt belongs to one case and one declared action target. Tracker aggregation cannot create a group receipt or allow one decision receipt to cover unstated customers or targets."
        },
        {
          "id": "NOTIFY-001",
          "title": "Customer notification truth.",
          "text": "A case outcome notification is sent only under JUDG-004 and ACT-013 and states only the authoritative or documented outcome permitted for its case and target. An ordinary conversation-scoped customer send is sent only under JUDG-004 and CONV-014, whether or not its conversation has linked cases; it is a reviewed conversation delivery, not a case outcome, action, command, or receipt."
        },
        {
          "id": "NOTIFY-002",
          "title": "Operator routing.",
          "text": "Case creation, assignment, escalation, approval request, return, failure, indeterminate outcome, and delivery failure notify authorised members. *Policy: `notification.routing.v1`; default: conversation or case assignee, otherwise approving member, otherwise eligible operators.*"
        },
        {
          "id": "NOTIFY-003",
          "title": "Quiet hours.",
          "text": "Configured quiet hours hold routine email alerts until the next working period while failure and indeterminate-outcome alerts send immediately. In-app alerts are not held."
        },
        {
          "id": "NOTIFY-004",
          "title": "Coalescing.",
          "text": "At most one unread customer-reply alert and one unread approval-needed alert are outstanding per member per case; later matching events update it."
        },
        {
          "id": "NOTIFY-005",
          "title": "Delivery status.",
          "text": "Every operator alert and customer notification records provider acceptance, delivery where confirmed, bounce, retry, and terminal failure without changing a confirmed provider outcome."
        },
        {
          "id": "NOTIFY-006",
          "title": "Customer-message failure.",
          "text": "A failed required customer message moves the case to notification_failed, preserves the action outcome, and alerts the responsible operator until reviewed delivery is resolved."
        },
        {
          "id": "NOTIFY-007",
          "title": "No loops.",
          "text": "Operator notifications, receipt links, transcripts, and optional customer notices use a no-reply address distinct from the support address. Mail to it creates no conversation, case, alert, or AI request."
        },
        {
          "id": "ANALYTICS-001",
          "title": "Event-derived metrics.",
          "text": "Analytics derive from immutable conversation, case, decision, action, notification, and feedback events; editing a live record never rewrites an earlier reporting event."
        },
        {
          "id": "ANALYTICS-002",
          "title": "Conversation and outcome definitions.",
          "text": "Reports distinguish first response, conversation volume, customer message delivery, decisions, approvals, provider-confirmed success, failure, indeterminate outcome, reconciliation, and no-action resolution."
        },
        {
          "id": "ANALYTICS-003",
          "title": "Filters and drill-down.",
          "text": "A report names its date range, workspace timezone, completion watermark, and filters. Each aggregate drills into the authorised source conversations, cases, or actions it counted."
        },
        {
          "id": "ANALYTICS-004",
          "title": "Exports and unavailable metrics.",
          "text": "An operator can export authorised report rows. A zero denominator or missing input is shown unavailable with reason, never fabricated as zero."
        },
        {
          "id": "ANALYTICS-005",
          "title": "Custom charts.",
          "text": "Operators can prepare saved report charts from declared event dimensions and measures. A chart does not expose Tracker data to a customer or change operational truth."
        },
        {
          "id": "ANALYTICS-006",
          "title": "Conversation topics.",
          "text": "Operators can review conversation-topic analysis with its source range and model provenance. AI-derived topics remain analysis, not verified customer facts."
        },
        {
          "id": "SETUP-001",
          "title": "Guided setup.",
          "text": "A workspace can present a setup path for sign-in, teammate grants, inboxes, verified channels, widget origins, knowledge, provider adapters, and deployment checks. It labels unconfigured steps rather than claiming readiness."
        },
        {
          "id": "DATA-001",
          "title": "Portable export contents.",
          "text": "An authorised operator export contains every permitted customer, visitor, conversation, message, case, tag, evidence, knowledge revision, decision, approval, action, provider event, receipt, notification, setting, and audit entry, plus permitted attachment originals."
        },
        {
          "id": "DATA-002",
          "title": "Retention after erasure, superseding BASE-DATA-003.",
          "text": "Erasing a customer removes their conversations, messages, attachments, evidence originals, and profile under the baseline five-minute cascade, and retains every approval, action, provider event, and receipt with customer reference, message bodies, and contact points anonymised because the business must prove completed operations. The erasure confirmation names what remains and why. The rest of BASE-DATA-003, including its cascade and confirmation, applies unchanged."
        },
        {
          "id": "IMPORT-001",
          "title": "Intercom export importer.",
          "text": "Beacon has one 1.0 importer: Intercom export import. It reads a real Intercom export and declares supported entities, links, attachment treatment, source-ID/timestamp/visibility/history preservation, deterministic idempotency, rejected-record diagnostics, and truthful unsupported-data handling before it imports. It never activates a live Fin reply, workflow, outbound send, provider mutation, public sharing, or Tracker disclosure from source data. Real export proof and implementation are Stage 2 work."
        }
      ],
      "hasReadme": true,
      "hasBaseline": true,
      "nonGoals": [
        "SMS and WhatsApp channel adapters are deferred beyond 1.0.",
        "In-app announcements, mobile push and carousel, product tours and checklists, and surveys are outside 1.0.",
        "A native mobile operator application is outside 1.0.",
        "Live autonomous AI replies, customer-message automation, and external-provider automation are outside 1.0.",
        "Generic workflow and runtime plugin systems are outside 1.0.",
        "Multiple workspaces or tenants are outside 1.0.",
        "Help Scout replacement and import are deferred until its report and importer qualify."
      ],
      "externals": [
        {
          "name": "mail-provider",
          "required": true,
          "requiredWhen": "",
          "reason": "An external mail provider receives customer email and delivers email outside the deployment, which owned Cloudflare primitives cannot do.",
          "data": [
            "Customer and sender email addresses.",
            "Customer message headers, bodies, and permitted attachments.",
            "Delivery identifiers and provider outcome requests."
          ],
          "adapters": [
            "mail.inbound.v1",
            "mail.sender.v1"
          ]
        },
        {
          "name": "operations-provider",
          "required": false,
          "requiredWhen": "A declared executable external operation is enabled.",
          "reason": "The external provider owns the action target and authoritative outcome, which the deployment cannot reproduce with an owned Cloudflare primitive.",
          "data": [
            "The reviewed operation target, approved parameters, reason, and Beacon idempotency key.",
            "Provider status and reconciliation requests for the declared operation."
          ],
          "adapters": [
            "operations.provider.v1"
          ]
        },
        {
          "name": "ai-provider",
          "required": false,
          "requiredWhen": "AI preparation is enabled.",
          "reason": "A replaceable AI provider performs requested preparation that no owned Cloudflare primitive provides.",
          "data": [
            "The operator-requested task context and cited workspace sources selected for AI preparation."
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "Unmeasured: local resource use and declared provider pricing/rate bases still need recording; this deployable-only wave does not require a hosted runtime."
      },
      "extensionPoints": [
        "app.routes.v1",
        "app.pages.v1",
        "app.navigation.v1",
        "app.settings.v1",
        "jobs.consumers.v1",
        "schedules.cron.v1",
        "approval.eligibility.v2",
        "conversation.assignment.v1",
        "case.assignment.v1",
        "case.due-time.v1",
        "evidence.requirements.v2",
        "ai.review-boundary.v1",
        "ai.instructions.v1",
        "notification.routing.v1",
        "conversation.created.v1",
        "conversation.status-changed.v1",
        "conversation.merged.v1",
        "message.received.v1",
        "case.created.v1",
        "case.assigned.v1",
        "case.status-changed.v1",
        "case.escalated.v1",
        "case.merged.v1",
        "case.linked.v1",
        "evidence.collected.v1",
        "decision.created.v1",
        "approval.requested.v1",
        "approval.decided.v1",
        "action.submitted.v1",
        "action.succeeded.v1",
        "action.failed.v1",
        "action.indeterminate.v1",
        "customer-notification.status-changed.v1",
        "case.completed.v1",
        "receipt.created.v1",
        "import.completed.v1",
        "app.navigation.after.v1",
        "conversation.list.row-actions.v1",
        "conversation.detail.header.after.v1",
        "conversation.timeline.after.v1",
        "conversation.customer-context.after.v1",
        "conversation.linked-cases.after.v1",
        "case.detail.header.after.v1",
        "case.evidence.after.v1",
        "case.decision.after.v1",
        "case.actions.after.v1",
        "approval.review.after.v1",
        "customer.profile.tabs.after.v1",
        "resolution.receipt.after.v1",
        "settings.sections.after.v1",
        "analytics.dashboard.after.v1",
        "mail.inbound.v1",
        "mail.sender.v1",
        "operations.provider.v1",
        "ai.provider.v1"
      ],
      "limits": {
        "status": "Estimated bootstrap rate limits are implemented but unmeasured; capacity limits await a load test.",
        "publicRequestsPerIpPerMinute": 60,
        "magicLinkRequestsPerIpPer15Minutes": 20,
        "magicLinkRequestsPerEmailPer15Minutes": 5,
        "magicLinkVerificationRequestsPerIpPer15Minutes": 30,
        "inboundAttachmentBytes": 26214400
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "Not yet verified for the conversation-desk contract declaration."
      },
      "landing": "assets/seeds/beacon/01-owner-front-door.jpg",
      "shots": [
        "assets/seeds/beacon/01-owner-front-door.jpg",
        "assets/seeds/beacon/02-resolution-desk.jpg",
        "assets/seeds/beacon/03-evidence-review.jpg"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/beacon",
        "release": "0.2.0",
        "manifestSha256": "fba1d0a56fcd93a60ac6ff066e194f41d22f689b03fa00f45df210058fb566c7"
      }
    },
    {
      "id": "brook",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Brook",
      "dir": "freshdesk",
      "category": "support",
      "categoryLabel": "Support",
      "spine": {
        "id": "approved-support-action",
        "record": "a customer support request with an immutable operator-approved refund decision and provider-confirmed outcome"
      },
      "composition": {
        "base": "base",
        "family": "support",
        "edition": "brook",
        "modules": [
          "support-intake",
          "support-email",
          "sendgrid-email"
        ]
      },
      "replaces": [
        {
          "name": "Freshdesk",
          "edition": null
        }
      ],
      "version": "0.2.0",
      "oneLiner": "Email-first support with recorded customer evidence and operator-approved Stripe refunds.",
      "summary": "Email-first support with recorded customer evidence and operator-approved Stripe refunds.",
      "scope": "One business, native operator sessions, shared requests.",
      "maturity": "Bounded operator-approved refund workflow verified locally (Workers/D1 and desktop/mobile browser); hosted provider sandbox and full retained contract acceptance pending.",
      "clauseCount": 92,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Owner identity plate.",
          "text": "`GET /` presents the configured owner name, one configured statement of what the internal support workspace is for, and a sign-in link to `/signin`. It contains no pricing, feature tour, testimonials, metrics, or other marketing fabrication. When the configured credit toggle is enabled (its default), the footer contains the plain static link `Powered by Brook · runeditrun.com`; rendering it makes no request to that site, the seed author, or any other third party."
        },
        {
          "id": "OPS-001",
          "title": "Equal operators.",
          "text": "Every operator can see and act on every ticket, customer, view, and setting except where a policy restricts a specific action. There are no roles, and no filtered list or assignment restricts who may open or act on a ticket."
        },
        {
          "id": "OPS-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. Any operator may invite. Any operator may remove any other operator, except the operator configured at setup, whom only they themselves can remove. A removal that would leave the deployment with no operator is rejected. Removing an operator unassigns their tickets and leaves their past messages, notes, and audit entries attributed to them. *Policy: `operator.management.v1`; default: any operator may invite or remove within these rules. A replacement may narrow who may invite or remove; it can never permit last-operator removal.*"
        },
        {
          "id": "OPS-003",
          "title": "Optimistic concurrency.",
          "text": "Every ticket mutation includes the expected ticket revision. A stale revision returns `409` with the current revision and creates no message, property change, status change, external action, or other partial effect."
        },
        {
          "id": "MAIL-001",
          "title": "Inbound email.",
          "text": "An inbound message that passes MAIL-002 and parses as an RFC 5322 message becomes a ticket, or appends to an existing ticket under MAIL-004, within 60 seconds. It creates no ticket when MAIL-014 quarantines it or CUST-006 blocks its sender. The stored message preserves the sender, every recipient, the subject, the text and sanitised HTML bodies, the threading headers, the provider's received time, and the accepted attachments."
        },
        {
          "id": "MAIL-002",
          "title": "Verified callbacks.",
          "text": "Every mail-provider webhook, including intake and delivery-status callbacks, is processed only after its configured signature is verified; an invalid signature returns `401` and creates no record, job, ticket, message, or attachment."
        },
        {
          "id": "MAIL-003",
          "title": "Delivery deduplication.",
          "text": "Re-delivery of the same provider event or RFC `Message-ID` produces one inbound message and records the duplicate as a no-op."
        },
        {
          "id": "MAIL-004",
          "title": "Conservative threading.",
          "text": "An inbound email appends to a ticket only when its `In-Reply-To` header, its `References` header, or the ticket reference of TKT-016 identifies that ticket and the sender is a known participant under MAIL-013; otherwise it creates a new ticket. Sender, subject, or body similarity alone never merges mail. The activity timeline records which of the three identifiers matched."
        },
        {
          "id": "MAIL-005",
          "title": "Multiple support addresses.",
          "text": "One RFC message addressed to more than one configured support address creates one inbound message. The ticket records every matched address and the single address it will reply from, which is the first match in the order the addresses are configured."
        },
        {
          "id": "MAIL-006",
          "title": "Reply submission.",
          "text": "Submitting a reply with a new idempotency key atomically creates one `pending` outbound message and one dispatch job. Reusing the key returns the original message and creates no additional dispatch."
        },
        {
          "id": "MAIL-007",
          "title": "Dispatch outcome.",
          "text": "Provider acceptance changes the message to `accepted` and records the provider message ID and threading headers within 60 seconds; acceptance does not mean delivery. Definitive transient failures are retried with the same provider idempotency key for at most five total attempts during one hour. A permanent failure becomes `failed`; an ambiguous outcome becomes `needs_review` and is not retried automatically."
        },
        {
          "id": "MAIL-008",
          "title": "Delivery reports.",
          "text": "A verified provider report changes an accepted message to `delivered`, `delayed`, `bounced`, or `rejected` and records the report time. Brook never labels a message delivered without an authoritative delivery report."
        },
        {
          "id": "MAIL-009",
          "title": "Automatic mail.",
          "text": "An inbound message is **non-actionable** when it carries an `Auto-Submitted` header whose value is not `no`, a `List-*` header, `Precedence: bulk`, or a delivery or read receipt, or when its sender's local part is `no-reply`, `noreply`, `mailer-daemon`, or `postmaster`. Non-actionable mail is retained and shown on the ticket but does not reopen it, notify operators, or trigger AI. Every other inbound customer message is **actionable**; clauses elsewhere use these two terms with this meaning."
        },
        {
          "id": "MAIL-010",
          "title": "Email attachments.",
          "text": "Accepted inbound attachments are private ticket attachments. An attachment rejected by the limits in `seed.json` leaves the message readable and records the filename and rejection reason; outbound attachments travel within those same limits."
        },
        {
          "id": "MAIL-011",
          "title": "Intake durability.",
          "text": "Brook acknowledges a verified intake callback only after a durable intake record exists. Failure before that returns `503` so the provider redelivers, and creates no partial ticket. A parse, storage, or enqueue failure after the intake record exists marks that record visibly failed with a retryable or terminal reason."
        },
        {
          "id": "MAIL-012",
          "title": "Participants.",
          "text": "A new inbound ticket records the sender as requester and non-support `To` and `Cc` addresses as participants; `Bcc` recipients are never disclosed. Operators may add or remove participants. A reply addresses the requester and current participants, excluding configured support and no-reply addresses."
        },
        {
          "id": "MAIL-013",
          "title": "Participant threading.",
          "text": "For MAIL-004, a known participant is the current requester or participant. A removed or unknown sender does not qualify even when the message contains a valid threading header or ticket reference."
        },
        {
          "id": "MAIL-014",
          "title": "Unknown destination.",
          "text": "A verified message with no configured support destination is quarantined without creating a ticket or message and displays the received destination and quarantine reason to operators."
        },
        {
          "id": "MAIL-015",
          "title": "Outbound identity.",
          "text": "A reply or acknowledgement is sent from the address the ticket recorded under MAIL-005, with the display name and signature configured in settings appended as the final block of the body, and carries the ticket reference of TKT-016 in the subject and in a Brook-issued header. Operator notifications instead use the no-reply sender of NOTE-005 and carry no signature."
        },
        {
          "id": "MAIL-016",
          "title": "Acknowledgement.",
          "text": "When acknowledgement is enabled in settings, the actionable message that creates a ticket produces exactly one automatic acknowledgement, stored as an outbound message on the ticket and subject to MAIL-006 through MAIL-008. No acknowledgement is sent for a ticket an operator created (TKT-014), for non-actionable mail (MAIL-009), for a blocked customer (CUST-006), or a second time on the same ticket."
        },
        {
          "id": "TKT-001",
          "title": "States.",
          "text": "A ticket is `new`, `in_progress`, `waiting_on_customer`, `on_hold`, or `resolved`. “Resolve & Close” sets `resolved`; `closed` is not a separate state. Every status change records the actor, previous state, next state, and time."
        },
        {
          "id": "TKT-002",
          "title": "Customer reopen.",
          "text": "An actionable message from an unblocked customer on a `resolved`, `waiting_on_customer`, or `on_hold` ticket atomically appends the message and changes the ticket to `in_progress`. *Policy: `ticket.reopen.v1`; default: retain the assignee and recalculate deadlines.*"
        },
        {
          "id": "TKT-003",
          "title": "Properties.",
          "text": "Operators can set priority (`low`, `medium`, `high`, or `urgent`), issue type, assignee, status, case-insensitive canonical tags, and configured ticket custom fields. Issue types and closure reasons are chosen from lists operators edit in settings. Each change records the actor, previous value, new value, and time in the activity timeline."
        },
        {
          "id": "TKT-004",
          "title": "Assignment.",
          "text": "New tickets are assigned by the assignment policy, and operators can assign or reassign them. An assignee removed under OPS-002 leaves the ticket unassigned. *Policy: `ticket.assignment.v1`; default: leave new tickets unassigned.*"
        },
        {
          "id": "TKT-005",
          "title": "Replies and notes.",
          "text": "A reply is customer-visible and enters the email delivery lifecycle. A private note is visually distinct from a reply, is never delivered to the requester or a participant on any channel, and is never used as the body of a reply or acknowledgement. Notes are included in the export required by `BASE-DATA-001`."
        },
        {
          "id": "TKT-006",
          "title": "Resolve.",
          "text": "Resolving a ticket requires an explicit confirmation that shows the reply, the status change, the closure reason selected from the settings list, and any external action. The result records each part independently with its own outcome, so a failed email or external action is never presented as successful, and the stored closure reason is the one confirmed."
        },
        {
          "id": "TKT-007",
          "title": "Filtered lists.",
          "text": "Operators can list tickets by any combination of status, priority, assignee, tag, issue type, source, deadline tier (QUEUE-002), and customer, with counts derived from the same filters."
        },
        {
          "id": "TKT-008",
          "title": "Operational sort.",
          "text": "The default order is breached first, then nearest active deadline, then priority from `urgent` to `low`, then most recent customer activity, then ticket reference. Operators may instead sort by newest or oldest customer activity, with ticket reference as the final tiebreak."
        },
        {
          "id": "TKT-009",
          "title": "Search.",
          "text": "Search covers ticket reference, subject, message text, note text, customer and participant names and email addresses, and surfaced order, charge, and refund identifiers, labels the field that matched, and returns within one second."
        },
        {
          "id": "TKT-010",
          "title": "Manual merge.",
          "text": "Operators may irreversibly merge duplicate tickets by choosing a primary; messages and attachments are ordered chronologically, each secondary ticket becomes a link to the primary and keeps its own reference, no customer message is sent, and the actor and reason are audited. *Policy: `record.destructive-actions.v1`.*"
        },
        {
          "id": "TKT-011",
          "title": "Deletion.",
          "text": "Deleting a ticket removes its messages and attachments under `BASE-DATA-003`. *Policy: `record.destructive-actions.v1`.*"
        },
        {
          "id": "TKT-012",
          "title": "Unread.",
          "text": "A ticket is unread for an operator until they open it after the latest customer message; queue and navigation counts use that operator-specific state."
        },
        {
          "id": "TKT-013",
          "title": "Activity timeline.",
          "text": "Each ticket presents one chronological timeline of inbound messages, replies, notes, property and participant changes, threading decisions, delivery attempts and delivery reports, AI analyses, external actions, follow changes, feedback results, merges, and references to deleted records."
        },
        {
          "id": "TKT-014",
          "title": "Manual creation.",
          "text": "An operator can create a `new` ticket for an existing customer with a subject and optional private note. Its source is `manual`; a ticket created from inbound mail has source `email`, and these are the only two values TKT-007 filters on. Creation sends no customer message; any later reply follows the MAIL-006 delivery lifecycle."
        },
        {
          "id": "TKT-015",
          "title": "Manual reopen.",
          "text": "An operator can reopen a resolved ticket to `in_progress` after confirmation; the action retains its history and previous closure reason, records the actor and reason, and recalculates deadlines under `ticket.reopen.v1`."
        },
        {
          "id": "TKT-016",
          "title": "Ticket reference.",
          "text": "Every ticket has a reference that is unique for the life of the deployment and never reused after deletion or merge. It is shown wherever the ticket appears, carried in outbound mail under MAIL-015, matched by MAIL-004, and accepted by search under TKT-009."
        },
        {
          "id": "TKT-017",
          "title": "Following.",
          "text": "An operator follows a ticket once they are assigned it, reply to it, or add a note to it, and any operator may follow or unfollow any ticket. Followers are the audience for the ticket notifications named in NOTE-001; unfollowing stops those notifications for that operator only and changes nothing else about the ticket."
        },
        {
          "id": "CUST-001",
          "title": "Creation.",
          "text": "The first actionable inbound email from an address matching no customer creates one customer and links the ticket to it."
        },
        {
          "id": "CUST-002",
          "title": "Email identity.",
          "text": "Email addresses are compared case-insensitively after normalisation, and one address identifies one customer unless an operator explicitly merges records."
        },
        {
          "id": "CUST-003",
          "title": "Profile.",
          "text": "A customer has name, email addresses, phone, address, preferred channel, preferred language, marketing preference, segment, tags, and configured customer custom fields; operators can edit them with every change audited."
        },
        {
          "id": "CUST-004",
          "title": "Context.",
          "text": "A customer profile lists their tickets with reference, subject, status, assignee, last update, and feedback rating, and their satisfaction score under FB-004. When adapters are configured it also lists recent orders, payments, refunds, spend, and the provider retrieval time of each. Every figure shown is derived from data the deployment holds or from a named adapter response."
        },
        {
          "id": "CUST-005",
          "title": "Merge.",
          "text": "Operators may irreversibly merge two customers by selecting the surviving record; tickets, notes, and provider links follow it, conflicting fields are resolved explicitly, and the action is audited. *Policy: `record.destructive-actions.v1`.*"
        },
        {
          "id": "CUST-006",
          "title": "Blocking.",
          "text": "An operator can block a customer. Inbound mail from a blocked customer is retained as non-actionable blocked activity on the ticket MAIL-004 selects, or as a visible blocked-intake record when MAIL-004 selects none. It creates no ticket, reopen, operator notification, acknowledgement, or AI work. Unblocking affects only later messages."
        },
        {
          "id": "CUST-007",
          "title": "Erasure scope.",
          "text": "Deleting a customer removes their tickets, messages, notes, attachments, feedback results, and locally stored commerce references under `BASE-DATA-003`; retained audit entries anonymise the customer reference. *Policy: `record.destructive-actions.v1`.*"
        },
        {
          "id": "CUST-008",
          "title": "Manual creation.",
          "text": "An operator can create a customer with a name and at least one normalised email address. If that address already identifies a customer, Brook opens the existing record and creates no duplicate."
        },
        {
          "id": "CUST-009",
          "title": "Customer notes.",
          "text": "Operators can add, edit, and delete dated notes on a customer. A customer note records its author and time, is never sent to the customer, is attached to no ticket, and is included in the export."
        },
        {
          "id": "QUEUE-001",
          "title": "Situation board.",
          "text": "The situation board shows, for the currently selected filter: the open backlog; the count of urgent tickets; the count in each deadline tier of QUEUE-002; the number resolved today; the mean first-response time of QUEUE-007 over the last 24 hours; the satisfaction score of FB-004 over the last 7 days; and, for each state of TKT-001, the count and the longest current wait. Every count is derived from the tickets the equivalent TKT-007 filter returns and opens that filter when selected. The board displays data no more than 30 seconds old together with the time of the data it is showing."
        },
        {
          "id": "QUEUE-002",
          "title": "Deadline tiers.",
          "text": "An active ticket's nearest unmet first-response or resolution deadline places it in exactly one tier: `breached` once the deadline has passed, `at_risk` when it is two hours or less away, `due_soon` when it is more than two and at most six hours away, and `later` beyond that. Remaining time is measured on the same basis as the deadline itself (QUEUE-003). Breached tickets are counted and displayed separately from `at_risk`."
        },
        {
          "id": "QUEUE-003",
          "title": "Business calendar.",
          "text": "Settings define one IANA timezone, weekly working hours, and dated holidays. Deadline calculations consume only configured business time unless the deadline policy chooses calendar time."
        },
        {
          "id": "QUEUE-004",
          "title": "Deadline targets.",
          "text": "First-response and resolution targets are calculated from ticket priority and the business calendar. *Policy: `ticket.deadlines.v1`; default first-response/resolution targets in business minutes are urgent 15/120, high 60/480, medium 240/1440, and low 480/2400.*"
        },
        {
          "id": "QUEUE-005",
          "title": "Deadline display.",
          "text": "Every active ticket shows the applicable target, calendar basis, absolute due time with timezone, and time remaining or breached; a recalculation records its reason and the previous due time."
        },
        {
          "id": "QUEUE-006",
          "title": "Breach behavior.",
          "text": "Crossing a deadline marks the ticket breached and emits one notification event but never closes, resolves, or reassigns it."
        },
        {
          "id": "QUEUE-007",
          "title": "Measured times.",
          "text": "A ticket's first-response time is recorded when its first operator reply is accepted; an acknowledgement under MAIL-016 is never a first response. Its resolution time is recorded when it first becomes `resolved`. Both are stored as elapsed business time and elapsed calendar time, are shown on the ticket, and are the values QUEUE-001 averages. Reopening never overwrites a recorded first-response time."
        },
        {
          "id": "GUIDE-001",
          "title": "Policies.",
          "text": "Operators can create, edit, retire, and view versioned resolution policies; a retired version remains readable from historical tickets that cited it."
        },
        {
          "id": "GUIDE-002",
          "title": "Policy match.",
          "text": "A resolution plan that relies on a policy identifies the exact policy version and the matching passage in the operator view."
        },
        {
          "id": "GUIDE-003",
          "title": "Historical evidence.",
          "text": "Editing a policy affects only future analyses; stored plans retain the policy version and evidence originally used."
        },
        {
          "id": "GUIDE-004",
          "title": "Internal articles.",
          "text": "Operators can create, edit, and retire versioned internal knowledge articles with a title and body; AI may cite an active version, and historical citations continue to open the version originally used. Articles are never published to a customer-facing surface."
        },
        {
          "id": "AI-001",
          "title": "Resolution plan.",
          "text": "Each actionable new or reopened ticket queues an AI resolution plan, and an operator may refresh it. The plan appears within 30 seconds when the provider responds and contains a recommended next action, confidence, and evidence."
        },
        {
          "id": "AI-002",
          "title": "Evidence provenance.",
          "text": "Every fact used by a plan, draft, or summary identifies its source type, source record, and retrieval time; operators can open the local source detail before acting."
        },
        {
          "id": "AI-003",
          "title": "Grounding failure.",
          "text": "A fact whose evidence is missing, older than the adapter's declared freshness window, or contradicted by another source is labelled unknown or conflicting. A plan in that state produces no recommended action, no draft asserting the fact, and no claim that an action succeeded."
        },
        {
          "id": "AI-004",
          "title": "Suggested reply.",
          "text": "When an operator requests a suggested reply, Brook prepares an editable reply in the composer; only an operator can submit it, and the outbound message records that AI supplied the draft and whether the operator edited it."
        },
        {
          "id": "AI-005",
          "title": "Triage.",
          "text": "AI sets the ticket's issue type and a sentiment of `positive`, `neutral`, or `negative` within 30 seconds of an actionable customer message. It never overwrites an issue type an operator set, and an ungrounded classification remains unset rather than being replaced with a guessed default."
        },
        {
          "id": "AI-006",
          "title": "Summary.",
          "text": "An operator can request a stored summary of a ticket. The summary records when it was produced, links to the messages and evidence it covers, and is shown alongside the ticket timeline, never in place of it."
        },
        {
          "id": "AI-007",
          "title": "No autonomous effects.",
          "text": "AI cannot send mail, add a customer-visible message, change ticket or customer state, or invoke an external action. No policy or instruction may grant those effects."
        },
        {
          "id": "AI-008",
          "title": "Provider failure.",
          "text": "AI calls go through the required `ai.provider.v1` adapter. If a configured provider times out or fails at runtime, the affected plan, draft, triage, or summary shows the error, stores no partial result, and leaves email and human ticket handling available."
        },
        {
          "id": "AI-009",
          "title": "Cost.",
          "text": "Every AI call records feature, provider, model, input and output units, latency, and estimated cost, whether it succeeded or failed; settings show totals by day and feature."
        },
        {
          "id": "AI-010",
          "title": "Instructions.",
          "text": "The plan-instructions policy selects buyer-supplied instructions for analyses and drafts but cannot widen the permitted data scope, change authorisation, or bypass AI-007. *Policy: `ai.plan-instructions.v1`; default: use active resolution policies and require concise, evidence-backed, operator-reviewed output in the customer's language.*"
        },
        {
          "id": "AI-011",
          "title": "Stale results.",
          "text": "Each AI request captures the ticket revision and request sequence. A result for an older revision remains in history marked stale and never replaces the current result."
        },
        {
          "id": "ACT-001",
          "title": "Linked evidence.",
          "text": "When order or payment adapters are configured, Brook resolves customer and ticket references to orders, fulfilments, captures, refunds, and provider object IDs and displays the retrieval time."
        },
        {
          "id": "ACT-002",
          "title": "Evidence failure.",
          "text": "An unavailable adapter or failed lookup is shown as unavailable with its last successful retrieval time; stale fixture data is never substituted."
        },
        {
          "id": "ACT-003",
          "title": "Proposal boundary.",
          "text": "A refund is the only external mutation Brook performs; every other adapter call is a read. A recommended refund is a proposal only and has no external effect until an operator confirms it."
        },
        {
          "id": "ACT-004",
          "title": "Refund eligibility.",
          "text": "A refund may be proposed only when the eligibility policy accepts the linked payment and amount. *Policy: `commerce.refund-eligibility.v1`; default: the charge belongs to the customer, the amount is positive, and it does not exceed the provider-reported refundable balance.*"
        },
        {
          "id": "ACT-005",
          "title": "Explicit approval.",
          "text": "Every refund requires an operator confirmation showing the adapter, operation, target provider object, amount and currency, customer, reply, and idempotency key. No policy or AI instruction may bypass the confirmation."
        },
        {
          "id": "ACT-006",
          "title": "Idempotent ledger.",
          "text": "Brook durably records an action ledger entry before dispatch. Repeating a request with the same idempotency key returns the original entry and never creates a second external mutation."
        },
        {
          "id": "ACT-007",
          "title": "Action states.",
          "text": "A refund action is `pending`, `succeeded`, `failed`, or `needs_review`; the ticket shows provider references, attempt times, the approving operator, and the provider's latest response with card data and secrets redacted."
        },
        {
          "id": "ACT-008",
          "title": "Uncertain outcome.",
          "text": "A timeout or ambiguous provider response becomes `needs_review` and is never retried automatically or replaced with a new refund."
        },
        {
          "id": "ACT-009",
          "title": "Reconciliation.",
          "text": "An operator can reconcile a `needs_review` action against the provider by recording the verified provider outcome and reference; reconciliation updates the existing ledger entry and never dispatches a mutation."
        },
        {
          "id": "ACT-010",
          "title": "Combined resolution order.",
          "text": "When one confirmation includes a refund, success reply, and resolution, Brook dispatches the refund first. Only authoritative refund success permits the reply to be dispatched, and only provider acceptance of that reply permits the ticket to become `resolved`. Failure or `needs_review` stops later steps and displays completed and incomplete steps separately. Retrying reuses the original action and message idempotency keys."
        },
        {
          "id": "ACT-011",
          "title": "Adapter failure.",
          "text": "A terminal refund failure leaves the ticket unresolved unless the operator separately resolves it, sends no success claim, and remains visible for correction."
        },
        {
          "id": "ACT-012",
          "title": "Bounded transport retry.",
          "text": "A retryable transport failure known to have produced no provider effect may be retried at most three times with the original idempotency key; any ambiguity follows ACT-008."
        },
        {
          "id": "NOTE-001",
          "title": "Notification events.",
          "text": "Brook emails a notification for a new unassigned ticket, a direct assignment, a deadline breach, and an unattended customer follow-up, subject to the recipient's mute and quiet-hours settings. A new unassigned ticket goes to every operator; a direct assignment goes to the new assignee; a deadline breach and an unattended follow-up go to the ticket's followers (TKT-017), or to every operator when the ticket has none."
        },
        {
          "id": "NOTE-002",
          "title": "Coalescing.",
          "text": "Repeated customer messages or deadline events for the same ticket produce at most one notification of each kind between operator open-or-acknowledge cycles."
        },
        {
          "id": "NOTE-003",
          "title": "Quiet hours.",
          "text": "Per-operator quiet hours defer that operator's non-urgent notifications until the next allowed time, without delaying intake, deadline calculation, or another operator's notification. *Policy: `ticket.quiet-hours.v1`; default: urgent and breached tickets bypass quiet hours; other email notifications wait.*"
        },
        {
          "id": "NOTE-004",
          "title": "Notification failure.",
          "text": "A notification delivery failure is retried under MAIL-007, becomes visible after terminal failure, and never blocks or rolls back the ticket event."
        },
        {
          "id": "NOTE-005",
          "title": "No mail loops.",
          "text": "Operator notifications and acknowledgements use a configured no-reply sender that is not a support destination. Mail addressed to that return path is rejected and never creates or reopens a ticket."
        },
        {
          "id": "NOTE-006",
          "title": "Unattended follow-up.",
          "text": "When an actionable customer message remains unopened for five minutes, Brook queues one unattended notification to the ticket's followers, or to every unmuted operator when the ticket has none."
        },
        {
          "id": "FB-001",
          "title": "Resolution survey.",
          "text": "A resolution email includes a signed customer-feedback link when the feedback policy enables it. No survey is sent to a blocked customer, and reopening a ticket before its survey is sent cancels it. *Policy: `ticket.feedback.v1`; default: include one survey with the first customer-visible resolution reply.*"
        },
        {
          "id": "FB-002",
          "title": "Feedback access.",
          "text": "The feedback link discloses no ticket or customer detail beyond the business name, accepts one submission, and expires 30 days after it is sent."
        },
        {
          "id": "FB-003",
          "title": "Feedback result.",
          "text": "A customer can submit a whole-number rating from one to five with an optional comment. The result is stored with its submission time, appears on the ticket and the customer profile, cannot be edited by an operator, and does not reopen the ticket or trigger AI."
        },
        {
          "id": "FB-004",
          "title": "Satisfaction score.",
          "text": "A satisfaction score is the mean of the ratings received in the stated window, shown with the number of ratings it covers. A window with no ratings is shown as unavailable, never as zero."
        },
        {
          "id": "DATA-001",
          "title": "Full export contents.",
          "text": "The portable export contains every ticket, message, attachment, customer, customer note, tag, resolution policy and version, internal article and version, AI analysis and usage record, feedback result, notification, setting, commerce reference and action-ledger entry, and audit record; it contains no secret."
        },
        {
          "id": "DATA-002",
          "title": "Operational export.",
          "text": "An operator may export the currently filtered ticket list as CSV with ticket properties, deadline tier, and measured times; this convenience export does not replace the complete archive required by `BASE-DATA-001`."
        }
      ],
      "hasReadme": true,
      "hasBaseline": true,
      "nonGoals": [
        "multiple workspaces or tenants",
        "granular roles, groups, or private queues",
        "chat widgets, social, SMS, voice, or a public help centre",
        "outbound campaigns or a generic workflow builder",
        "automatic semantic ticket merging",
        "autonomous AI replies, state changes, or external actions",
        "external commerce mutations other than refunds",
        "a reporting module beyond the figures the situation board defines",
        "a library of operator-maintained canned replies",
        "marketing engagement signals such as email open and click rates",
        "native mobile applications",
        "arbitrary outbound webhooks",
        "a built-in payment or commerce system",
        "malware scanning"
      ],
      "externals": [
        {
          "name": "cloudflare-platform",
          "required": true,
          "requiredWhen": "",
          "reason": "Cloudflare Worker, D1 and optional Email Routing host owner-controlled application data and inbound email.",
          "data": [
            "support records",
            "native session hashes",
            "email bodies and metadata"
          ],
          "adapters": []
        },
        {
          "name": "sendgrid",
          "required": false,
          "requiredWhen": "customer replies or hosted magic links are sent through SendGrid",
          "reason": "Delivers operator-approved customer mail and native sign-in links.",
          "data": [
            "recipient email",
            "approved message text",
            "magic-link URL"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        },
        {
          "name": "stripe",
          "required": false,
          "requiredWhen": "Stripe payment evidence or refunds are enabled",
          "reason": "Authoritative payment evidence and explicitly approved refunds.",
          "data": [
            "charge identifier",
            "approved refund amount",
            "decision identifier"
          ],
          "adapters": [
            "commerce.payments.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "not measured",
        "currency": "USD",
        "assumptions": "One business deployment; usage depends on traffic and email/refunds.",
        "excluded": [
          "Cloudflare",
          "SendGrid",
          "Stripe fees"
        ]
      },
      "extensionPoints": [
        "mail.sender.v1",
        "commerce.payments.v1"
      ],
      "limits": {
        "status": "unverified-load-capacity",
        "note": "No load capacity claim; inbound parsing refuses messages over 2 MB.",
        "inboundMessageBytes": 2000000
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target; desktop/mobile browser journey checks only",
        "includes": [
          "operator app",
          "sign-in flow"
        ]
      },
      "landing": "assets/seeds/brook/verification-pilot-20260912-01-refund-review-desktop.png",
      "shots": [
        "assets/seeds/brook/verification-pilot-20260912-01-refund-review-desktop.png",
        "assets/seeds/brook/verification-pilot-20260912-03-provider-confirmed-refund.png",
        "assets/seeds/brook/verification-pilot-20260912-04-reviewed-reply-before-send.png",
        "assets/seeds/brook/verification-pilot-20260912-05-reply-accepted-desktop.png",
        "assets/seeds/brook/verification-pilot-20260912-07-native-inbound-desktop.png",
        "assets/seeds/brook/00-landing-1440x900.png"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/brook",
        "release": "0.2.0",
        "manifestSha256": "e9545310ef569769e60424e6c7664aba29a98b01489efa0c4289927a8a2dec28"
      }
    },
    {
      "id": "happytohelp",
      "authoring": {
        "mode": "edition-owned",
        "agentSource": "AGENTS.md",
        "id": "runeditrun/happytohelp",
        "parent": {
          "repository": "https://github.com/runeditrun/base.git",
          "revision": "d840a75b4c41ddc1feae99695d6632c8b0a451d2",
          "path": "foundation/template"
        },
        "toolchain": {
          "id": "runeditrun/edition-authoring",
          "version": "1.1.0",
          "sha256": "a13d7d0bcf174618923ca5d1134575c149ea6b57c89d1640c03c1ebf091ab372"
        },
        "components": [
          {
            "id": "runeditrun/base",
            "role": "foundation",
            "source": {
              "kind": "local",
              "paths": [
                "src/base",
                "scripts/edition-authoring",
                "scripts/operations.mjs"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "ac90a05c0549e1f8a65a94f64ac6e2d629f4d5e1",
                "path": ".",
                "paths": [
                  "foundation/runtime",
                  "edition-authoring",
                  "operations/operations.mjs"
                ]
              },
              "mappings": [
                {
                  "path": "src/base",
                  "originPath": "foundation/runtime"
                },
                {
                  "path": "scripts/edition-authoring",
                  "originPath": "edition-authoring"
                },
                {
                  "path": "scripts/operations.mjs",
                  "originPath": "operations/operations.mjs"
                }
              ]
            }
          },
          {
            "id": "runeditrun/onboarding",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "scripts/base-onboarding"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "390328736c8dae82f87c86e5af4c17cca6bb9c00",
                "path": "onboarding"
              }
            }
          },
          {
            "id": "runeditrun/sendgrid-email",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/sendgrid-email"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "d1343b638afd254679bc2b35819fc1d268bb6cc7",
                "path": ".",
                "paths": [
                  "modules/sendgrid-email/src"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/sendgrid-email",
                  "originPath": "modules/sendgrid-email/src"
                }
              ]
            }
          },
          {
            "id": "runeditrun/conversation",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/conversations",
                "migrations/0002_conversations.sql",
                "migrations/0004_message_intent.sql",
                "migrations/0007_conversation_history.sql",
                "tests/conversation-admission.mjs",
                "tests/conversation-socket.mjs",
                "tests/conversation-worker.ts"
              ]
            }
          },
          {
            "id": "runeditrun/widget",
            "role": "module",
            "requires": [
              "runeditrun/base",
              "runeditrun/conversation"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/widget",
                "scripts/build-widget.mjs",
                "tests/widget-transport.mjs",
                "tests/widget-native-lost-response.mjs",
                "tests/widget-browser.mjs",
                "tests/widget-browser-runtime.mjs",
                "tests/widget-lifecycle-browser.mjs"
              ]
            }
          },
          {
            "id": "runeditrun/ai-support",
            "role": "module",
            "requires": [
              "runeditrun/base",
              "runeditrun/conversation",
              "runeditrun/knowledge",
              "runeditrun/customer-context"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/ai-support",
                "migrations/020_ai_support.sql",
                "tests/ai-support"
              ]
            }
          },
          {
            "id": "runeditrun/knowledge",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/knowledge",
                "migrations/0050_knowledge.sql",
                "migrations/0051_knowledge_authoring.sql",
                "migrations/0052_knowledge_imports.sql",
                "tests/knowledge-context.mjs",
                "tests/knowledge-worker.ts",
                "tests/knowledge-workerd.mjs",
                "migrations/0053_knowledge_recovery.sql",
                "tests/knowledge-recovery.ts",
                "tests/knowledge-deleted-projects.ts"
              ]
            }
          },
          {
            "id": "runeditrun/customer-context",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/customer-context",
                "migrations/0060_customer_context.sql",
                "migrations/0060_customer_context_engine.sql",
                "tests/customer-context",
                "tests/customer-context-routes.mjs"
              ]
            }
          },
          {
            "id": "runeditrun/local-computer",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/local-computer",
                "local-agent",
                "scripts/build-local-agent.cjs",
                "scripts/local-agent-archive.cjs",
                "migrations/030_local_computer.sql",
                "tests/local-computer-worker.ts",
                "tests/local-computer-runtime.mjs",
                "tests/local-computer-revocation.mjs",
                "tests/local-computer-sweep-worker.ts",
                "tests/local-agent-archive.mjs"
              ]
            }
          },
          {
            "id": "runeditrun/training-runs",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/training-runs",
                "scripts/eval",
                "migrations/040_training_runs.sql",
                "tests/training",
                "tests/eval-harness",
                "migrations/041_training_model_usage.sql"
              ]
            }
          },
          {
            "id": "runeditrun/plugin-sdk",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/plugin-sdk",
                "tests/plugin-sdk.mjs"
              ]
            }
          },
          {
            "id": "runeditrun/site-preview",
            "role": "module",
            "requires": [
              "runeditrun/base"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/site-preview",
                "migrations/0096_site_preview.sql",
                "tests/site-preview.mjs",
                "tests/site-preview-worker.ts"
              ]
            }
          },
          {
            "id": "runeditrun/happytohelp",
            "role": "edition",
            "requires": [
              "runeditrun/base",
              "runeditrun/conversation",
              "runeditrun/widget",
              "runeditrun/ai-support",
              "runeditrun/knowledge",
              "runeditrun/customer-context",
              "runeditrun/local-computer",
              "runeditrun/training-runs",
              "runeditrun/plugin-sdk",
              "runeditrun/site-preview",
              "runeditrun/onboarding",
              "runeditrun/sendgrid-email"
            ],
            "source": {
              "kind": "local",
              "paths": [
                "src/platform",
                "src/development",
                "src/ui",
                "src/routes",
                "src/server.ts",
                "src/router.tsx",
                "src/modules/identity",
                "src/modules/management",
                "src/modules/integrations",
                "src/modules/legacy-import",
                "src/modules/public-site",
                "migrations/0001_identity.sql",
                "migrations/0003_identity_credentials.sql",
                "migrations/0005_widget_identity.sql",
                "migrations/0006_registration.sql",
                "migrations/0070_management.sql",
                "migrations/0071_management_preferences.sql",
                "migrations/0080_integrations.sql",
                "migrations/0081_billing_receipts.sql",
                "migrations/0082_github_oauth.sql",
                "migrations/0083_lead_operations.sql",
                "migrations/0085_ai_context_guards.sql",
                "migrations/0095_public_site.sql",
                "migrations/090_legacy_import.sql",
                "tests/email-provider.mjs",
                "tests/email-worker.mjs",
                "tests/github-integration.mjs",
                "tests/github-discovery.mjs",
                "tests/github-workerd-runtime.mjs",
                "tests/helpers",
                "tests/identity.mjs",
                "tests/lead-operations.mjs",
                "tests/legacy-import.mjs",
                "tests/management.mjs",
                "tests/deleted-background.mjs",
                "tests/deleted-background-worker.ts",
                "tests/prepare-browser-fixture.mjs",
                "tests/public-help-center.mjs",
                "tests/public-help-center-browser.mjs",
                "tests/public-site-runtime.mjs",
                "tests/runtime.mjs",
                "tests/ui-transport.mjs",
                "scripts/deploy.mjs",
                "scripts/deployment",
                "scripts/import-legacy.mjs",
                "scripts/migrations.mjs",
                "scripts/operations-config.mjs",
                "scripts/setup.mjs",
                "scripts/verify-modules.mjs",
                "src/app.ts",
                "src/jobs.ts",
                "src/message-effects.ts",
                "migrations/0008_account_sessions.sql",
                "tests/operating-environment.mjs",
                "tests/ui-browser.mjs",
                "tests/navigation-browser.mjs",
                "tests/login-return.mjs",
                "migrations/0084_billing_project_usage.sql",
                "migrations/0097_owner_setup.sql",
                "tests/identity-setup.mjs",
                "tests/setup-browser.mjs",
                "scripts/install.mjs",
                "scripts/installer-fill.mjs",
                "scripts/installer-artwork.mjs",
                "scripts/installer",
                "public/setup",
                "docs/INSTALLER.md",
                "src/editor",
                "public/homepage",
                "migrations/0098_personal_visual_inspiration.sql",
                "migrations/0099_personal_sidebar_artwork.sql",
                "scripts/setup-link.mjs",
                "tests/demo-inbox.mjs",
                "tests/demo-inbox-browser.mjs",
                "tests/demo-transcript-events.mjs",
                "tests/workspace-artwork.mjs",
                "tests/local-owner-login.mjs",
                "tests/cloudflare-email.mjs",
                "tests/sendgrid-inbound.mjs",
                "tests/ai-instructions-keyboard.mjs",
                "tests/installer-address.mjs",
                "tests/installer-artwork-bridge.mjs",
                "tests/installer-blank.mjs",
                "tests/installer-collision.mjs",
                "tests/installer-prefill.mjs",
                "tests/installer-presentation.mjs",
                "tests/installer-providers.mjs",
                "tests/installer-resume.mjs",
                "tests/installer-secret-scopes.mjs",
                "tests/installer-security.mjs",
                "tests/installer-summary.mjs",
                "tests/installer-ui-fixtures.mjs",
                "tests/installer-workspace-art.mjs",
                "tests/installer-wrangler-auth.mjs",
                "tests/installer.mjs",
                "src/edition",
                "tests/module-boundaries.mjs",
                "tests/widget-events.mjs",
                "tests/deployment-config-private.mjs",
                "tests/email-recovery.mjs",
                "tests/email-classification.mjs",
                "migrations/0100_inbound_email_claims.sql",
                "migrations/0101_email_mailbox_lifecycle.sql",
                "migrations/0102_public_abuse_limits.sql",
                "migrations/0103_contact_erasure.sql",
                "migrations/0104_inbound_email_classification.sql",
                "tests/abuse-limits.mjs",
                "tests/abuse-limits-worker.ts",
                "tests/abuse-limits-runtime.mjs",
                "tests/widget-rate-limit.mjs",
                "docs/PUBLIC-LIMITS.md",
                "docs/INBOUND-EMAIL-RECOVERY.md",
                "tests/contact-erasure.mjs",
                "tests/contact-erasure-browser.mjs",
                "docs/PRIVACY.md"
              ]
            }
          },
          {
            "id": "runeditrun/happytohelp-management-integration",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "management"
              ]
            }
          }
        ],
        "manifestSha256": "2864e75de0cff87ddf356d39f99af358f21303c2dac88d4b2484c8dc5d36d11a",
        "lockSha256": "8ddfdb1b24ba14a3617eaa9565a07f25eb78afe2684b39f34ff3146c71ef9ead"
      },
      "name": "HappyToHelp",
      "dir": "happytohelp",
      "category": "support",
      "categoryLabel": "Support",
      "spine": {
        "id": "happytohelp-conversation",
        "record": "Project-owned customer conversations with durable messages, evidence and explicitly authorized work."
      },
      "replaces": [],
      "version": "0.2.1",
      "license": "MIT",
      "repository": "https://github.com/just-every/happytohelp",
      "oneLiner": "Open-source customer support workspace: editable team dashboard, customer chat widget, configured AI assistance, knowledge and published help…",
      "summary": "Open-source customer support workspace: editable team dashboard, customer chat widget, configured AI assistance, knowledge and published help centers, customer context, integrations and the village public homepage, running on Cloudflare Workers with D1, R2, Queues and Durable Objects.",
      "scope": "",
      "maturity": "Public source is available. Assisted and Managed are in early access with hands-on onboarding.",
      "clauseCount": 14,
      "clauses": [
        {
          "id": "NATIVE-001",
          "title": "Native owner installation.",
          "text": "A clean owner can inspect the operating description before installing dependencies, install locked source, prepare local or selected remote bindings/migrations, build, verify and deploy through the common commands. Resource setup and publication have distinct effects. The delivered runtime requires no original checkout or PHP service."
        },
        {
          "id": "IDENTITY-001",
          "title": "Scoped authority.",
          "text": "Owner setup, registration, login/recovery, project switching, membership and widget admission establish native identity. Every private read/write enforces the current actor/project/role; blocked or deleted projects and revoked credentials lose access. Provider configuration never substitutes for user authority. The public widget key and origin are not a secret: visitor, registration and project-creation requests are bounded by D1-backed limits (`RATE_LIMITS`) and refused with HTTP 429 and `Retry-After`."
        },
        {
          "id": "CONVERSATION-001",
          "title": "Durable transcript.",
          "text": "Customer, operator, generation, import and administrative writers preserve stable message/command identity, revision/frontier, attachment ownership and final-reply admission atomically. Competing or repeated writes cannot admit two incompatible final replies. Owner/admin contact erasure removes message content while keeping identity, revision and frontier, so erased transcripts stay coherent."
        },
        {
          "id": "DELIVERY-001",
          "title": "Live delivery and recovery.",
          "text": "Persist before broadcast; reconnect/outbox replay and socket authorization recover committed messages after reload, disconnection or process wake. Provisional AI deltas never become durable final answers until guarded admission succeeds."
        },
        {
          "id": "UI-001",
          "title": "Existing editable experience.",
          "text": "Reuse the original React dashboard, transcripts/drafts/suggestions, settings and Shadow DOM widget; preserve useful desktop/mobile flows, attachment display, collector/privacy controls and errors. Native routing/transport replaces baked origins and Laravel bootstrapping."
        },
        {
          "id": "AI-001",
          "title": "Configured model execution.",
          "text": "Retain Ensemble model selection, context/tools, streamed events, normalization, raw per-round provider usage, attribution, bounded suggestions and reply lifecycle. Missing providers fail explicitly; uncertain provider attempts remain uncertain and are not replayed automatically. Usage is diagnostic/provider accounting data, not an active payment, balance or debit system. Visitor-caused automatic AI work is capped per UTC day per project and installation (`AI_DAILY_CEILINGS`); a skipped reply is recorded as terminal and never replayed."
        },
        {
          "id": "KNOWLEDGE-001",
          "title": "Published knowledge.",
          "text": "Dataset ingestion/crawl, retained corpus, lexical/selected semantic retrieval, citations and article/category authoring maintain project scope and publication state. A failed refresh cannot replace the last usable published corpus. Public pages exclude drafts/deleted projects and preserve original canonical article/search routes."
        },
        {
          "id": "CONTEXT-001",
          "title": "Attributed customer context.",
          "text": "Profiles, monitoring, insights and context packets preserve evidence attribution, source bounds, consent and project ownership. Provider output must satisfy the selected schemas before becoming usable customer context."
        },
        {
          "id": "DELEGATION-001",
          "title": "Explicit local authority.",
          "text": "Pairing, devices/grants, revocation, tool context and work/result states preserve investigation, resolve and advance-authorized resolve-and-reply distinctions. Revocation withdraws authority and never asserts that a local process stopped: started work becomes terminal with an unconfirmed outcome, and only the device may report cancellation. Local processes remain in the optional connector, never an implicit Worker capability."
        },
        {
          "id": "TRAINING-001",
          "title": "Durable evaluation.",
          "text": "Training and evaluation preserve run/case state, model/budget selection, raw provider usage and cancellation/recovery. Synthetic data admission is explicitly local/test-only. Historical pending attempts require reconciliation; a fixture pass is not live-provider quality acceptance."
        },
        {
          "id": "INTEGRATION-001",
          "title": "Honest external effects.",
          "text": "Email/inbound mail, GitHub, branding and knowledge imports use configured native adapters, tenant-scoped credentials, durable identity and ambiguous-outcome handling. Website previews use a separate origin, scoped expiring grants and public-network restrictions. Machine-generated inbound mail never triggers autonomous AI replies, notifications or outbound email; its classification is recorded before admission and survives recovery and redelivery. The `email:` message local-id prefix is reserved for inbound claims and refused by the message API. Payment, checkout, Stripe and balance behavior are outside this edition."
        },
        {
          "id": "PUBLIC-001",
          "title": "Public product source.",
          "text": "The approved village homepage is the real public `/` experience: the beacon-lit mountain procession, conversation square, knowledge archive, agent handoff and open workshop. Its text and real links remain accessible independently of the art; layered motion respects reduced-motion preferences and does not change document layout. Dynamic published help centers retain project scope, canonical links, search and draft/deleted denial. The customer widget remains available through owner-configured installation; broader marketing routes, pricing, legal, referrer, waitlist and public auto-widget behavior are outside this edition."
        },
        {
          "id": "MIGRATION-001",
          "title": "Prior-data custody.",
          "text": "Explicit legacy artifacts preserve IDs, exact historical financial values, transcripts, object hashes, publication and permissions through resumable guarded D1/R2 import. Historical financial SQL, imported rows and reconciliation holds remain immutable archive custody; they never become active payments, balances or provider work. The one exception is an explicit owner/admin contact erasure: it takes precedence over archive custody for that contact's personal data and redacts the imported rows and their verbatim archive copies (row digests are kept, so replaying the bundle does not restore them), leaving a `contact_erasures` audit tombstone. Old credentials do not become trusted native authority."
        },
        {
          "id": "SOURCE-001",
          "title": "Author-owned capability graph.",
          "text": "The edition selects maintained source with inspect/check/lock/release commands. Shared capabilities include substantial implementation, migrations and meaningful tests; a second consumer preserves its own policy. Release excludes credentials/private fixtures and retains asset provenance/licensing obligations. Original work is preserved."
        }
      ],
      "hasReadme": true,
      "hasBaseline": false,
      "nonGoals": [
        "Implicit deployment or provider execution from source inspection",
        "Reusing legacy passwords or grants as native authority",
        "Regenerating or erasing original owner checkouts"
      ],
      "externals": [
        {
          "name": "cloudflare",
          "required": true,
          "requiredWhen": "",
          "reason": "Selected Worker/D1/R2/Queues/DO deployment target. Implementation/configuration source: wrangler.jsonc. No versioned replacement adapter is declared.",
          "data": [
            "Application records",
            "Retained files",
            "Requests and queued work"
          ],
          "adapters": []
        },
        {
          "name": "configured-providers",
          "required": false,
          "requiredWhen": "",
          "reason": "Selected AI, semantic/crawl, email, GitHub and branding capabilities. Implementation/configuration source: scripts/deployment/environment.mjs. No versioned replacement adapter is declared.",
          "data": [
            "Explicitly scoped provider inputs"
          ],
          "adapters": []
        },
        {
          "name": "run-edit-run-editor",
          "required": false,
          "requiredWhen": "",
          "reason": "Optional owner-enabled editor frame; separate Run Edit Run authentication and installation authority are required. Disabled by default. Implementation/configuration source: src/editor/EditorLauncher.tsx. No versioned replacement adapter is declared.",
          "data": [
            "Installation ID and edition origin",
            "User-selected element structure",
            "Explicitly reviewed text limited to 240 characters"
          ],
          "adapters": []
        }
      ],
      "operatingCost": {
        "status": "unmeasured"
      },
      "extensionPoints": [],
      "limits": {
        "status": "unmeasured"
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target; unaudited"
      },
      "landing": "assets/seeds/happytohelp/01-inbox-desktop.jpg",
      "shots": [
        "assets/seeds/happytohelp/01-inbox-desktop.jpg",
        "assets/seeds/happytohelp/02-chat-widget-desktop.jpg",
        "assets/seeds/happytohelp/03-widget-install-desktop.jpg",
        "assets/seeds/happytohelp/04-homepage-desktop.jpg"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "installable",
      "releaseProvenance": {
        "kind": "publisher-release",
        "repository": "https://github.com/just-every/happytohelp",
        "revision": "59c6e2e1beb321e9f1cb5493efd05e20fc068638",
        "tag": "v0.2.1",
        "note": "Documents copied unmodified from the tagged release (git archive of v0.2.1); the listing is the one the site published from v0.2.0 on 25 September 2026, with the version and maturity of v0.2.1.",
        "screenshots": "Captured by Run Edit Run on 29 September 2026 from v0.2.1 running locally (pnpm run dev, as its README describes) in Google Chrome: a new workspace with fictional customers, whose conversations were sent through the chat widget and answered in the inbox.",
        "project": "runeditrun/happytohelp",
        "release": "0.2.1",
        "manifestSha256": "160b2c115b314790d20619587f70660204849e4fdb8404ad7a1179f59ab1cc66"
      }
    },
    {
      "id": "orbit",
      "authoring": {
        "mode": "edition-owned",
        "agentSource": "AGENTS.md",
        "id": "runeditrun/orbit",
        "parent": {
          "repository": "https://github.com/runeditrun/sales-orbit.git",
          "revision": "c65a2054227426c980bb7ec7ed2a1a94c9366b4f"
        },
        "toolchain": {
          "id": "runeditrun/edition-authoring",
          "version": "1.1.0",
          "sha256": "a13d7d0bcf174618923ca5d1134575c149ea6b57c89d1640c03c1ebf091ab372"
        },
        "components": [
          {
            "id": "runeditrun/base",
            "role": "foundation",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/deployment/health.mjs",
                "scripts/deployment/base-secret-json.mjs",
                "tools/seed-schema/SEED.schema.json",
                "tools/seed-schema/json-schema.mjs",
                "tools/seed-schema/validate.mjs",
                "tools/seed-schema/RELEASE.schema.json",
                "scripts/operations.mjs",
                "src/base/sendgrid-event-relay/crypto.ts"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "603ee025aa5991fb44e0c3b0731f2113be85ceab",
                "path": ".",
                "paths": [
                  "deployment/health.mjs",
                  "deployment/secret-json.mjs",
                  "schema/SEED.schema.json",
                  "schema/json-schema.mjs",
                  "schema/validate.mjs",
                  "schema/RELEASE.schema.json",
                  "operations/operations.mjs",
                  "infrastructure/sendgrid-event-relay/src/crypto.ts"
                ]
              },
              "mappings": [
                {
                  "path": "scripts/deployment/health.mjs",
                  "originPath": "deployment/health.mjs"
                },
                {
                  "path": "scripts/deployment/base-secret-json.mjs",
                  "originPath": "deployment/secret-json.mjs"
                },
                {
                  "path": "tools/seed-schema/SEED.schema.json",
                  "originPath": "schema/SEED.schema.json"
                },
                {
                  "path": "tools/seed-schema/json-schema.mjs",
                  "originPath": "schema/json-schema.mjs"
                },
                {
                  "path": "tools/seed-schema/validate.mjs",
                  "originPath": "schema/validate.mjs"
                },
                {
                  "path": "tools/seed-schema/RELEASE.schema.json",
                  "originPath": "schema/RELEASE.schema.json"
                },
                {
                  "path": "scripts/operations.mjs",
                  "originPath": "operations/operations.mjs"
                },
                {
                  "path": "src/base/sendgrid-event-relay/crypto.ts",
                  "originPath": "infrastructure/sendgrid-event-relay/src/crypto.ts"
                }
              ]
            }
          },
          {
            "id": "runeditrun/sales",
            "role": "family",
            "source": {
              "kind": "local",
              "paths": [
                "src/family/sales",
                "composition/families/sales/README.md",
                "composition/families/sales/LICENSE",
                "tests/composition/sales",
                "scripts/test-composition.mjs"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/sales.git",
                "revision": "4b7a3a6c4ecdbb27742d565dfd96e0704175f23b",
                "path": ".",
                "paths": [
                  "core/src",
                  "core/README.md",
                  "core/LICENSE",
                  "core/tests",
                  "core/test-components.mjs"
                ]
              },
              "mappings": [
                {
                  "path": "src/family/sales",
                  "originPath": "core/src"
                },
                {
                  "path": "composition/families/sales/README.md",
                  "originPath": "core/README.md"
                },
                {
                  "path": "composition/families/sales/LICENSE",
                  "originPath": "core/LICENSE"
                },
                {
                  "path": "tests/composition/sales",
                  "originPath": "core/tests"
                },
                {
                  "path": "scripts/test-composition.mjs",
                  "originPath": "core/test-components.mjs"
                }
              ]
            }
          },
          {
            "id": "runeditrun/sendgrid-email",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/sendgrid-email",
                "composition/modules/sendgrid-email/README.md",
                "composition/modules/sendgrid-email/LICENSE",
                "tests/composition/sendgrid-email"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "d1343b638afd254679bc2b35819fc1d268bb6cc7",
                "path": ".",
                "paths": [
                  "modules/sendgrid-email/src",
                  "modules/sendgrid-email/README.md",
                  "modules/sendgrid-email/LICENSE",
                  "modules/sendgrid-email/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/sendgrid-email",
                  "originPath": "modules/sendgrid-email/src"
                },
                {
                  "path": "composition/modules/sendgrid-email/README.md",
                  "originPath": "modules/sendgrid-email/README.md"
                },
                {
                  "path": "composition/modules/sendgrid-email/LICENSE",
                  "originPath": "modules/sendgrid-email/LICENSE"
                },
                {
                  "path": "tests/composition/sendgrid-email",
                  "originPath": "modules/sendgrid-email/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/orbit",
            "role": "edition",
            "source": {
              "kind": "local",
              "paths": [
                "src/core",
                "src/ext",
                "src/routes",
                "migrations"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/sales.git",
                "revision": "4b7a3a6c4ecdbb27742d565dfd96e0704175f23b",
                "path": ".",
                "paths": [
                  "editions/orbit/src/core",
                  "editions/orbit/src/ext",
                  "editions/orbit/src/routes",
                  "editions/orbit/migrations"
                ]
              },
              "mappings": [
                {
                  "path": "src/core",
                  "originPath": "editions/orbit/src/core"
                },
                {
                  "path": "src/ext",
                  "originPath": "editions/orbit/src/ext"
                },
                {
                  "path": "src/routes",
                  "originPath": "editions/orbit/src/routes"
                },
                {
                  "path": "migrations",
                  "originPath": "editions/orbit/migrations"
                }
              ]
            }
          }
        ],
        "manifestSha256": "34638adb38f9aff94a7ef8e2ef982c4d0b59a68bc81268ada6cc2a2392d58c86",
        "lockSha256": "45368d1f7dae7c0e43acaa952c71fa5cc3a20444fc80a99c9a78bbc62d40d839"
      },
      "name": "Orbit",
      "dir": "hubspot",
      "category": "sales",
      "categoryLabel": "Sales",
      "spine": {
        "id": "sales",
        "record": "an account opportunity with attributed customer evidence"
      },
      "composition": {
        "base": "base",
        "family": "sales",
        "edition": "orbit",
        "modules": [
          "sendgrid-email"
        ]
      },
      "replaces": [],
      "version": "0.1.0",
      "oneLiner": "A CRM for one revenue team that turns customer activity reported by the tools they already run into explainable, human-reviewed next actions.",
      "summary": "A CRM for one revenue team that turns customer activity reported by the tools they already run into explainable, human-reviewed next actions.",
      "scope": "",
      "maturity": "complete-workflow-implementation; acceptance requires pinned runtime and native receipts",
      "clauseCount": 64,
      "clauses": [
        {
          "id": "TEAM-001",
          "title": "Equal operators.",
          "text": "Every operator can view and act on every contact, company, opportunity, signal, activity, and notification. There are no roles and no private records."
        },
        {
          "id": "TEAM-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. The operator configured at setup can be removed only by themselves. *Policy: `operator.management.v1`; default: any operator may invite or remove.*"
        },
        {
          "id": "TEAM-003",
          "title": "One workspace.",
          "text": "A deployment holds exactly one workspace and offers no way to create a second. Its name, timezone, currency, working hours, stale interval, awaiting-response interval, pipeline stages, signal sources, and free-mail domain list are configured in settings and apply to every record."
        },
        {
          "id": "TEAM-004",
          "title": "Removal handover.",
          "text": "Removing an operator revokes their sessions, cancels their scheduled messages, and unassigns their open opportunities. Their notes, drafts, sent messages, and timeline entries remain and stay attributed to them."
        },
        {
          "id": "RECORD-001",
          "title": "Core records.",
          "text": "Operators can create, edit, and delete contacts, companies, and opportunities. A contact requires a name or an email address and may also hold a phone number, job title, and timezone; a company requires a name and may hold one primary domain; an opportunity requires a title and holds a pipeline stage, a value in the workspace currency, zero or one owner, an expected close date, at most one company, and any number of contacts. Each entity listed in `seed.json → customFieldEntities` also holds custom fields."
        },
        {
          "id": "RECORD-002",
          "title": "Associations.",
          "text": "A contact belongs to at most one company and to any number of opportunities. An opportunity with at least one contact names one of them primary, and that contact is the default recipient for its outreach; an opportunity with no contact is never recommended an email follow-up. Changing an association moves no timeline entry: each record keeps the entries it already had."
        },
        {
          "id": "RECORD-003",
          "title": "Identity keys.",
          "text": "A contact's key is its email address lowercased with surrounding whitespace removed; a company's key is its primary domain lowercased with a leading `www.` removed. A domain on the configured free-mail list is never a company key. Creating or changing a record to a key another record already holds returns 409 with that record's ID and writes nothing. A record with no key is created and edited normally, and signals never link to it automatically. Orbit never merges records by name similarity, fuzzy match, or score. *Policy: `record.identity.v1`; default: the rules in this clause.*"
        },
        {
          "id": "RECORD-004",
          "title": "Duplicate merge.",
          "text": "Merging two contacts or two companies requires an operator to choose the surviving record and the value of every conflicting field. The merge moves associations and timeline entries once, retires the other record's key as an alias so later signals carrying it link to the survivor, creates no signal, recommendation, or outreach, and records the retired ID in the audit trail."
        },
        {
          "id": "RECORD-005",
          "title": "Timeline.",
          "text": "A record's timeline lists its signals, stage and owner changes, notes, logged calls and meetings, recommendations, drafts, outbound attempts, replies, and merges, most recent occurred time first with ties broken by ID. Every entry shows its occurred time, its recorded time, and the operator, source, or feature that created it."
        },
        {
          "id": "RECORD-006",
          "title": "Search and views.",
          "text": "Operators can search contacts, companies, opportunities, and timeline text, and can filter opportunities by state, stage, owner, band, and queue. A first page of at most 50 matches returns within 500ms."
        },
        {
          "id": "RECORD-007",
          "title": "Ownership.",
          "text": "An opportunity has zero or one owner. Any operator can assign or reassign it, and the change appears on its timeline. *Policy: `opportunity.assignment.v1`; default: a new opportunity is unassigned.*"
        },
        {
          "id": "RECORD-008",
          "title": "Import.",
          "text": "Operators import contacts and companies from a CSV, mapping its columns to fields once per import. A row updates an existing record only when it carries an Orbit ID or matches exactly one key under RECORD-003; a row matching more than one record fails. The import reports created, updated, and failed rows with row numbers and reasons, and writes nothing for a failed row."
        },
        {
          "id": "RECORD-009",
          "title": "Duplicate candidates.",
          "text": "Orbit lists contacts sharing a name within one company, and companies sharing a name, as duplicate candidates; names are compared lowercased with runs of whitespace collapsed. A candidate is a review item only: nothing is merged, altered, or hidden until an operator completes RECORD-004."
        },
        {
          "id": "RECORD-010",
          "title": "Manual entries.",
          "text": "An operator can add a note to any record and log a call or meeting with an occurred time, duration, and attending contacts. A manual entry is attributed to its author, editable and deletable by any operator, never sent to a customer, and never contributes to a score."
        },
        {
          "id": "RECORD-011",
          "title": "Related records.",
          "text": "A company page lists its contacts and its opportunities with each opportunity's stage, owner, and value, plus the total value of its open opportunities. A contact page lists their opportunities. Both reflect an association change on the next load."
        },
        {
          "id": "RECORD-012",
          "title": "Contactability.",
          "text": "A contact is contactable or do-not-contact. An operator can set do-not-contact at any time, and an unsubscribe event or a permanent bounce also sets it. No operation sends customer email to a do-not-contact contact, the state is shown wherever the contact appears, and only an operator can clear it."
        },
        {
          "id": "RECORD-013",
          "title": "Deletion.",
          "text": "Deleting a company detaches its contacts and opportunities and deletes neither. Deleting an opportunity deletes its recommendations, drafts, and unsent scheduled messages, and leaves its contacts, its company, and their entries for messages already sent. Deleting a contact follows DATA-002. Each deletion is confirmed before it runs."
        },
        {
          "id": "SIGNAL-001",
          "title": "Ingestion.",
          "text": "A valid event from a configured source, carrying that source's signature, becomes one normalised signal within 10 seconds. The signal keeps its source, source event ID, type, subject, occurred time, received time, payload hash, and linking basis, and is never edited afterwards."
        },
        {
          "id": "SIGNAL-002",
          "title": "Signal types.",
          "text": "Orbit accepts form submissions; email sent, opened, replied, bounced, and unsubscribed events; meeting viewed, booked, completed, and cancelled events; page views; chats; content downloads; and firmographic changes. A source can add a type namespaced to itself, which changes no existing type. An event whose type is neither standard nor namespaced is rejected with 400."
        },
        {
          "id": "SIGNAL-003",
          "title": "Idempotency.",
          "text": "Repeated or concurrent delivery of one `(source, source event ID)` produces one signal and one set of downstream evaluations. Reusing that identity with a different payload hash is rejected and recorded as an error on that source."
        },
        {
          "id": "SIGNAL-004",
          "title": "Linking.",
          "text": "A signal links first by an Orbit record ID the source supplies, then by RECORD-003. A uniquely linked signal appears once on that record's timeline. A signal with no unique match is held in the unlinked queue, where it changes no score; an operator linking it later appends it to the chosen record's timeline exactly once and triggers one evaluation."
        },
        {
          "id": "SIGNAL-005",
          "title": "Event time.",
          "text": "A late or out-of-order event keeps its occurred time, appears in that position on the timeline, and triggers a fresh evaluation. It never overwrites a record field that a later-occurring event wrote."
        },
        {
          "id": "SIGNAL-006",
          "title": "Evidence confidence.",
          "text": "Every signal carries one confidence: observed, when the customer acted deliberately (form submission, reply, booking, meeting, download, chat), or inferred, when a tool reported activity (email open, page view). An inferred signal alone cannot create an opportunity, produce a high band, or satisfy ACTION-001's evidence requirement."
        },
        {
          "id": "SIGNAL-007",
          "title": "No tracking.",
          "text": "Orbit serves no tracking script, pixel, or cookie, and performs no fingerprinting or cross-site collection. A page-view, chat, or download signal is accepted only when its source supplies the visitor identity it already holds and the basis for holding it; without that basis the event is held unlinked under SIGNAL-004 and never becomes contact history."
        },
        {
          "id": "SIGNAL-008",
          "title": "Ingestion failure.",
          "text": "A missing or invalid signature returns 401, a request naming a source this deployment has not configured returns 403, and a payload over `seed.json → limits.signalPayloadBytes` returns 413; none of them writes anything. A persistence failure returns 503 and writes no partial signal, timeline entry, or evaluation, so the source can safely replay the same event identity. Each source shows its last accepted event, its last error, and its rejected-event count in settings."
        },
        {
          "id": "SIGNAL-009",
          "title": "Signed sources.",
          "text": "This clause supersedes BASE-ACCESS-004 for the paths `/api/v1/signals/` and `/api/v1/mail/events/` only: a request there is authorised by a signature from a source or mail provider configured in this deployment instead of an operator session, and may write only signals, delivery states, and their timeline entries. Every other mutation still requires a valid operator session and a server-side ownership check."
        },
        {
          "id": "OPP-001",
          "title": "Lifecycle.",
          "text": "An opportunity is open, won, or lost. Closing it as won or lost records the operator, the time, and a reason, and removes it from NOW and NEXT. Later signals append to its timeline and never reopen it; only an operator can reopen it, which returns it to open without changing its stage or owner."
        },
        {
          "id": "OPP-002",
          "title": "Independent state.",
          "text": "Stage, owner, score, band, and queue state are independent fields. A score or band change never changes stage, owner, or won/lost state, and a stage change never changes a score."
        },
        {
          "id": "OPP-003",
          "title": "Evaluation.",
          "text": "A linked signal whose type carries a non-zero weight in the scoring policy, or a change to an open opportunity's stage, expected close date, value, owner, or associations, evaluates that opportunity within 30 seconds. The result records a 0–100 score, a low, medium, or high band, the evaluation time, the policy version, and each contributing signal's contribution. A closed opportunity is not evaluated. *Policy: `opportunity.scoring.v1`; default: each contributing signal's configured weight decays linearly to zero over the 30 days after its occurred time, the sum is clamped to 0–100, and 0–39 is low, 40–69 is medium, 70–100 is high.*"
        },
        {
          "id": "OPP-004",
          "title": "Explainability.",
          "text": "The opportunity view shows every signal the current score used, by ID, with its source, occurred time, confidence, and positive or negative contribution, and shows the reason for the current recommendation beside them."
        },
        {
          "id": "OPP-005",
          "title": "Evaluation failure.",
          "text": "If an evaluation fails or returns output that does not validate, the previous score, band, and evidence remain readable and are marked stale with the time of the failed attempt. Orbit shows no score, band, or recommendation it did not compute."
        },
        {
          "id": "OPP-006",
          "title": "NOW ordering.",
          "text": "NOW lists the open opportunities that OPP-007 placed there, ordered by score descending, then by the occurred time of their most recent contributing signal descending, then by ID. Operators can filter it by owner, stage, and band. *Policy: `opportunity.prioritization.v1`; default: the ordering in this clause, and every factor it uses is shown in the opportunity view.*"
        },
        {
          "id": "OPP-007",
          "title": "Queue entry.",
          "text": "An open opportunity enters NOW when its band rises, a customer reply or delivery problem arrives, a scheduled message cannot be sent, its expected close date passes, its last timeline entry becomes older than the stale interval, or an operator puts it there. It is in NEXT while it waits for a scheduled send or for a customer response inside the awaiting-response interval, and in neither when no action is pending. The stale interval defaults to 14 days and the awaiting-response interval to 5 working days, counted Monday to Friday in the workspace timezone. Every entry, move, and removal records its reason and actor on the timeline."
        },
        {
          "id": "OPP-008",
          "title": "Manual completion.",
          "text": "An operator can complete or skip the current recommended action without sending anything, with an optional note; the outcome, note, and operator appear on the timeline. *Policy: `opportunity.next-action.v1`; default: evaluate once, keep the opportunity in NOW if that produces a new recommendation, and otherwise remove it from NOW and NEXT.*"
        },
        {
          "id": "OPP-009",
          "title": "Pipeline stages.",
          "text": "The deployment has one pipeline. Its stages are named and ordered in settings; an opportunity can move to any stage in any order, and each move records the previous stage, the actor, and the time. A stage holding opportunities cannot be deleted until they are moved."
        },
        {
          "id": "ACTION-001",
          "title": "Current recommendation.",
          "text": "An open opportunity has at most one current recommended action, which is either an email follow-up or a manual action. It names the action, cites at least one observed signal by ID, and records the policy or model version that produced it. Its written explanation is generated under AI-001 and is absent when AI-003 applies."
        },
        {
          "id": "ACTION-002",
          "title": "Replacement.",
          "text": "A new contributing signal, a changed opportunity, a completed or skipped action, or an operator request replaces the current recommendation. The replaced one stays on the timeline marked superseded and can no longer be drafted from, sent, or completed."
        },
        {
          "id": "ACTION-003",
          "title": "Draft on request.",
          "text": "An operator can request a one-to-one email draft for the current recommendation. Generation reads the opportunity, the primary or operator-selected contact, that contact's company, the cited signals, the messages already exchanged with that contact, and the configured instructions, and sends nothing. *Policy: `outreach.instructions.v1`; default: at most 150 words, and no fact, price, discount, or commitment that a cited record or signal field does not carry.*"
        },
        {
          "id": "ACTION-004",
          "title": "Durable draft.",
          "text": "Recipient, subject, and body are visible and editable, and the draft is stored with its author, source recommendation, created time, and last edit time, and is readable after signing out and back in."
        },
        {
          "id": "ACTION-005",
          "title": "Stale draft.",
          "text": "A reply, meeting, unsubscribe, do-not-contact, permanent bounce, changed recipient, changed opportunity, or replaced recommendation marks a draft stale. A stale draft is shown as stale, and sending it returns 409 and writes no outbound message until an operator opens it, sees what changed, and confirms."
        },
        {
          "id": "OUT-001",
          "title": "Human send boundary.",
          "text": "No signal, score, recommendation, draft, job, or AI result can approve or start a customer message. A send requires an authenticated operator who has seen the exact recipient, subject, and body to invoke it, and a worker may dispatch only the unchanged content an operator approved for the schedule in OUT-009. No policy, adapter, extension, or setting removes this requirement."
        },
        {
          "id": "OUT-002",
          "title": "Message states.",
          "text": "Customer mail is submitted through `mail.sender.v1`. An outbound message is draft, scheduled, queued, reconciling, accepted, delivered, bounced, failed, cancelled, or delivery-unknown. The operator view calls a message sent only after the provider accepts it, and delivered only after a delivery event."
        },
        {
          "id": "OUT-003",
          "title": "Send idempotency.",
          "text": "Repeated or concurrent use of one send idempotency key creates one outbound message and at most one provider submission."
        },
        {
          "id": "OUT-004",
          "title": "Threading.",
          "text": "An outbound message carries message and reference headers that return the customer's reply to the same opportunity. A reply carrying those headers is attached once; a reply without them becomes an unlinked activity for operator review rather than a subject-based guess."
        },
        {
          "id": "OUT-005",
          "title": "Accepted outcome.",
          "text": "When the provider accepts a submission, one local transaction records the reviewed content and provider message ID as a timeline activity, completes the recommendation, sets the next action to awaiting response, moves the opportunity from NOW to NEXT, and creates one notification for the responsible operator. A replayed provider event repeats none of it."
        },
        {
          "id": "OUT-006",
          "title": "Delivery failure.",
          "text": "A transient submission failure retries up to 5 times over 1 hour. A permanent failure or an exhausted retry returns the opportunity to NOW, notifies the responsible operator once, and shows the provider's reason on the message. A permanent bounce also sets the contact do-not-contact under RECORD-012."
        },
        {
          "id": "OUT-007",
          "title": "Stop conditions.",
          "text": "A reply, a booked meeting, an unsubscribe, a do-not-contact, or a permanent bounce cancels that contact's scheduled messages and marks their unsent drafts stale under ACTION-005. A reply or a bounce ends awaiting response and returns the opportunity to NOW with the reply or the delivery problem as its next action."
        },
        {
          "id": "OUT-008",
          "title": "Quiet hours.",
          "text": "A draft can be written at any time. Sending outside 08:00–18:00 Monday to Friday in the contact's timezone, or the workspace timezone when the contact has none, requires a second confirmation naming the recipient's local time; a scheduled send waits for the next permitted minute. *Policy: `outreach.quiet-hours.v1`; default: the window in this clause, with no holiday calendar.*"
        },
        {
          "id": "OUT-009",
          "title": "Scheduled send.",
          "text": "An operator can schedule one reviewed message for a future time and cancel it until it dispatches. Dispatch re-checks ACTION-005, RECORD-012, OUT-007, OUT-008, and OUT-010 against the current record and submits only the approved content; a stale or prohibited message is not sent, returns the opportunity to NOW, and shows the reason."
        },
        {
          "id": "OUT-010",
          "title": "Sending limits.",
          "text": "The per-minute deployment limit and the per-operator rolling-day limit in `seed.json → limits` are checked before submission. A message over a limit stays unsent and visible with the limit that stopped it and the time it may be sent; Orbit neither retries it silently nor merges it with another message."
        },
        {
          "id": "OUT-011",
          "title": "Unresolved submission.",
          "text": "Orbit records the outbound intent and its provider idempotency key durably before submitting. If the transaction in OUT-005 fails the message becomes reconciling; if the provider's response is a timeout it becomes delivery-unknown. In both states a job queries the provider by that idempotency key every 5 minutes for up to 24 hours, submits nothing again, and applies the OUT-005 outcome exactly once if acceptance is confirmed. A message still unresolved after 24 hours becomes a failure the operator can see, and notifies the responsible operator."
        },
        {
          "id": "OUT-012",
          "title": "Unsubscribe.",
          "text": "Every outbound customer message carries `List-Unsubscribe` and `List-Unsubscribe-Post` headers handled by the mail provider. An unsubscribe event from the provider sets that contact do-not-contact under RECORD-012 within 60 seconds and cancels their scheduled messages."
        },
        {
          "id": "AI-001",
          "title": "Provider and input.",
          "text": "Recommendation explanations and email drafts go through `ai.provider.v1`. The request preview shows the operator every field, timeline entry, and instruction the request will carry, and nothing outside the preview is sent."
        },
        {
          "id": "AI-002",
          "title": "Output validation.",
          "text": "AI output must validate against its operation's schema and cite only evidence IDs present on the record. Output failing either check fails visibly and is never repaired with invented content or a semantic fallback. Only a transient transport failure is retried, at most twice within 30 seconds."
        },
        {
          "id": "AI-003",
          "title": "Provider failure.",
          "text": "When the AI provider is unconfigured or unavailable, records, signal ingestion, linking, scoring, queue placement, search, manual updates, hand-written drafts, and sending all continue to work, and a recommendation still names its action and evidence. A generation request shows the failure and produces no explanation or draft."
        },
        {
          "id": "AI-004",
          "title": "Cost.",
          "text": "Every AI call records provider, model, input and output tokens, feature, duration, status, and estimated cost. Operators can view totals by day and by feature in settings."
        },
        {
          "id": "NOTIFY-001",
          "title": "Responsible operator.",
          "text": "The responsible operator for an opportunity is its owner, or, when it is unassigned, the operator whose action appears most recently on its timeline. They receive one in-app notification when it enters NOW, when a customer reply arrives on it, and when one of its outbound messages fails, bounces, or becomes delivery-unknown. When an unassigned opportunity that no operator has acted on enters NOW, every operator is notified instead. Duplicate source or provider events add no notification."
        },
        {
          "id": "NOTIFY-002",
          "title": "Read state.",
          "text": "A notification is unread until the operator opens it or marks it read. The unread count updates within 5 seconds and never counts one event twice."
        },
        {
          "id": "PUBLIC-001",
          "title": "Landing page.",
          "text": "`GET /` returns 200 without an operator session, takes its name, description, and images from `ext/config.ts`, and links to magic-link sign-in. It shows no contact, company, opportunity, or signal from this deployment."
        },
        {
          "id": "PUBLIC-002",
          "title": "Safe demo.",
          "text": "When enabled in `ext/config.ts` and configured with the explicit `PUBLIC_DEMO_OPPORTUNITY_ID` binding, `GET /demo/` loads one deliberately published opportunity from the dedicated hosted demo deployment after the visitor starts it. The opportunity and its observed evidence, recommendation, and publish-safe draft are created through Orbit's real operator workflow; no fixture or runtime mock is used. The visitor can edit and preview the follow-up entirely in the browser. The page states before loading and after preview that nothing is sent and no record changes. The browser makes no write request and calls no mail adapter, AI adapter, signal endpoint, or notification. The public endpoint cannot enumerate or select another record, and omits private addresses and raw signal payloads. Turning it off in `ext/config.ts`, or omitting the explicit binding, makes `/demo` and `/demo/` return 404. This wording records the owner-authorized 13 September 2026 ruling that replaced the earlier browser-generated sample method while preserving its safety purpose."
        },
        {
          "id": "PUBLIC-003",
          "title": "Unconfigured sign-in.",
          "text": "While no mail-auth adapter is configured, `GET /signin` returns 200 without an operator session and states that magic-link delivery is unavailable. It presents no credential field, demo account, local-login action, or session bypass."
        },
        {
          "id": "PUBLIC-004",
          "title": "Native sign-in.",
          "text": "Configured magic-link delivery sends only to an existing active operator, gives the same request response for unknown and inactive addresses, and exchanges a hashed, single-use link within 15 minutes for a secure native session. Return locations stay inside the operator app. Sign-out revokes that session. A missing or failed mail configuration cannot issue a session."
        },
        {
          "id": "DATA-001",
          "title": "Export contents.",
          "text": "The export contains contacts, companies, opportunities, associations, source configuration without secrets, signals, evaluations with their policy versions, recommendations, drafts, outbound messages with their provider statuses, timeline activities, notifications, settings, and audit entries as JSON."
        },
        {
          "id": "DATA-002",
          "title": "Contact erasure.",
          "text": "Within 5 minutes of an operator deleting a contact, their email addresses, signal payloads, drafts, message bodies, and the notifications naming them are absent from records, search results, exports, and every later AI or mail request. Only audit entries survive, with the contact's reference anonymised, as BASE-DATA-003 requires."
        }
      ],
      "hasReadme": true,
      "hasBaseline": true,
      "nonGoals": [
        "A second workspace",
        "Roles, territories, or private records",
        "A second pipeline",
        "Workflow builder",
        "Bulk campaigns or sequences",
        "Autonomous customer outreach",
        "SMS, phone, or social sending",
        "Attachments on outbound messages",
        "Email or digest notifications",
        "A tracking script, cross-site tracking, fingerprinting, or data-broker identity resolution",
        "Hosted forms or landing-page building",
        "Marketing CMS, ads, or multi-touch attribution",
        "Per-record currencies",
        "Plan or pricing pages",
        "Revenue forecasting",
        "Subscription billing or plan enforcement",
        "Native mobile applications"
      ],
      "externals": [
        {
          "name": "sendgrid",
          "required": false,
          "requiredWhen": "Production operator sign-in or customer mail is configured.",
          "reason": "Native sign-in and operator-approved mail require an external delivery provider.",
          "data": [
            "Operator or customer email address",
            "Message subject and body",
            "Durable delivery correlation ID"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        },
        {
          "name": "openai",
          "required": false,
          "requiredWhen": "AI explanation or draft generation is configured.",
          "reason": "Generate operator-requested recommendation explanations and drafts from the exact reviewed preview.",
          "data": [
            "Fields, evidence, prior messages and instructions shown in the operator request preview"
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        }
      ],
      "operatingCost": {
        "currency": "USD",
        "period": "month",
        "estimated": true,
        "low": 0,
        "high": 10,
        "assumptions": "Cloudflare platform only, excluding buyer-selected mail and AI adapters: Workers Free is the $0 default while daily Worker requests stay at or below 100000, D1 stays at or below 5000000 rows read and 100000 rows written per day, R2 stays within its monthly free tier, and Queues stays at or below 10000 operations per day. Workers Paid is required to sustain the declared 600 signed-ingress requests per minute (864000 requests/day): 600 * 60 * 24 * 30 = 25920000 monthly Worker requests, or 15920000 above the 10000000 Standard-plan inclusion. At $0.30 per additional million, that is $4.78 request overage plus the $5 monthly Paid minimum, rounded up to the $10 high; this excludes CPU, D1 overages, and future Queue fan-out, which must be measured before those capabilities ship. One public IP at the declared 300 requests per minute reaches the Free Worker request ceiling in about 5.6 hours. Sources: https://developers.cloudflare.com/workers/platform/pricing/ ; https://developers.cloudflare.com/d1/platform/pricing/ ; https://developers.cloudflare.com/queues/platform/pricing/",
        "status": "estimated; not load verified"
      },
      "extensionPoints": [
        "signal.sources.v1",
        "operator.management.v1",
        "record.identity.v1",
        "opportunity.assignment.v1",
        "opportunity.scoring.v1",
        "opportunity.prioritization.v1",
        "opportunity.next-action.v1",
        "outreach.instructions.v1",
        "outreach.quiet-hours.v1",
        "signal.recorded.v1",
        "record.merged.v1",
        "opportunity.evaluated.v1",
        "opportunity.queue-changed.v1",
        "recommendation.created.v1",
        "outreach.scheduled.v1",
        "outreach.accepted.v1",
        "outreach.failed.v1",
        "notification.created.v1",
        "navigation.after.v1",
        "now.header.after.v1",
        "now.row.actions.v1",
        "contact.sidebar.after.v1",
        "company.sidebar.after.v1",
        "opportunity.header.after.v1",
        "opportunity.sidebar.after.v1",
        "opportunity.timeline.after.v1",
        "composer.context.after.v1",
        "settings.sections.after.v1",
        "mail.sender.v1",
        "ai.provider.v1"
      ],
      "limits": {
        "status": "estimated-until-load-tested",
        "operators": 25,
        "contacts": 100000,
        "companies": 25000,
        "openOpportunities": 25000,
        "signals": 1000000,
        "timelineActivities": 2000000,
        "signalPayloadBytes": 262144,
        "signedIngressRequestsPerMinute": 600,
        "publicRequestsPerMinutePerIp": 300,
        "outboundMessagesPerMinute": 3,
        "outboundMessagesPerOperatorPerRollingDay": 100
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "prototype automation only; full shared pack pending"
      },
      "landing": "assets/seeds/orbit/verification-ui-1440-app-notifications.png",
      "shots": [
        "assets/seeds/orbit/verification-ui-1440-app-notifications.png",
        "assets/seeds/orbit/verification-ui-1440-app-records.png",
        "assets/seeds/orbit/verification-ui-1440-app-settings.png",
        "assets/seeds/orbit/verification-ui-1440-app-sources.png",
        "assets/seeds/orbit/verification-ui-1440-app.png",
        "assets/seeds/orbit/verification-ui-1440-assistance-preview.png"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/orbit",
        "release": "0.1.0",
        "manifestSha256": "8793df0a4a4656a8e5860eade7657a4761d981d1abf76f0fcbea2fe26b69b594"
      }
    },
    {
      "id": "mango",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Mango",
      "dir": "mailchimp",
      "category": "marketing",
      "categoryLabel": "Marketing",
      "spine": {
        "id": "marketing",
        "record": "campaign over an audience"
      },
      "replaces": [
        {
          "name": "Mailchimp",
          "edition": null
        }
      ],
      "version": "0.1.0",
      "oneLiner": "An approval-first marketing desk that turns one ecommerce store's signals into evidence-backed email and SMS campaigns the owner reviews before…",
      "summary": "An approval-first marketing desk that turns one ecommerce store's signals into evidence-backed email and SMS campaigns the owner reviews before anything is sent.",
      "scope": "One self-hosted deployment for one ecommerce store and its equal operators.",
      "maturity": "contract-drafted-ui-prototype",
      "clauseCount": 78,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Owner front door.",
          "text": "The sessionless landing at `/` names the configured deployment owner, shows their configured one-line description of Mango, and links to `/signin`. It contains no vendor pricing, feature tour, testimonials, metrics, cross-store claims, or telemetry. It may show the static credit `Powered by Mango · runeditrun.com` only when the owner enables it in configuration."
        },
        {
          "id": "OPER-001",
          "title": "Equal operators.",
          "text": "Every operator can see and act on every proposal, campaign, contact, and setting. There are no roles."
        },
        {
          "id": "OPER-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. The operator configured at setup can be removed only by themselves. *Policy: `operator.management.v1`; default: any operator may invite or remove.*"
        },
        {
          "id": "OPER-003",
          "title": "Removal does not rewrite history.",
          "text": "Removing an operator revokes their sessions and leaves every proposal, approval, revision, and audit entry attributed to them intact."
        },
        {
          "id": "STORE-001",
          "title": "Store connection.",
          "text": "One commerce connection imports customers, products, variants, inventory, orders, refunds, carts, and browsing events through `commerce.store.v1`, retaining each source record's identifier and occurrence time."
        },
        {
          "id": "STORE-002",
          "title": "Source identity.",
          "text": "A commerce webhook or scheduled sync carrying a source identifier already stored updates that source record; it never creates a second customer, order, cart, refund, or browsing event."
        },
        {
          "id": "STORE-003",
          "title": "Freshness.",
          "text": "The Decision Desk shows, per source type, the completion time of the last successful sync and the error of any sync that has failed since it."
        },
        {
          "id": "STORE-004",
          "title": "Source failure.",
          "text": "If the configured commerce adapter is unavailable, ingestion and proposal generation record a failed operation carrying the adapter's error; saved drafts and already recorded delivery outcomes remain readable and no campaign is sent as a side effect."
        },
        {
          "id": "STORE-005",
          "title": "Stale sources block approval.",
          "text": "A source type is stale when its last successful sync completed more than 24 hours ago or a sync has failed since it. A proposal or campaign whose inclusion or exclusion rule reads a stale source type is labelled stale and cannot be approved until that source type syncs successfully."
        },
        {
          "id": "STORE-006",
          "title": "Declared adapter capability.",
          "text": "The commerce connection declares which source types it supplies. An audience rule, exclusion, or proposal that requires a source type the connection does not supply is shown as unavailable and cannot be approved; it never resolves to an empty or partial audience."
        },
        {
          "id": "STORE-007",
          "title": "Ingestion latency.",
          "text": "A source record from an accepted commerce webhook is readable in the operator app within 60 seconds of the webhook being acknowledged."
        },
        {
          "id": "CONT-001",
          "title": "Contact identity.",
          "text": "Email addresses are compared case-insensitively after trimming, phone numbers are stored in E.164 form, and one canonical email address or phone number identifies at most one contact."
        },
        {
          "id": "CONT-002",
          "title": "Channel status.",
          "text": "Each contact has an independent state per channel: *pending* (consent captured, confirmation outstanding), *subscribed*, *non-subscribed* (no consent evidence recorded), *unsubscribed* (opted out), or *suppressed* (blocked by CONT-006 or CONT-013). Only *subscribed* is eligible for a marketing send. Store activity or consent on one channel never changes the other channel's state."
        },
        {
          "id": "CONT-003",
          "title": "Consent evidence.",
          "text": "Every subscription stores the channel, brand or legal sender, SMS sender or programme where applicable, message subject or category, disclosed frequency, destination jurisdiction, capture method and source, occurrence time, consent text and version, and confirmation evidence. Consent applies only to that recorded scope and current recipient; a capture or import without the evidence creates a non-subscribed contact, and a phone number reported as reassigned or no longer belonging to that recipient becomes non-subscribed until fresh evidence is captured."
        },
        {
          "id": "CONT-004",
          "title": "Opt-out precedence.",
          "text": "An email unsubscribe, one-click unsubscribe, or SMS opt-out sets that channel to unsubscribed before the request is acknowledged, and excludes it from every draft, scheduled campaign, and queued recipient-step. Later imports, syncs, store events, merges, or operator edits never reverse it."
        },
        {
          "id": "CONT-005",
          "title": "Resubscription.",
          "text": "A previously unsubscribed channel becomes subscribed only from new affirmative consent evidence recorded after the opt-out and confirmed per CONT-009. A suppressed channel also requires the suppression cause to be resolved. An operator edit alone cannot resubscribe either state."
        },
        {
          "id": "CONT-006",
          "title": "Email suppression.",
          "text": "A hard bounce, spam complaint, or provider suppression suppresses that email address before the next hand-off to any campaign. Soft bounces are recorded and visible, and suppress the address according to the bounce policy; a later provider-confirmed delivery resets its consecutive-soft-bounce count. *Policy: `contact.soft-bounce.v1`; default: suppress after three consecutive soft bounces.*"
        },
        {
          "id": "CONT-007",
          "title": "SMS compliance replies.",
          "text": "A signed inbound SMS webhook consumes the adapter's canonical result for that sender, programme, and destination jurisdiction. STOP, STOPALL, END, QUIT, CANCEL, UNSUBSCRIBE, OPTOUT, and REVOKE always opt out; HELP and INFO request help; START and UNSTOP begin the fresh-consent flow required by CONT-005 and never subscribe by themselves. Mango records each reply once and applies an opt-out before acknowledging the webhook. It returns the configured compliance response only when the provider has not already sent the required reply."
        },
        {
          "id": "CONT-008",
          "title": "Contact history.",
          "text": "A contact page shows source activity, consent changes, suppressions, campaign eligibility decisions, channel deliveries, and operator actions in time order."
        },
        {
          "id": "CONT-009",
          "title": "Confirmation.",
          "text": "Where the opt-in policy requires confirmation for a channel, a contact captured with consent on it enters *pending* and receives one confirmation message on that channel; following its opaque per-contact confirmation link sets the channel to *subscribed* and stores the confirming request's time. A pending channel receives no marketing send, and a confirmation link stops working 30 days after it is issued. Where the policy does not require confirmation, the captured consent evidence of CONT-003 sets the channel to *subscribed* directly. *Policy: `contact.opt-in.v1`; default: confirmation is required on both channels.*"
        },
        {
          "id": "CONT-010",
          "title": "Import.",
          "text": "An operator imports contacts from a CSV whose rows carry an email address or phone number and, optionally, the consent evidence CONT-003 requires. Each row is validated independently; a row matching an existing canonical identifier updates that contact rather than creating a second; re-running the same file changes nothing further. The import reports created, updated, and rejected row counts with a reason per rejected row, and never sets a channel to subscribed for a contact CONT-004 has unsubscribed."
        },
        {
          "id": "CONT-011",
          "title": "Merge.",
          "text": "An operator can merge two contacts. The surviving contact keeps the union of source activity, consent records, campaign history, and suppressions, and takes the most restrictive state per channel: suppressed beats unsubscribed, which beats non-subscribed, which beats pending, which beats subscribed. A merge cannot resubscribe a channel."
        },
        {
          "id": "CONT-012",
          "title": "Dormancy without deletion.",
          "text": "A contact with no recorded click, order, or consent change inside the dormancy window is labelled dormant and excluded by the audience policy (AUD-003). Mango never deletes, unsubscribes, or suppresses a contact for inactivity; BASE-DATA-002 stands, and clearing a dormant list is an operator action."
        },
        {
          "id": "CONT-013",
          "title": "Erasure and surviving opt-out.",
          "text": "Deleting a contact removes their profile, source activity, consent evidence, message content, and delivery detail within five minutes, confirms completion, and anonymises their actor identity in retained audit entries as BASE-DATA-003 requires. It retains only one opt-out record per unsubscribed or suppressed channel: a salted one-way hash of the email address or phone number, the channel, reason, and time. A later import, sync, or capture matching that hash creates a contact whose channel is already unsubscribed. **Supersedes BASE-DATA-003 only for this opt-out dependency**; all of that baseline clause's other guarantees and all other personal-data entities remain unchanged."
        },
        {
          "id": "PROP-001",
          "title": "Decision Desk.",
          "text": "The Decision Desk lists proposals in the *proposed* state ordered by the proposal policy, highest-ranked first. *Policy: `proposal.selection.v1`; default: rank by expected revenue where this deployment has enough of its own attribution history (RESULT-003) to estimate it, then by the earliest send-by instant; proposals with neither rank last.*"
        },
        {
          "id": "PROP-002",
          "title": "Evidence.",
          "text": "A proposal names its triggering source events, lookback window, inclusion rule, exclusion rule, source-data as-of time, send-by instant, proposed channels and step instants, and the method behind every forecast or confidence label."
        },
        {
          "id": "PROP-003",
          "title": "Counts.",
          "text": "A proposal distinguishes candidates, excluded contacts, eligible contacts, and per-channel recipient-steps. No count is labelled recipients until exclusions have been applied."
        },
        {
          "id": "PROP-004",
          "title": "Content.",
          "text": "A proposal contains an editable campaign name, email subject and body, SMS body when SMS is proposed, sender identity, destination links, and a preview for every selected channel."
        },
        {
          "id": "PROP-005",
          "title": "Owner control.",
          "text": "A generated proposal is a draft and cannot schedule or send anything until an operator explicitly approves a particular saved revision."
        },
        {
          "id": "PROP-006",
          "title": "Grounded generation.",
          "text": "AI-generated audience explanations, timing claims, and forecasts use only this deployment's imported store data and configured knowledge, supplied to `ai.provider.v1`. A generation containing a claim not derivable from that evidence is rejected and the proposal is marked failed per PROP-007; no generic marketing copy is substituted."
        },
        {
          "id": "PROP-007",
          "title": "Generation failure.",
          "text": "If the configured AI adapter returns invalid output or does not complete within 60 seconds, the proposal is marked failed carrying the provider error, no semantic fallback is created, and existing proposals and campaigns are unaffected."
        },
        {
          "id": "PROP-008",
          "title": "Forecast provenance.",
          "text": "Every revenue, recovery-rate, or confidence figure names the deployment's own campaigns and store events it was computed from and the number of them. Mango never presents a benchmark drawn from other stores, and where its own history is too thin to compute a figure it says so instead of showing a range."
        },
        {
          "id": "PROP-009",
          "title": "Proposal states.",
          "text": "A proposal is *proposed*, *approved* (an operator approved a revision and a campaign exists), *dismissed* (an operator rejected it, with the reason recorded), *expired* (its send-by instant passed), or *failed* (PROP-007). Only a proposed proposal can be approved or dismissed, and every transition records its actor and time."
        },
        {
          "id": "AUD-001",
          "title": "Review snapshot.",
          "text": "Campaign review shows the audience rule and as-of time, candidate count, each exclusion reason with its count, eligible contact count, and the recipient-step count per channel."
        },
        {
          "id": "AUD-002",
          "title": "Consent and suppression.",
          "text": "Contacts whose state on a channel is pending, non-subscribed, unsubscribed, or suppressed are excluded from that channel regardless of segment membership or an operator-authored inclusion rule."
        },
        {
          "id": "AUD-003",
          "title": "Commerce and dormancy exclusions.",
          "text": "Contacts excluded by the audience policy are listed in review with their reason and count. *Policy: `audience.exclusions.v1`; default: exclude a contact from a product campaign for seven days after purchasing that product, while a refund or return of it is unresolved, and while the contact is dormant under a twelve-month dormancy window (CONT-012).*"
        },
        {
          "id": "AUD-004",
          "title": "Frequency protection.",
          "text": "A contact inside the channel's frequency window is excluded from that step and recorded with the reason `frequency-window`, never silently deferred to a later time. *Policy: `audience.frequency.v1`; default: 24 hours since the contact's last marketing email and 72 hours since their last marketing SMS.*"
        },
        {
          "id": "AUD-005",
          "title": "Dispatch recheck.",
          "text": "Eligibility is recalculated when a step's dispatch begins and again immediately before each provider hand-off, so a purchase, refund, opt-out, bounce, complaint, consent change, merge, or frequency-window change after approval prevents that delivery and is recorded as the exclusion reason. Every source type used by the audience must still satisfy STORE-005; if refresh fails or the source becomes stale, the affected channel pauses before any further hand-off and resumes only after a successful refresh."
        },
        {
          "id": "AUD-006",
          "title": "Multi-step exit.",
          "text": "Each later step re-applies its audience and exclusion rules; a contact who converts or becomes ineligible after an earlier step receives no later step."
        },
        {
          "id": "AUD-007",
          "title": "Frozen candidate set.",
          "text": "Approval freezes the candidate set resolved at that instant. A contact who first matches the inclusion rule after approval is never added to the approved campaign, and each contact yields at most one recipient-step per channel per step even when they match the rule more than once. Exclusions still apply at dispatch (AUD-005)."
        },
        {
          "id": "CAMP-001",
          "title": "States.",
          "text": "A campaign is *draft* (editable, nothing scheduled), *scheduled* (an approved revision with a future step instant), *dispatching* (a step has begun handing off), *paused* (dispatch stopped by SEND-009), *completed* (SEND-007), *cancelled* (CAMP-009), or *failed* (its first step could not begin before its deadline because a required adapter or sender registration was unavailable). Every transition records its actor or the feature that acted, and its time; an invalid transition is rejected."
        },
        {
          "id": "CAMP-002",
          "title": "Draft editing.",
          "text": "Operators can edit and persist the campaign name, selected channels, audience rule, message content, sender identity, links, and schedule while a campaign is a draft."
        },
        {
          "id": "CAMP-003",
          "title": "Revision preview.",
          "text": "Desktop, mobile, and plain-text previews render from the saved revision that will be approved, including resolved personalisation examples and the compliance footer or SMS opt-out text that will be sent."
        },
        {
          "id": "CAMP-004",
          "title": "Approval checks.",
          "text": "Approval atomically validates the audience, consent, source freshness (STORE-005), sender registration, email authentication, sending reputation (MSG-011), compliance content, personalisation, links, schedule, and adapter configuration. A sender adapter must provide effective deduplication and provider reconciliation as required by SEND-001 and SEND-008. Any failure leaves the campaign a draft and names every blocking check."
        },
        {
          "id": "CAMP-005",
          "title": "Idempotent approval.",
          "text": "Repeated approval requests carrying the same campaign and revision identifier produce one scheduled campaign and one set of steps and recipient-steps, whatever the number of requests."
        },
        {
          "id": "CAMP-006",
          "title": "Schedule.",
          "text": "Each step stores an IANA time zone and the resolved UTC instant. A local time that does not exist or is ambiguous under daylight saving is rejected until the operator chooses an unambiguous instant. *Policy: `campaign.send-time.v1`; default: resolve every step against the store time zone and the operator-selected local time.*"
        },
        {
          "id": "CAMP-007",
          "title": "Scheduled edits.",
          "text": "Editing a scheduled campaign creates a new draft revision and leaves the approved revision active until the replacement passes approval. Replacement approval atomically makes the new revision the campaign's only active schedule and cancels every pending recipient-step of the prior revision. Once any step has begun dispatching, edits can only create a new campaign; the original campaign and its later steps remain active until the operator explicitly cancels them."
        },
        {
          "id": "CAMP-008",
          "title": "Test send.",
          "text": "A test sends only to the configured operator test addresses or phone numbers, is visibly marked as a test in its content and in campaign history, and changes no contact activity, reporting, attribution, or frequency protection."
        },
        {
          "id": "CAMP-009",
          "title": "Cancellation.",
          "text": "Cancelling a scheduled, dispatching, or paused campaign stops every step and every recipient-step confirmed not handed to a provider within 30 seconds, records provider-confirmed hand-offs separately, and leaves an in-flight request in `handoff-unknown` until SEND-008 reconciles it. It never claims to recall a delivered message. Campaign detail shows the time remaining until the next step dispatches."
        },
        {
          "id": "CAMP-010",
          "title": "Step deadline.",
          "text": "Each step carries a send-by instant, defaulting to the end of that step's scheduled local day. Recipient-steps not handed to a provider by then are skipped and recorded as `deadline-passed`; a step whose deadline passes before dispatch begins does not send."
        },
        {
          "id": "CAMP-011",
          "title": "Dispatch punctuality.",
          "text": "Dispatch of a scheduled step begins within 60 seconds of its resolved UTC instant, and each step's instant dispatches exactly once even if the scheduler runs twice."
        },
        {
          "id": "MSG-001",
          "title": "Sender identity.",
          "text": "Every email uses the configured business display name, From address, reply-to address, and physical postal address; every SMS uses a sender registered for the recipient's country."
        },
        {
          "id": "MSG-002",
          "title": "Email authentication.",
          "text": "A marketing email cannot be scheduled unless the mail adapter reports SPF, DKIM, and DMARC passing for the sending domain and the visible From domain aligns with an authenticated domain."
        },
        {
          "id": "MSG-003",
          "title": "Email unsubscribe.",
          "text": "Every marketing email carries a visible unsubscribe link and an opaque per-recipient HTTPS `List-Unsubscribe` URL with `List-Unsubscribe-Post: List-Unsubscribe=One-Click`; DKIM covers both headers. The one-click URL accepts a context-free HTTPS POST body of `List-Unsubscribe=One-Click`, does not redirect, and applies CONT-004 before returning success. The visible link opens a page that applies CONT-004 on a single confirming action and never on page load, so a mailbox scanner or link prefetcher cannot unsubscribe anyone. Both paths are idempotent under repeats, need no session or cookie, and reveal no contact other than their own."
        },
        {
          "id": "MSG-004",
          "title": "SMS permission.",
          "text": "An SMS is eligible only when its phone number is subscribed for the recorded consent scope and the SMS adapter confirms an approved sender for that recipient's country. For every permitted sender and jurisdiction, the adapter must be able to receive and enforce the standard opt-out and help keywords in CONT-007; otherwise SMS is blocked for that recipient."
        },
        {
          "id": "MSG-005",
          "title": "SMS opt-out.",
          "text": "Every marketing SMS carries the configured programme identity and `Reply STOP to unsubscribe`, or the jurisdiction's required equivalent, and the configured frequency disclosure where required. An opaque unsubscribe URL may be additional but never replaces reply-based opt-out. Review shows the exact compliance text and resulting segment count."
        },
        {
          "id": "MSG-006",
          "title": "Links.",
          "text": "Scheduling is blocked when a destination link is malformed, is not HTTPS, or resolves to a host outside the allowed link hosts configured in settings. Review names every blocked link."
        },
        {
          "id": "MSG-007",
          "title": "Content checks.",
          "text": "Review names blocking and advisory content findings per channel. An operator can acknowledge an advisory finding; the consent, sender registration, opt-out, authentication, link, personalisation, and reputation checks are blocking and cannot be overridden."
        },
        {
          "id": "MSG-008",
          "title": "Personalisation.",
          "text": "Every personalisation token carries a fallback value. Scheduling is blocked when a token has no fallback and cannot be resolved for any one eligible contact; otherwise the fallback is rendered and the count of contacts using it is shown in review."
        },
        {
          "id": "MSG-009",
          "title": "Email body.",
          "text": "Every marketing email is sent as `multipart/alternative` with an HTML part and a plain-text part carrying the same offer, links, and unsubscribe link, plus the preheader text set on the revision. Operator-authored HTML is transmitted as authored within the allowed tag and attribute set, which admits no script, form, or remote stylesheet; the preview renders it sanitised per BASE-INPUT-002."
        },
        {
          "id": "MSG-010",
          "title": "Unsubscribe availability.",
          "text": "The unsubscribe path answers within 2 seconds and is rate-limited per unsubscribe token at the limit in `seed.json`, not per client IP address. The token is the rate-limit key declared for this public path under BASE-ACCESS-003, so mailbox-provider requests for unrelated recipients cannot consume one another's allowance; an exceeded token allowance returns 429 without affecting other tokens."
        },
        {
          "id": "MSG-011",
          "title": "Sending reputation.",
          "text": "Once a sending domain has at least 1,000 marketing deliveries in the reporting window, scheduling a marketing email from it is blocked while provider-reported complaints divided by delivered marketing emails are at or above 0.3%, or hard bounces divided by attempted marketing hand-offs are at or above 5% (RESULT-006). A provider's stricter block applies at any volume. The block names each numerator, denominator, reporting window, and provider rule, cannot be acknowledged away, recomputes when late facts arrive, and clears only when every applicable threshold is below its limit."
        },
        {
          "id": "SEND-001",
          "title": "Provider hand-off.",
          "text": "Email uses `mail.sender.v1` and SMS uses `sms.sender.v1`. Every recipient-step carries a provider-effective idempotency key derived from the campaign, revision, step, and contact. Mango stores the provider message identifier before treating it as handed off. A timeout or lost response becomes `handoff-unknown` and is reconciled by provider lookup before any retry; an adapter whose deduplication retention cannot cover the campaign deadline and reconciliation window cannot pass CAMP-004. A retried job, redelivered queue message, or resumed dispatch therefore never produces a second provider message."
        },
        {
          "id": "SEND-002",
          "title": "Quiet hours.",
          "text": "SMS is handed off only inside the recipient-local legal and provider-required windows. The recipient time zone is resolved from recorded location evidence with its source and freshness, never substituted with the store time zone; an unknown or stale zone blocks hand-off. Legal and provider windows are mandatory minima that a policy cannot weaken. *Policy: `sms.quiet-hours.v1`; default: defer until the next allowed instant at or before the step's send-by instant (CAMP-010), otherwise skip and record `quiet-hours-deadline`.*"
        },
        {
          "id": "SEND-003",
          "title": "Throttling and progress.",
          "text": "Dispatch paces hand-offs to the provider and carrier rate limits in `seed.json`. Campaign status shows the estimated completion instant and updates the handed-off, delivered, skipped, and failed counts within 30 seconds of each change."
        },
        {
          "id": "SEND-004",
          "title": "Transport failure.",
          "text": "A transient provider or network failure on a recipient-step is retried up to 5 times over 1 hour with increasing delay, except that `handoff-unknown` is reconciled rather than retried. A permanent rejection or exhausted retry budget marks that recipient-step failed with the provider's error and campaign detail shows it; a hard rejection, complaint, or provider suppression also updates CONT-006 atomically before another hand-off."
        },
        {
          "id": "SEND-005",
          "title": "Provider-event identity.",
          "text": "Every signed provider webhook is stored once as an immutable fact using the strongest identity that adapter declares: provider event identifier where supplied, otherwise provider message identifier, event kind, provider event time or version, and a stable payload digest. Replaying or reordering delivery, bounce, complaint, suppression, and SMS reply facts produces the same derived delivery disposition and contact state, and an older fact never downgrades a later provider disposition."
        },
        {
          "id": "SEND-006",
          "title": "Delivery truth.",
          "text": "A recipient-step has a transport disposition of queued, handoff-unknown, handed-off, accepted, delivered, bounced, skipped, or failed, plus independent complaint and suppression facts. *Delivered* is recorded only from a provider fact asserting delivery; provider acceptance is never reported as delivery. A later complaint remains visible without erasing the earlier delivery fact, and a skipped disposition carries its reason."
        },
        {
          "id": "SEND-007",
          "title": "Partial completion.",
          "text": "A campaign becomes completed only once every recipient-step has a terminal transport disposition; `handoff-unknown` is not terminal. Its summary keeps delivered, skipped, failed, bounced, complained, and suppressed totals separate and never sums them into a single success figure. Later complaint or suppression facts update the completed campaign's summary and the contact ledger."
        },
        {
          "id": "SEND-008",
          "title": "Resumable dispatch and reconciliation.",
          "text": "Dispatch survives worker eviction, redeployment, and restart: it resumes from recipient-steps without a terminal transport disposition, sends nothing twice (SEND-001), and needs no operator action. At the reconciliation interval in `seed.json`, it queries the provider for handoff-unknown and other unresolved messages and imports missed provider facts. After the provider's declared lookup window expires, an unresolved message becomes failed with `outcome-unconfirmed`; it is never counted as delivered or retried blindly. A campaign never remains dispatching with no work or reconciliation in progress."
        },
        {
          "id": "SEND-009",
          "title": "Provider outage.",
          "text": "When hand-offs to a provider fail transiently for 5 continuous minutes, or the adapter reports the provider unavailable, the affected campaign channel moves to paused and no recipient hand-offs are attempted on it. The adapter runs a bounded health probe or non-recipient canary at the reconciliation interval in `seed.json`; a successful probe resumes dispatch automatically, subject to CAMP-010 and AUD-005. The pause and provider error are shown on campaign detail, and each operator is emailed once as soon as the mail provider is available."
        },
        {
          "id": "RESULT-001",
          "title": "Campaign history.",
          "text": "Operators can list campaigns by state and channel, open the approved revision, and inspect its audience snapshot, delivery timeline, cancellation, and per-channel outcome totals."
        },
        {
          "id": "RESULT-002",
          "title": "Engagement.",
          "text": "Clicks and store conversions are shown as observed events. Open tracking is off by default; when an operator enables it, opens are labelled privacy-sensitive estimates and never support a confidence, deliverability, or attribution figure."
        },
        {
          "id": "RESULT-003",
          "title": "Attribution.",
          "text": "Revenue attributed to a campaign names the qualifying store event and attribution window, is reported separately from any forecast, and is assigned at most once per order. *Policy: `campaign.attribution.v1`; default: the most recent non-automated tracked click in the seven days before purchase wins; a cancellation or refund subtracts the corresponding attributed amount.*"
        },
        {
          "id": "RESULT-004",
          "title": "Cost.",
          "text": "Each AI call and provider hand-off records the external provider, model or channel, billable units, and reported cost; totals are visible by campaign and by day."
        },
        {
          "id": "RESULT-005",
          "title": "Link tracking.",
          "text": "A tracked link is rewritten to an opaque per-recipient-step redirect on this deployment that records the request and forwards to the original URL. The redirect sets no cookie, loads nothing, and carries no contact identifier in the destination URL. Known mailbox security scanners and bot requests are retained as automated events but excluded from engagement and attribution. An operator can disable tracking per campaign, which sends the original URLs and records no clicks."
        },
        {
          "id": "RESULT-006",
          "title": "Deliverability reporting.",
          "text": "Per sending domain and SMS sender, Mango shows the delivered, bounced, complained, and failed counts and the resulting hard-bounce and spam-complaint rates over the reporting window in `seed.json`, and the rates MSG-011 is currently evaluating."
        },
        {
          "id": "DATA-001",
          "title": "Export contents.",
          "text": "The export contains every contact, source record, consent record, suppression and surviving opt-out record (CONT-013), proposal, campaign, revision, audience decision, message, recipient-step outcome, attribution event, setting, and audit entry as JSON, and every uploaded campaign asset as its original file."
        }
      ],
      "hasReadme": false,
      "hasBaseline": true,
      "nonGoals": [
        "Multiple stores or workspaces",
        "Roles and permissions",
        "A general CRM",
        "Signup-form, landing-page, and website builders",
        "Social and advertising campaigns",
        "Store transactional messaging (order, shipping, and account notifications)",
        "Autonomous sends",
        "Visual journey automation",
        "A/B and multivariate testing",
        "Predictive send-time models",
        "Multi-touch attribution",
        "Purchased-list prospecting",
        "Native mobile apps"
      ],
      "externals": [
        {
          "name": "commerce-store",
          "required": true,
          "requiredWhen": "",
          "reason": "Mango needs the buyer's real customers, products, carts, browsing signals, orders, refunds, and inventory to identify and recheck opportunities.",
          "data": [
            "Webhook acknowledgements sent back to the store confirming receipt of an event",
            "Explicit adapter requests for source records (customers, products, orders, refunds, carts, browsing events)"
          ],
          "adapters": [
            "commerce.store.v1"
          ]
        },
        {
          "name": "ai-provider",
          "required": true,
          "requiredWhen": "",
          "reason": "A language model turns structured evidence into reviewable campaign content and explanations; Cloudflare primitives do not provide that model capability.",
          "data": [
            "The proposal evidence required for the requested generation",
            "The configured business context required for the requested generation"
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        },
        {
          "name": "mail-sender",
          "required": true,
          "requiredWhen": "",
          "reason": "Internet email delivery, domain authentication, bounce handling, and complaint feedback require a delivery provider with established sending infrastructure.",
          "data": [
            "Approved email content",
            "Sender metadata",
            "Each eligible recipient's email address",
            "Operator email addresses that receive magic links, test sends, and dispatch-paused notices"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        },
        {
          "name": "sms-sender",
          "required": true,
          "requiredWhen": "",
          "reason": "Registered sending numbers, carrier routing, country approval, compliance keywords, and delivery receipts require a telecommunications provider.",
          "data": [
            "Approved SMS content",
            "Programme metadata",
            "Each eligible recipient's phone number"
          ],
          "adapters": [
            "sms.sender.v1"
          ]
        }
      ],
      "operatingCost": {
        "assumptions": "One store, 10000 contacts, 4 campaigns per month, 20000 email and 5000 SMS recipient-steps, and 500 AI generations. Queue messages are at most 64 KB and dispatch stays below 3334 recipient-steps per UTC day.",
        "cloudflareMonthlyUsd": "$0 on the current Workers Free plan while the deployment stays inside every Free quota: at most 100000 dynamic Worker requests per UTC day with 10 ms CPU per invocation; 10000 Queue operations per UTC day (write, read, and delete are each one operation per 64 KB); D1 at 5 million rows read per UTC day, 100000 rows written per UTC day, and 5 GB total storage; and SQLite Durable Objects at 100000 requests and 13000 GB-s per UTC day. Workers Paid starts at $5/month and is required for the declared 100000-contact campaign: its 100000 recipient-step writes plus step and schedule mutations exceed D1 Free's 100000 rows-written-per-UTC-day limit.",
        "externalMonthlyUsd": "Not yet estimated because the reference commerce, email, SMS, and AI adapters have not been selected; required before first release.",
        "totalMonthlyUsd": "Incomplete until reference adapters and unit prices are selected.",
        "status": "cloudflare-free-plan-assumptions-researched-2026-08-31; external-adapters-unselected"
      },
      "extensionPoints": [
        "routes.v1",
        "pages.v1",
        "navigation.v1",
        "settings.sections.v1",
        "jobs.v1",
        "cron.v1",
        "queue.consumers.v1",
        "operator.management.v1",
        "contact.opt-in.v1",
        "contact.soft-bounce.v1",
        "proposal.selection.v1",
        "audience.exclusions.v1",
        "audience.frequency.v1",
        "campaign.send-time.v1",
        "sms.quiet-hours.v1",
        "campaign.attribution.v1",
        "store.event-recorded.v1",
        "contact.consent-recorded.v1",
        "contact.channel-suppressed.v1",
        "contact.merged.v1",
        "proposal.created.v1",
        "proposal.failed.v1",
        "campaign.scheduled.v1",
        "campaign.dispatch-paused.v1",
        "campaign.cancelled.v1",
        "campaign.completed.v1",
        "delivery.outcome-recorded.v1",
        "app.navigation.after.v1",
        "decision-desk.header.after.v1",
        "proposal.evidence.after.v1",
        "proposal.actions.before.v1",
        "campaign.review.summary.after.v1",
        "campaign.review.audience.after.v1",
        "campaign.review.safety.after.v1",
        "campaign.detail.tabs.after.v1",
        "contact.detail.tabs.after.v1",
        "settings.sections.after.v1",
        "commerce.store.v1",
        "ai.provider.v1",
        "mail.sender.v1",
        "sms.sender.v1"
      ],
      "limits": {
        "status": "estimated-until-load-tested",
        "maxContacts": 100000,
        "maxStoredCommerceEvents": 5000000,
        "maxContactImportRows": 50000,
        "maxCampaignSteps": 10,
        "maxEligibleContactsPerCampaign": 100000,
        "maxCampaignAssetBytes": 10485760,
        "allowedCampaignAssetContentTypes": [
          "image/jpeg",
          "image/png",
          "image/gif",
          "image/webp"
        ],
        "maxEmailHtmlBytes": 102400,
        "maxSmsSegmentsPerRecipientStep": 3,
        "emailHandoffsPerMinute": 3000,
        "smsHandoffsPerMinute": 600,
        "deliverabilityReportingWindowDays": 30,
        "operatorRequestsPerMinute": 300,
        "publicRequestsPerMinutePerIp": 60,
        "unsubscribeRequestsPerMinutePerToken": 10,
        "providerWebhooksPerMinute": 5000,
        "providerReconciliationIntervalMinutes": 15,
        "unresolvedProviderOutcomeHours": 24
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target-not-yet-verified"
      },
      "landing": "assets/seeds/mango/01-landing-desktop.png",
      "shots": [
        "assets/seeds/mango/01-landing-desktop.png",
        "assets/seeds/mango/03-decision-desk-desktop.png",
        "assets/seeds/mango/04-campaign-review-desktop.png",
        "assets/seeds/mango/05-campaign-scheduled-desktop.png",
        "assets/seeds/mango/06-campaigns-desktop.png",
        "assets/seeds/mango/07-audience-desktop.png"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/mango",
        "release": "0.1.0",
        "manifestSha256": "9dc022a75a39f9ac62a9b6686b9b512cf435dd216c5e8835cb5b93ec405a83c1"
      }
    },
    {
      "id": "slate",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Slate",
      "dir": "notion",
      "category": "knowledge",
      "categoryLabel": "Knowledge",
      "spine": {
        "id": "knowledge",
        "record": "a page in a tree in a container"
      },
      "replaces": [
        {
          "name": "Notion",
          "edition": null
        },
        {
          "name": "Confluence",
          "edition": null
        }
      ],
      "version": "0.1.0",
      "oneLiner": "A self-hosted knowledge workspace where pages live in a tree inside containers, with structured records, search and AI beside them.",
      "summary": "A self-hosted knowledge workspace where pages live in a tree inside containers, with structured records, search and AI beside them.",
      "scope": "One business, one workspace: containers of nested pages, invited members with access levels, groups and guests, and pages published to the anonymous web only by an explicit act.",
      "maturity": "foundation-and-components-consolidated: TanStack Start and Hono share one Worker with native sessions and the knowledge migration; real-export parsers and the supplied-prop UI library pass their regressions and clean-checkout verification; all 191 product contract journeys remain planned; no deployment or release tag",
      "clauseCount": 191,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Owner identity plate.",
          "text": "The public root path (`/`) shows only the deployment owner's buyer-configured mark, name, and one-line description, plus a sign-in action. It may show a static “Powered by Slate · runeditrun.com” link when the buyer-configured credit toggle is on, which is its default. The landing exposes no workspace records or member identity, makes no marketing claims, and shows no metrics, testimonials, pricing, feature tour, tracking, or third-party resources. Pages published to the anonymous web are served under the published-page prefix and are governed by `WEB-001` to `WEB-007`; the root never lists them."
        },
        {
          "id": "WORK-001",
          "title": "One workspace.",
          "text": "A deployment holds exactly one workspace and every container, page, record and comment belongs to it. Its name and mark are editable in settings and shown in the app header. No interface or endpoint creates a second workspace."
        },
        {
          "id": "WORK-003",
          "title": "Member management.",
          "text": "Members are invited by email address and removed in settings. A workspace owner may invite any person and remove any member other than the setup member; a member may invite and may remove no one. The member created at setup can be removed only by themselves, and `WORK-007` rejects removing the last workspace owner. No policy value, extension, automation or AI action changes any of this."
        },
        {
          "id": "WORK-006",
          "title": "Invitations.",
          "text": "An invitation is single-use, can be revoked before it is used, and can no longer be accepted 24 hours after it was sent; an expired invitation is retained, not deleted. Requesting an invitation returns the same response whether or not the address already belongs to a member."
        },
        {
          "id": "WORK-007",
          "title": "Removal.",
          "text": "Removing a member revokes their sessions and unused invitations immediately, removes their personal grants from every page and container, archives their personal container and its pages (`CONT-005`) rather than exposing or deleting them, leaves each page they owned with no owner and offers reassignment in bulk (`CONT-006`), and keeps their name on existing activity until personal-data erasure anonymises it (`BASE-DATA-003`). Removing the only remaining workspace owner is rejected."
        },
        {
          "id": "WORK-008",
          "title": "Roles.",
          "text": "A member is a workspace owner or a member. Workspace owners administer the workspace: settings, members, groups, guests, containers they are not in, and the site-wide switches in `CONT-007`. Every other permission comes from a grant on a page or a container (`SHARE-001`, `CONT-009`), never from the role."
        },
        {
          "id": "WORK-009",
          "title": "Groups.",
          "text": "A workspace owner can create a named group, add and remove members, and use it as a grant principal anywhere a person can be used. Changing a group's membership changes access at once and writes one activity entry, and deleting a group removes its grants without touching the grants of its members."
        },
        {
          "id": "WORK-010",
          "title": "Guests.",
          "text": "A guest is a person invited to named pages or containers only. A guest is outside the member count and the member list, reaches nothing but what they were granted, and cannot see the container index, the workspace member list, or search results beyond their grants. A container may forbid guests (`CONT-007`)."
        },
        {
          "id": "WORK-011",
          "title": "Workspace settings.",
          "text": "Settings are a shell with its own navigation, holding workspace identity, members, groups, guests, the status vocabulary, published-page and guest switches, connected services, AI, import and export. It is separate from a container's own settings shell (`CONT-004`), and a change in one never silently changes the other."
        },
        {
          "id": "WORK-012",
          "title": "Theme.",
          "text": "Light, dark and follow-the-system are offered, apply to every operator surface including editors, and persist per member across sessions and devices. The anonymous read view uses the published page's own theme (`WEB-002`), never the reader's member setting."
        },
        {
          "id": "WORK-013",
          "title": "First run.",
          "text": "A new deployment runs a short wizard that names the workspace, creates the first container and its first page, and ends on an invitation step that can be skipped. It runs once, is never shown to a member who joins later, and creates nothing beyond those three records."
        },
        {
          "id": "WORK-014",
          "title": "Shortcut reference.",
          "text": "One reference lists every keyboard shortcut and every typed formatting shortcut the editor accepts, is reachable from anywhere in the operator app by a single shortcut, and matches what the editor actually does."
        },
        {
          "id": "WORK-015",
          "title": "Identity provider.",
          "text": "Sign-in is by the baseline's magic link (`BASE-ACCESS-001`) and, when a workspace owner configures one, by an identity provider through `identity.provider.v1`. A provider sign-in creates no member who was not invited or allow-listed, and an unconfigured or failing provider leaves magic-link sign-in working and says which is unavailable. There is no password, no security question and no account-recovery path, because `BASE-ACCESS-001` stores no password: access to the mailbox is the credential, and the identity provider is the only alternative to it. A member who has lost their mailbox is re-invited at a new address (`WORK-003`), which is not a recovery flow and does not restore their old sessions."
        },
        {
          "id": "SAVE-001",
          "title": "Durable save state.",
          "text": "An editor shows Saving until the write and its activity entry are committed, then shows Saved within 2 seconds of the commit."
        },
        {
          "id": "SAVE-002",
          "title": "Failed writes.",
          "text": "A failed write leaves the editor in a failed state, never Saved. It keeps the unsent value, names the failure, and offers an explicit retry."
        },
        {
          "id": "SAVE-003",
          "title": "Revision conflicts.",
          "text": "Every edit is submitted with the revision it began from: the block's revision for a body edit, the page's for a title, property, structure or order change. An edit against a superseded revision is rejected with 409, keeps the local draft, and offers compare, reload, or explicit overwrite. Slate never resolves a conflict by last write wins."
        },
        {
          "id": "SAVE-004",
          "title": "Network loss.",
          "text": "While the server is unreachable the editor shows Offline and keeps the unsent draft in browser storage as recovery only. A recovered draft is labelled unsaved and never appears in the tree, in lists, in search, or in export as workspace data."
        },
        {
          "id": "SAVE-005",
          "title": "Operation keys.",
          "text": "Every mutation accepts an operation key. Repeating a key with the same body within 24 hours returns the first response and creates no second record, notification, AI run, publish, import, or activity entry; repeating it with a different body is rejected with 409."
        },
        {
          "id": "SAVE-006",
          "title": "Concurrent editing without co-editing.",
          "text": "Several members may edit different blocks of one page at the same time and each save succeeds; two edits to one block are resolved by `SAVE-003`, never merged. A page shows who else has it open as an avatar stack and nothing more: there are no shared cursors, no character-by-character streaming, and no automatic merge. An open page refreshes another member's committed blocks within 10 seconds, leaving the reader's own unsaved block untouched."
        },
        {
          "id": "PAGE-001",
          "title": "What a page is.",
          "text": "A page has an identifier, a title, an optional icon, an optional cover, an owner, contributors, created and updated times, a container, a position in that container's tree, a body (`BLOCK-001`), a lifecycle state (`LIFE-001`), grants (`SHARE-001`), a revision (`REV-001`) and a set of labels. A label is a short text tag, unique on the page, added and removed by anyone with edit access, shown in the page's details (`PAGE-007`), carried through import (`IMP-003`) and through export and re-import (`PORT-001`, `PORT-002`, `PORT-007`), and never discarded; filtering a container by label is a Confluence-edition contribution and is not in 1.0. A database record is a page (`DB-010`), and every comment thread, revision, decision record and public exposure record names exactly one page."
        },
        {
          "id": "PAGE-002",
          "title": "Creation.",
          "text": "A member with edit access on the destination can create a page blank or from a template (`TMPL-003`), from the tree, from the global create action, or as a child of an open page. It is created in exactly one container at one position, and its initial lifecycle state follows `LIFE-006`."
        },
        {
          "id": "PAGE-003",
          "title": "Identity survives everything.",
          "text": "A page keeps its identifier for its lifetime. Renaming it, editing it, moving it within or between containers, archiving, trashing, restoring, publishing and unpublishing never change it, and every link, mention, citation, content query and search result continues to resolve to it (`LINK-001`)."
        },
        {
          "id": "PAGE-004",
          "title": "Move.",
          "text": "A member with edit access on both places can move a page to another position, another parent, or another container, choosing the destination from a searchable picker. Children move with it, grants are re-resolved against the new parent and the change is shown before it is applied, and comments, versions and links are unchanged."
        },
        {
          "id": "PAGE-005",
          "title": "Duplicate.",
          "text": "Duplicating a page copies its title, icon, cover, body and properties into a new page with a new identifier, optionally including its children. It copies no comment, version, watcher, analytics figure, grant beyond the destination's inheritance, or public link."
        },
        {
          "id": "PAGE-006",
          "title": "Icon and cover.",
          "text": "A page takes an icon from emoji, a supplied icon set or an upload, and a cover from a gallery, an upload or a link. Both are removable, both render in the tree and in search results where the surface has room, and neither is required."
        },
        {
          "id": "PAGE-007",
          "title": "Details.",
          "text": "A page's details show its owner, creator, contributors, created and updated times, length, status (`STAT-002`), labels (`PAGE-001`), views (`PAGE-013`) and the pages that link to it. A member with `full` access can reassign the owner, which changes no grant and writes one activity entry."
        },
        {
          "id": "PAGE-008",
          "title": "Favourite.",
          "text": "A member can favourite a page or a container and unfavourite it. Favourites are personal, are listed in the navigation (`NAV-003`), and are visible to no other member."
        },
        {
          "id": "PAGE-009",
          "title": "Watch.",
          "text": "A member can watch a page, and watches it implicitly after commenting on it until they stop. A watched page delivers its comments, replies and published changes to that member's inbox (`NOTIF-001`). Watching is personal and a member's own action never notifies them."
        },
        {
          "id": "PAGE-010",
          "title": "Outline.",
          "text": "A page shows an outline built from its headings that reflects the current body within 2 seconds of an edit, and selecting an entry scrolls to that heading. A page with no headings shows no outline rather than an empty one."
        },
        {
          "id": "PAGE-011",
          "title": "Present.",
          "text": "Present mode fills the screen, paginates the page by its top-level headings, and shows no navigation, comments or editing affordances. Leaving it returns to the same scroll position."
        },
        {
          "id": "PAGE-012",
          "title": "Page display options.",
          "text": "Each page carries its own typeface from a fixed set, small-text and full-width toggles, and these are stored on the page and are the same for every reader. They change no stored content and survive export and import."
        },
        {
          "id": "PAGE-013",
          "title": "Analytics.",
          "text": "A page records total views and unique viewers over time with the members who viewed and edited it, and a container rolls the same figures up over its pages. Both scopes ship and neither is gated by a plan, a role beyond read access to the page, or a policy. A member's own views of their own page are counted and labelled as such, and analytics never expose a viewer a member could not otherwise see."
        },
        {
          "id": "PAGE-014",
          "title": "Reactions.",
          "text": "A member can add and remove an emoji reaction on a page and on a comment (`COMM-005`). Reactions show who reacted, are not notifications, and are not counted in analytics."
        },
        {
          "id": "TREE-001",
          "title": "A nested tree in a container.",
          "text": "Every container holds one tree of pages of unlimited depth. A node shows its icon, title and whether it has children, expands and collapses, and remembers its expansion per member. A page has exactly one parent — a page or the container root — and appears in exactly one place."
        },
        {
          "id": "TREE-002",
          "title": "Reordering and nesting.",
          "text": "A member with edit access can reorder a page among its siblings and re-parent it by dragging or through `PAGE-004`. Order is explicit and stored, is the same for every member, and never changes as a side effect of an edit, a rename or a publish."
        },
        {
          "id": "TREE-003",
          "title": "Shared with me.",
          "text": "A member's navigation lists pages granted to them individually that sit in containers they are not a member of, with their container named. Removing the grant removes the entry within the same load."
        },
        {
          "id": "TREE-004",
          "title": "Filter the tree.",
          "text": "Each container's tree carries a filter over page titles that narrows the visible nodes while keeping their ancestors, matches within 200ms of typing, and clears in one action without changing expansion state."
        },
        {
          "id": "TREE-005",
          "title": "Create in place.",
          "text": "A page can be created at the foot of a tree, as a child from any node's own actions, and from one global create action available on every operator screen. Each names the destination before creating."
        },
        {
          "id": "BLOCK-001",
          "title": "A body of blocks.",
          "text": "A page body is an ordered tree of typed blocks, each with an identifier, a type, content, and optional children. Every block has a drag handle, can be reordered and nested, can be selected with its neighbours, and can be duplicated or deleted. Block identifiers survive editing, moving and export, and an anchored comment (`COMM-001`) points at one."
        },
        {
          "id": "BLOCK-002",
          "title": "Inline text.",
          "text": "Text in a block carries bold, italic, underline, strikethrough, inline code, colour, links, and mentions, and nothing else. It is stored in one format, rendered under `BASE-INPUT-002`, and round-trips unchanged through page export, workspace export and import."
        },
        {
          "id": "BLOCK-003",
          "title": "The insert menu.",
          "text": "Typing `/` at the cursor opens an insert menu grouped by kind, filtered by what is typed, navigable and selectable by keyboard alone, and it inserts at the cursor without leaving the editor. Every block type this contract names is reachable from it, and on a touch client the same list is reachable from a labelled control above the keyboard."
        },
        {
          "id": "BLOCK-004",
          "title": "Markdown shortcuts.",
          "text": "Typed Markdown converts as it is written — headings, lists, quotes, code fences, dividers, bold, italic and inline code — and each conversion is undone by one undo without losing the typed characters. The reference in `WORK-014` lists every shortcut the editor accepts."
        },
        {
          "id": "BLOCK-005",
          "title": "Mentions.",
          "text": "Typing `@` offers people, pages and dates in one list. A person mention notifies them (`NOTIF-001`) and renders their name; a page mention renders the page's current title and follows renames and moves (`LINK-003`); a date mention renders in the reader's locale."
        },
        {
          "id": "BLOCK-006",
          "title": "Structured blocks.",
          "text": "The body carries headings, bulleted, numbered and toggle lists, quotes, dividers, callout panels, expandable sections, tables with header rows, and code blocks with a language and copy action. Each survives export and import and each has a Markdown or import equivalent named in `IMP-002` and `IMP-003`."
        },
        {
          "id": "BLOCK-007",
          "title": "Task items.",
          "text": "A checkable item in the body toggles for anyone with edit access, records who completed it and when, and is counted where the page shows a completion figure. It is a block, not a record in a database, and it never appears in a database view."
        },
        {
          "id": "BLOCK-008",
          "title": "Media blocks.",
          "text": "Images, video, audio and file attachments are inserted by upload, by link, or from a configured stock source, each with an optional caption and an alt text field, and stored under `BASE-INPUT-003`. This clause supersedes `BASE-INPUT-003` on disposition alone. A file requested for display — an image, video or audio block, a page icon or cover (`PAGE-006`), or either of those behind a published page (`WEB-003`) — is served inline with its declared content type; every other request for a stored file, including every file attachment and every direct request for a media file, is served with a download disposition. Both dispositions send the declared content type and `X-Content-Type-Options: nosniff`, and an SVG is sanitised under `BASE-INPUT-002` before it is served inline. Disposition changes nothing about access: a media file is readable only by a session that may read its page, or through that page's public link (`WEB-003`)."
        },
        {
          "id": "BLOCK-009",
          "title": "Link cards.",
          "text": "A pasted URL becomes a link card showing the target's title, description and icon, and can be switched between a plain link, an inline card and a block card without retyping it. Fetching a preview goes through `link.preview.v1` and issues a bodyless GET to public HTTPS origins only, sends no session, credential or member identity, rejects local, private, link-local and cloud-metadata addresses before the first request and before each redirect, and enforces the redirect, byte, time and media-type limits in `seed.json → limits`. A refused or failed fetch leaves the plain link and says so; the URL is stored exactly as pasted either way."
        },
        {
          "id": "BLOCK-010",
          "title": "Content queries.",
          "text": "A body can hold a live list of pages defined by container, ancestor, type, status, contributor and date, ordered and limited by the author. It re-runs when the page is opened, shows when it last ran, shows each reader only the pages that reader may read, and never renders a draft (`LIFE-002`). A query whose source is deleted shows that, not an empty list."
        },
        {
          "id": "LIFE-001",
          "title": "Lifecycle states.",
          "text": "A page is `draft`, `scheduled`, `live`, `live-with-unpublished-changes` or `archived`. It always has a working revision and has a published revision in every state but `draft`; `live-with-unpublished-changes` means both exist and differ. Every transition records the actor and the time, and every state exists in the stored model whichever value `LIFE-006` carries."
        },
        {
          "id": "LIFE-002",
          "title": "A draft is private.",
          "text": "While a page is `draft` it is absent from search, from content queries, from the inbox, from analytics, from AI grounding, from every administrative and export listing, and from the tree of every member it has not been shared with. Its body is readable only by its author and by the principals in its own grant list: sharing a draft, by link or by address, creates a grant under `SHARE-001` and `SHARE-006`, that grant requires a session like every other, it is shown in the draft's grant list, and revoking it ends access under `SHARE-007`. There is no readable address for a draft that carries no grant, a draft has no general access grant under either value of `SHARE-005` (`SHARE-004`), and a draft cannot be published to the anonymous web (`WEB-001`). No role, policy value, container setting, workspace owner action, automation or extension defeats this. `LIFE-006` decides whether a draft state arises, never whether this clause holds."
        },
        {
          "id": "LIFE-003",
          "title": "Publish.",
          "text": "Publishing takes a location, a general access setting (`SHARE-004`), an optional version comment, an optional schedule and an optional publish-as attribution, shows a preview of what readers will see, and on confirmation makes the page visible in its container's tree, in search and in content queries, and writes one version with origin `publish` (`REV-002`)."
        },
        {
          "id": "LIFE-004",
          "title": "Scheduled publish.",
          "text": "A publish set for a future time leaves the page `scheduled`, shows that time wherever the page appears to its author, and publishes at that time without a further action. Cancelling before it fires returns the page to `draft` and publishes nothing; a schedule that fails to fire leaves the page `scheduled` and reports the failure under `BASE-OPS-004`."
        },
        {
          "id": "LIFE-005",
          "title": "Unpublished changes.",
          "text": "Editing a `live` page changes what readers see only when it is republished: every reader other than the editors sees the published revision, and the page shows that it has unpublished changes to anyone who may edit it. Discarding them restores the published revision as the working revision and deletes no version."
        },
        {
          "id": "LIFE-006",
          "title": "Lifecycle profile.",
          "text": "*Policy: `page.lifecycle.v1`; default: `always-live`, following Notion. Under `always-live` a new page is live from creation and the product hides the publish action and its modal, close and discard draft, the draft banner, the draft chip in the tree, the scheduled state, the unpublished-changes state, the version-comment field and the publish-as selector; the page's own share panel (`SHARE-001`) is how it becomes visible to others. Under `draft-then-publish` a new page appears only in its author's tree with a draft chip, nothing is hidden, and `LIFE-003` to `LIFE-005` are the working path. Both values store every state in `LIFE-001`, so a workspace can be switched either way without a migration and an imported draft is never published on arrival.*"
        },
        {
          "id": "LIFE-007",
          "title": "Discarding a draft.",
          "text": "The author of a `draft` page can discard it, which moves it to trash (`ARCH-005`) rather than deleting it, and can close it, which leaves it a draft. Neither action notifies anyone or leaves a trace in another member's tree."
        },
        {
          "id": "SHARE-001",
          "title": "Grants.",
          "text": "A page carries an ordered list of grants. Each grant names a principal — a person, a group, a container or the workspace — a level from `full`, `edit`, `comment` and `view`, and an optional expiry. The panel that edits them shows every effective grant, whether it is inherited or set here, and where an inherited one comes from."
        },
        {
          "id": "SHARE-002",
          "title": "What the levels mean.",
          "text": "`full` may read, edit, comment, share and change access; `edit` may read, edit and comment; `comment` may read and comment; `view` may read only. Every server operation checks the level for the acting member (`BASE-ACCESS-004`), and no interface offers an action the level does not carry."
        },
        {
          "id": "SHARE-003",
          "title": "Inheritance.",
          "text": "A page inherits its parent's grants, and a child added later inherits the parent's grants at that moment. A page may add grants or be restricted, and a restriction is shown on the page and on every descendant it affects. Removing a restriction restores inheritance rather than copying the parent's grants."
        },
        {
          "id": "SHARE-004",
          "title": "General access.",
          "text": "At most one grant on a page has the container or the workspace as its principal; that is the general access grant. A page that carries none is restricted to its named grants, and a page in a personal container (`CONT-005`) carries none under either value of `SHARE-005`. Changing it is one action and is recorded in activity."
        },
        {
          "id": "SHARE-005",
          "title": "Access profile.",
          "text": "*Policy: `page.access.v1`; default: `workspace-general` with all four levels, following Notion. `workspace-general` makes the workspace the general principal and shows the four-level ladder on each grant. `container-general` makes the container the general principal, labels that row with the container's name, and hides nothing else. Removing a level from the list hides it everywhere, including the comment affordance when `comment` is removed. A page imported with a container-wide general grant is created with a container principal under either value, never widened to the workspace. Switching the value relabels the general access row and rewrites no grant: no value widens an existing page's general principal, and no page gains a general grant it did not already carry.*"
        },
        {
          "id": "SHARE-006",
          "title": "Sharing with a person who is not a member.",
          "text": "Granting access to an address that belongs to no member creates a guest invitation (`WORK-010`, `MAIL-001`) and the grant takes effect when it is accepted. Until then the page shows the pending grant, and revoking it cancels the invitation."
        },
        {
          "id": "SHARE-007",
          "title": "Access changes take effect at once.",
          "text": "Removing or narrowing a grant applies to the next request from every existing session, closes an open editor with an explanation rather than silently discarding the writer's unsaved text, and removes the page from that member's search results, tree, inbox items and AI answers within 5 seconds."
        },
        {
          "id": "SHARE-008",
          "title": "Expiring grants.",
          "text": "A grant may carry an expiry time. When it passes, access ends under `SHARE-007`, the grant is shown as expired rather than deleted, and re-granting is one action."
        },
        {
          "id": "WEB-001",
          "title": "Anonymous access takes two acts.",
          "text": "A page is readable without signing in only when a member with `full` access on it publishes it (`WEB-002`) and only while its container allows it. Each of the container's three switches (`CONT-007`) vetoes its own act and no other. While public links are forbidden, publishing a page in that container is rejected and every existing public link on its pages stops resolving at once. While anonymous access is forbidden, a published page in that container resolves only for a signed-in member who may read it. While guests are forbidden, a guest invitation to that container or to a page in it is rejected and existing guest access to it ends under `SHARE-007`. Each veto is not gated by a plan or a role beyond container administration, is not a policy value, and no extension, automation or AI action may bypass it."
        },
        {
          "id": "WEB-002",
          "title": "What a published page carries.",
          "text": "Publishing writes a public exposure record holding the host, a slug the publisher may edit, a search-engine indexing flag, a theme, the set of header controls the page shows, whether readers may duplicate it, and the time it was revoked if it was. The slug is stored, never derived, so an imported public address is preserved exactly; a slug already in use is rejected."
        },
        {
          "id": "WEB-003",
          "title": "The anonymous read view.",
          "text": "A published page renders its published revision and nothing else: no navigation, no editing, no comments, no member names beyond the byline the page itself shows, and no other page unless that page is itself published. It sets no cookie, loads nothing from a third-party domain, and sends no visitor data before an interaction (`BASE-PUBLIC-001`), and it counts a view without identifying the viewer (`PAGE-013`)."
        },
        {
          "id": "WEB-004",
          "title": "Children are not published by inheritance.",
          "text": "Publishing one page publishes only that page unless the publisher explicitly includes its children, and a child added later is not published until someone publishes it. A link from a published page to an unpublished one is rendered as text, not as a link that would return a sign-in page."
        },
        {
          "id": "WEB-005",
          "title": "Indexing is off until it is chosen.",
          "text": "Search-engine indexing is off for a newly published page: the response and the served document tell crawlers not to index it until a member turns indexing on, and turning it off again restores that state within one request."
        },
        {
          "id": "WEB-006",
          "title": "Unpublish.",
          "text": "Unpublishing stops the address resolving within 5 seconds, returns a content-free page saying the link is no longer available, and keeps the slug reserved for that page so republishing restores the same address. It deletes no version and changes no grant."
        },
        {
          "id": "WEB-007",
          "title": "Public-link profile.",
          "text": "*Policy: `page.public-access.v1`; default: `site`, following Notion. `site` shows the host and slug fields, the theme, the header controls, the duplicate-as-template switch, social preview settings and the live-site banner over the page. `link` hides all of those and shows a single toggle, the resulting URL, the indexing switch and unpublish. Both values store the whole record in `WEB-002`, so a workspace switched to `link` keeps the slug and theme an imported or previously published page already had.*"
        },
        {
          "id": "REV-001",
          "title": "What a revision is.",
          "text": "Every page, record, container, comment, template and decision record (`DEC-001`) carries a revision identifier that changes on every committed edit and is never reused. A page revision is immutable: it holds the body and properties as committed, its author, its time, its origin of `autosave` or `publish`, and a content hash, being SHA-256 of the revision's extracted text after Unicode NFC normalisation and whitespace collapse. A retried write whose hash matches the latest revision commits nothing. Wherever another clause names a revision, it means this identifier."
        },
        {
          "id": "REV-002",
          "title": "History.",
          "text": "Each of those records has a history listing its revisions newest first with the actor, the time, the origin, and, where `REV-007` assigns them, an ordinal and a label. History is readable by anyone who may read the record and is never editable."
        },
        {
          "id": "REV-003",
          "title": "Restore.",
          "text": "A member with edit access can restore a previous revision. Restoring writes a new revision holding the earlier content, records who restored which revision in activity, and changes no link, comment anchor, child page, grant or public link."
        },
        {
          "id": "REV-004",
          "title": "Compare.",
          "text": "Any two revisions of one page can be compared, showing added, removed and changed blocks in place. Compare is available under every value of `REV-007`."
        },
        {
          "id": "REV-005",
          "title": "A citation resolves to its revision.",
          "text": "A link, mention or AI citation that names a revision opens that revision with its content as committed, even after the page has changed, and says that a newer revision exists. The reader's access is checked against the page as it is now: a member who may not read the page today gets `LINK-004` and no historical content, an anonymous reader gets nothing unless the page is published (`WEB-001`), and no grant that has since been removed or expired reopens a revision. When the page has been erased it opens a content-free tombstone (`LINK-002`) carrying no body, quote or personal metadata."
        },
        {
          "id": "REV-006",
          "title": "Deleting a version.",
          "text": "A member with `full` access can delete one intermediate version, which removes its content and leaves a numbered tombstone in history naming who deleted it and when. The current revision and the published revision cannot be deleted, and deletion is offered only where `REV-007` says so."
        },
        {
          "id": "REV-007",
          "title": "Version profile.",
          "text": "*Policy: `page.version.v1`; default: `autosnapshot`, following Notion. `autosnapshot` writes a revision on save, assigns no ordinal or label, and hides the version and comment columns, the version-comment field and version deletion, keeping selection, compare, restore and difference summaries. `labelled` numbers each published revision, carries the version comment taken at publish and the page's status at that moment, and hides nothing. Ordinals come from publish events, so a workspace with `page.lifecycle.v1` at `always-live` and this policy at `labelled` numbers nothing until a page is published; the model stores the ordinal and label fields under both values, so an imported numbered history is never discarded.*"
        },
        {
          "id": "STAT-001",
          "title": "The status vocabulary.",
          "text": "The workspace holds a list of status entries, each with a key, a label, a colour and an optional verification requirement. It ships seeded with Rough draft, In progress, Ready for review and Verified under every value of `STAT-005`, and a workspace owner can add, rename, recolour and retire entries. Retiring an entry keeps it on the pages that carry it and offers a bulk change (`CONT-006`)."
        },
        {
          "id": "STAT-002",
          "title": "A page's status.",
          "text": "A page carries at most one status. Setting or clearing it is one action for anyone with edit access, writes one activity entry, and makes the page findable by that status in search (`SEARCH-003`) and in content queries (`BLOCK-010`)."
        },
        {
          "id": "STAT-003",
          "title": "Verification.",
          "text": "Setting a status entry that requires verification records the verifying member as its owner and an expiry of 7, 30 or 90 days, a chosen date, or none, and can be set only by a member who has opened the exact revision they are attesting to. The badge shows the owner, the expiry and the revision verified, and editing the page after verification shows that the verified revision is not the current one."
        },
        {
          "id": "STAT-004",
          "title": "Verification lapses.",
          "text": "When an expiry passes, the page shows the status as lapsed rather than verified, notifies the owner once (`NOTIF-001`), and keeps the original verification in history. Nothing renews it automatically, no AI action renews it (`AI-011`), and a lapsed verification never reads as current in search results, content queries, AI answers or exports."
        },
        {
          "id": "STAT-005",
          "title": "Status profile.",
          "text": "*Policy: `page.status.v1`; default: `verification-only`, following Notion. `verification-only` exposes only the entries that require verification, hides the colour picker and the custom-status field, offers the control from the title's own hover row, and renders the chip beside the breadcrumb title. `open-vocabulary` exposes every entry, shows the colour picker and a custom label of at most twenty characters, and renders the chip on the byline between the author and the reading time. Both values store the whole vocabulary, so switching exposes or hides entries and rewrites no page.*"
        },
        {
          "id": "COMM-001",
          "title": "Two kinds of thread.",
          "text": "A member with `comment` access or more can open an inline thread anchored to a selection in a block, and a page thread about the page as a whole. A thread is chronological, is Open or Resolved, and records each comment's author and time. Both kinds are threads; only their anchor differs."
        },
        {
          "id": "COMM-002",
          "title": "Editing and deletion.",
          "text": "A member can edit or delete only their own comments. An edited comment shows the time it was last edited, a deleted comment leaves a tombstone in place, and both write an activity entry."
        },
        {
          "id": "COMM-003",
          "title": "Mentions.",
          "text": "Mentioning a member in a comment creates one unread notification (`NOTIF-001`) linked to that comment, and mentions no one who cannot read the page. Editing the comment notifies only a member the comment did not already mention."
        },
        {
          "id": "COMM-004",
          "title": "Anchors survive editing.",
          "text": "An inline thread follows its text through edits, splits and moves within the page. When its text is deleted the thread becomes orphaned rather than lost: it stays in the discussions panel, names the revision it was anchored in, and opens that revision (`REV-005`)."
        },
        {
          "id": "COMM-005",
          "title": "Reactions.",
          "text": "A member can add and remove an emoji reaction on any comment they can read. Reactions show who reacted and never notify."
        },
        {
          "id": "COMM-006",
          "title": "Discussions panel.",
          "text": "One panel lists every thread on the page, filtered by kind, status and person, with an action that resolves all open threads. It shows unresolved threads whose anchor is orphaned, and selecting a thread scrolls to its anchor."
        },
        {
          "id": "COMM-007",
          "title": "Resolve.",
          "text": "Any member with `comment` access can resolve or reopen a thread. A resolved thread is hidden from the page body, stays readable in the panel and in search, and keeps its comments and reactions."
        },
        {
          "id": "ARCH-001",
          "title": "Archive.",
          "text": "A page, a container or a database can be archived. Archiving records the actor, the time, the prior location and an optional reason, and archives the record's descendants with it."
        },
        {
          "id": "ARCH-002",
          "title": "What archived means.",
          "text": "An archived record is read-only, is absent from the tree, from default search (`SEARCH-009`), from content queries and from AI grounding, and its existing links, mentions and citations still resolve. It is discoverable by the archived filter, and its comments, versions and grants are unchanged."
        },
        {
          "id": "ARCH-003",
          "title": "Restore from archive.",
          "text": "Restoring returns a record to its prior parent and position with the same identifiers, revisions, comments, grants and status. Where the prior parent is gone, the member chooses a destination before the restore completes."
        },
        {
          "id": "ARCH-004",
          "title": "Archive is not deletion.",
          "text": "Archiving deletes no data. Permanent deletion is a separate confirmed action governed by `BASE-DATA-002`, `BASE-DATA-003` and `ARCH-006`."
        },
        {
          "id": "ARCH-005",
          "title": "Trash.",
          "text": "Deleting a page moves it and its descendants to the trash, where they are listed with the deleting member, the time and the prior location, searchable and filterable within the trash, and restorable to their prior place. Nothing leaves the trash on a timer (`BASE-DATA-002`), and a page in the trash is absent from every surface but the trash itself."
        },
        {
          "id": "ARCH-006",
          "title": "Permanent deletion.",
          "text": "Emptying the trash, or permanently deleting one record in it, is explicit and confirmed, names what will be destroyed, and removes the body, every revision, uploaded files, comments, analytics rows and AI grounding within the cascade deadline in `BASE-DATA-003`. Every link to it then resolves to a content-free tombstone (`LINK-002`), and activity entries survive with their references anonymised."
        },
        {
          "id": "CONT-001",
          "title": "What a container is.",
          "text": "A container has an identifier, an optional key, an icon, a name, a description, owners, a visibility of `default`, `open`, `closed`, `private` or `personal`, a grant list shaped like a page's, and external-access switches (`CONT-007`). It holds one page tree (`TREE-001`) and the databases in that tree."
        },
        {
          "id": "CONT-002",
          "title": "Creating a container.",
          "text": "A workspace owner or, where the workspace allows it, any member can create a container from a form taking an icon, a name, a description, a visibility and an optional key, and a purpose that sets the form's defaults. A purpose supplies default values only; it creates no second kind of container and nothing about the container is unreachable afterwards."
        },
        {
          "id": "CONT-003",
          "title": "The container index.",
          "text": "One index lists every container the member may see with its name, description, access and member count, filtered by membership, visibility and whether it is personal, and opens each one. Containers a member cannot see are neither listed nor counted."
        },
        {
          "id": "CONT-004",
          "title": "Container settings.",
          "text": "Each container has its own settings shell holding its identity, members and grants, external-access switches, templates, trash and bulk tools. It is separate from workspace settings (`WORK-011`) and a container administrator reaches only their own container's."
        },
        {
          "id": "CONT-005",
          "title": "Personal containers.",
          "text": "Every member has one personal container that no other member can be granted access to; its pages are theirs alone until they are moved or shared to another container. Deleting the member archives it under `WORK-007` rather than exposing it."
        },
        {
          "id": "CONT-006",
          "title": "Bulk actions.",
          "text": "A member can select several pages, in a tree or in a container's content list, and archive, trash, move, change owner or change status for all of them in one action. Each selected page records its own change with its own activity entry, each is checked against the acting member's access, and a partial failure names the pages that failed and applies the rest."
        },
        {
          "id": "CONT-007",
          "title": "External-access switches.",
          "text": "A container carries three switches — guests, public links and anonymous access — each `allowed` or `forbidden`. A workspace owner sets a workspace-wide value and a container administrator may only narrow it. Forbidding takes effect at once under `WEB-001` and `SHARE-007`. These switches carry no plan, tier or upgrade gate."
        },
        {
          "id": "CONT-008",
          "title": "Container access profile.",
          "text": "*Policy: `container.access.v1`; default: `class-led`, following Notion. `class-led` leads with the visibility values in `CONT-001`, each stating who may see and join, and folds the member, group and guest lists behind one manage-access view. `grant-led` leads with tabbed lists of members, groups and guests and an external-access card, and hides the visibility dropdown. Neither value hides the switches in `CONT-007`. Both store visibility and the grant list, so switching relabels the surface and rewrites no grant.*"
        },
        {
          "id": "CONT-009",
          "title": "Container grants.",
          "text": "A container's grants use the principals and levels of `SHARE-001` and `SHARE-002`, are the source of every page's inherited access, and are editable only by a container administrator or a workspace owner. Every change is recorded in activity and takes effect under `SHARE-007`."
        },
        {
          "id": "DB-001",
          "title": "A database.",
          "text": "A database is a node in the page tree with a name, an icon, typed fields and records. It is created, moved, archived and trashed like a page, and its access follows `SHARE-001`."
        },
        {
          "id": "DB-002",
          "title": "Field types.",
          "text": "A field is one of: text, number, checkbox, date, select, multi-select, status, person, file, URL, email, phone, place, relation to another database, rollup over a relation, formula, created and updated metadata, and identifier. Each has a defined empty value, a defined sort order and a defined import and export representation, and changing a field's type either converts every value or is rejected naming what would be lost. Rollup and formula are stored, imported, exported and displayed in 1.0 and are not evaluated: an imported rollup or formula field keeps its configuration and the values the export carried, shows on the field that it is not being recomputed, and is never presented as current. Evaluating them is a Notion-edition contribution on `database.field-type.v1`. Relation ships evaluated, because both incumbents have it and Confluence's Page, Jira and Other-Database fields land on it at import; neither incumbent's evidenced field list has a rollup or a formula."
        },
        {
          "id": "DB-003",
          "title": "Views.",
          "text": "A database has several named views over one set of records. A view is saved for everyone who can see the database, is renamed, duplicated and deleted by anyone with edit access, and never changes the records it shows."
        },
        {
          "id": "DB-004",
          "title": "Layouts.",
          "text": "A view is a table, a board, a list, a gallery, a calendar or a timeline. A board or calendar requires the field it groups or dates by, and offers to create it rather than failing when it is missing."
        },
        {
          "id": "DB-005",
          "title": "Filter and sort.",
          "text": "A view carries filter rules over any field, combined with and or or, and an ordered list of sorts. Both are stored on the view, are shown in force above the records, and are cleared in one action."
        },
        {
          "id": "DB-006",
          "title": "Group.",
          "text": "A view groups its records by a select, status, person, checkbox or date field, shows the count in each group, allows a record to be moved between groups where the field permits it, and hides empty groups on request."
        },
        {
          "id": "DB-007",
          "title": "Calculations.",
          "text": "Each column in a table view carries a calculation under it — count, filled, empty, sum, average, minimum, maximum, range or percent — computed over the view's filtered records and updated within 2 seconds of a change."
        },
        {
          "id": "DB-008",
          "title": "Charts.",
          "text": "A chart is built over one view, showing counts or a numeric field aggregated by a grouping field, and can be embedded in a page (`DB-011`). It reads the same filtered records the view shows and states which view it reads."
        },
        {
          "id": "DB-009",
          "title": "Lock structure.",
          "text": "A database's structure can be locked, which rejects field, view, filter, sort and layout changes while allowing record edits, names who locked it, and is unlocked by anyone with `full` access."
        },
        {
          "id": "DB-010",
          "title": "A record is a page.",
          "text": "Every record has an identifier that is also a page identifier, a field map, an optional body of blocks, an optional icon and an optional position in the tree. *Policy: `record.page-parity.v1`; default: `record-is-page`, following Notion. `record-is-page` shows the per-record icon, opens a record as a side peek, a centre peek or a full page, offers its body, and lists it in the tree and in page search. `record-is-row` opens a record as a field list with a print action, and hides the icon, the body area, the tree position and the record from page search. The body is stored under both values and is never dropped, so a workspace can be switched back and an imported record with a body keeps it.*"
        },
        {
          "id": "DB-011",
          "title": "A view inside a page.",
          "text": "A view can be embedded in a page body and stays live: it shows each reader only the records that reader may read, reflects a change to the source within 5 seconds, and carries its own filters and sorts without changing the source view."
        },
        {
          "id": "DB-012",
          "title": "Record detail.",
          "text": "Opening a record shows its fields in a defined order with inline editing, its comments and its history (`REV-002`), and, where `DB-010` exposes it, its body. A field a member may not edit is shown and not editable rather than hidden."
        },
        {
          "id": "AUTO-001",
          "title": "A rule.",
          "text": "A rule has a name, a scope, a trigger, ordered conditions, an optional branch over related records, ordered actions, and an actor. Triggers are record and page events and a schedule; actions edit fields, create pages and records, move and archive them, add comments, and send a notification through a configured adapter."
        },
        {
          "id": "AUTO-002",
          "title": "A rule acts as its actor.",
          "text": "A rule performs only what its actor could perform by hand: every action is checked against that member's access at the moment it runs, and an action that would exceed it is skipped and logged rather than performed. A rule cannot publish or unpublish a page, change grants or external-access switches, verify a page, permanently delete anything, or manage members."
        },
        {
          "id": "AUTO-003",
          "title": "Run log.",
          "text": "Every rule keeps a per-run log with the trigger, the actor, the records touched, the time and one of the states configuration-changed, queued, running, success, no-action, condition-not-met, throttled, error, failure and disabled. A failed run is visible without a search (`BASE-OPS-004`), and a rule that fails repeatedly is disabled and says so."
        },
        {
          "id": "AUTO-004",
          "title": "Loops are bounded.",
          "text": "A rule never triggers itself, a chain of rules stops at the depth in `seed.json → limits`, and a run that would exceed the workspace's rate limit is throttled and logged rather than dropped silently."
        },
        {
          "id": "AUTO-005",
          "title": "Automation profile.",
          "text": "*Policy: `automation.scope.v1`; default: `source`, following Notion. `source` creates rules from a database's own toolbar, scopes them to that database, shows one When-and-Do form, scopes the run log to that database, and hides the scope selector, the site option, the branch rail and the cross-container rule list. `container` and `site` create rules from a rule list, expose the scope selector and the branch rail, and hide nothing from each other. The scope field and the run log are stored under every value, so an imported container or site rule keeps its scope and its history.*"
        },
        {
          "id": "SEARCH-001",
          "title": "Global search.",
          "text": "One search covers page titles and retained body text, record field values, container names and descriptions, comments and templates, and returns the first page of ranked results within 500ms with the total number of matches and an ordering stable across pages."
        },
        {
          "id": "SEARCH-002",
          "title": "Results.",
          "text": "Each result names its kind, shows the matched text in context, and opens the record with the match highlighted. A quoted query matches only records whose retained text contains that exact phrase."
        },
        {
          "id": "SEARCH-003",
          "title": "Filters.",
          "text": "Results are narrowed by kind, container, contributor, status, updated date, and whether archived or trashed records are included, without changing the query text, and the filters in force are shown with the results."
        },
        {
          "id": "SEARCH-004",
          "title": "Freshness.",
          "text": "A committed create, edit, publish, move, archive, restore or delete is reflected in results within 5 seconds. An import is not complete until its records are searchable (`PORT-004`), and content removed under `ARCH-006` stops matching within the cascade deadline in `BASE-DATA-003`."
        },
        {
          "id": "SEARCH-005",
          "title": "What search never shows.",
          "text": "Search returns only what the searching member may read: no page they lack a grant for, no other member's draft (`LIFE-002`), nothing in another member's personal container (`CONT-005`), and no title, snippet, facet count or result total that would reveal the existence of any of them. Access is the only rule that removes a record from search outright; every other exclusion is a default the member can undo (`SEARCH-009`)."
        },
        {
          "id": "SEARCH-006",
          "title": "Search in one container.",
          "text": "Search can be scoped to one container from that container's own surface, keeps every filter in `SEARCH-003`, and says which container it is scoped to."
        },
        {
          "id": "SEARCH-007",
          "title": "No results.",
          "text": "When nothing matches, search restates the query and every applied filter, offers one action that clears the filters and one that creates a page with that title, and shows no invented result or answer."
        },
        {
          "id": "SEARCH-008",
          "title": "Search without AI.",
          "text": "With the AI provider unconfigured, disabled, failing or over budget, ranked search and every filter still return within 500ms with no error, and the generated answer (`AI-015`) is absent rather than empty."
        },
        {
          "id": "SEARCH-009",
          "title": "Default visibility.",
          "text": "Search without the archived or trashed filter (`SEARCH-003`) excludes archived and trashed records and nothing else. This is the only rule that hides a record the searching member may read, and every record it hides stays reachable by applying that filter and through every existing link, mention and citation (`LINK-001`, `LINK-003`)."
        },
        {
          "id": "NAV-001",
          "title": "Home.",
          "text": "A member's home shows their recent pages, their favourites and the containers they belong to, each entry opening the record. It shows nothing the member may not read and no workspace-wide activity feed."
        },
        {
          "id": "NAV-002",
          "title": "Recents.",
          "text": "Recently opened pages are listed per member, newest first and grouped by day, and a page appears there for no one but the member who opened it."
        },
        {
          "id": "NAV-003",
          "title": "Favourites.",
          "text": "Favourited pages and containers (`PAGE-008`) are listed in the navigation, reorderable by the member, and removed from the list in one action."
        },
        {
          "id": "NAV-004",
          "title": "One navigation.",
          "text": "The operator app has one sidebar holding the global destinations, the personal sections and the container trees, collapsible to leave the page full width. Its order and which entries appear come from workspace configuration and from the navigation slot (`workspace.navigation.after.v1`); changing any policy value in this contract changes no entry in it and no entry's position. A container's own navigation nests inside it rather than replacing it."
        },
        {
          "id": "NAV-005",
          "title": "Global create.",
          "text": "One create action is available from every operator screen and creates a page, a database, a container or a page from a template, naming the destination before it creates."
        },
        {
          "id": "NAV-006",
          "title": "The touch client edits.",
          "text": "Every operator surface in this contract works at phone width on a touch device, and none of them is read-only there: a member can create and edit a page and its blocks, comment, resolve, publish, share, and use every database layout. Where a gesture cannot carry a desktop interaction, the touch form is named in the same clause — the insert menu from a labelled control above the keyboard (`BLOCK-003`), reordering by a drag handle rather than a hover target (`BLOCK-001`). Slate is one responsive application; a native mobile application is a non-goal."
        },
        {
          "id": "NOTIF-001",
          "title": "What notifies.",
          "text": "A member is notified in the app when they are mentioned, when someone comments or replies on a page they watch or authored, when a page they watch is published or republished, when a verification they own lapses (`STAT-004`), when they are granted access to a page or container, and when an AI run they started needs review or failed. Their own action never notifies them, and nothing notifies anyone about a record they may not read."
        },
        {
          "id": "NOTIF-002",
          "title": "Inbox.",
          "text": "Notifications are listed newest first, grouped by period, with an unread count and separate views for those addressed to the member and those from watched pages. Opening one marks it read and opens the referenced record at the referenced comment; a member can mark all read and archive read notifications."
        },
        {
          "id": "NOTIF-003",
          "title": "Chat notifications.",
          "text": "A workspace owner can connect one chat destination through `chat.notifier.v1` and choose which events reach it. A message carries the event, the actor, the page title and a link, never the page body, and the link still requires access. The connection shows its last delivery and its last failure, retries only transient transport failures, and stops sending the moment it is disconnected; its credential is encrypted with `CONNECTOR_CREDENTIALS_KEY`, returned by no API response, and destroyed on disconnect. A deployment with a chat destination connected and that variable missing fails at startup naming it (`BASE-SECRET-002`) rather than running with the connection off. An expired or revoked authorisation marks the connection as needing attention, offers a reconnect action, and stops sending without deleting the connection, its event selection or its delivery history."
        },
        {
          "id": "MAIL-001",
          "title": "Outbound only, two kinds.",
          "text": "Slate sends mail only to deliver a sign-in link (`BASE-ACCESS-001`) and an invitation — a member invitation (`WORK-006`) or a guest invitation (`SHARE-006`) — from the configured sender address through `mail.sender.v1`. No product event sends mail, and Slate accepts no inbound mail: a reply to that address creates and changes nothing."
        },
        {
          "id": "MAIL-002",
          "title": "Delivery failure.",
          "text": "A send that fails is retried up to 3 times over 5 minutes. A send that still fails is recorded with the provider's error and shown to a person — on the sign-in screen for a sign-in link, in settings for an invitation — and is never reported as sent."
        },
        {
          "id": "ACT-001",
          "title": "Durable timeline.",
          "text": "Activity is append-only. Each entry records the actor or the feature that acted, the action, the target record, the time, and the operation key, automation run or AI run that caused it. A mutation and its activity entry commit together or not at all."
        },
        {
          "id": "ACT-002",
          "title": "Coverage.",
          "text": "Activity records page creation, body and property changes, publish and unpublish, moves, archive, trash, restore and permanent deletion, version restores and deletions, status and verification changes, grant and external-access changes, comment activity, container and group changes, database structure changes, automation runs, AI runs and their acceptance or rejection, member invitation and removal, import and export."
        },
        {
          "id": "ACT-003",
          "title": "Retrieval.",
          "text": "Members can filter activity by actor, action, record kind, container and date range, with newest-first pagination that stays stable while new entries arrive, and see only entries about records they may read."
        },
        {
          "id": "ACT-004",
          "title": "Context.",
          "text": "Opening an activity entry opens the record it describes together with the comment thread, revision or grant it references, and changes no entry."
        },
        {
          "id": "ACT-005",
          "title": "Retained explanation.",
          "text": "No member can edit or delete an activity entry. When a described record is erased the entry remains, naming the record kind and time, with personal references anonymised as `BASE-DATA-003` requires."
        },
        {
          "id": "TMPL-001",
          "title": "Gallery.",
          "text": "Members can search templates by title and description and filter them by category. A search with no match shows \"No templates match this search.\""
        },
        {
          "id": "TMPL-002",
          "title": "Preview.",
          "text": "A template preview shows the page it produces, including its body, properties and any child pages, before it is used. A figure such as a team size or a duration appears only when the template's author wrote one; Slate never estimates, infers, or displays a placeholder for either."
        },
        {
          "id": "TMPL-003",
          "title": "Snapshot on use.",
          "text": "Using a template creates a page holding a copy of its body, properties and children in the destination the member chose. Editing, archiving or deleting the template afterwards changes no page already created from it."
        },
        {
          "id": "TMPL-004",
          "title": "Custom templates.",
          "text": "Members can create, edit, archive, restore and delete templates. Templates shipped with Slate cannot be edited or deleted and can be duplicated into an editable copy."
        },
        {
          "id": "TMPL-005",
          "title": "Save a page as a template.",
          "text": "Saving a page as a template copies its body, its properties and, on request, its children. It copies no comment, version, grant, watcher, analytics figure, status or public link."
        },
        {
          "id": "TMPL-006",
          "title": "Where a template lives.",
          "text": "A template belongs to the workspace or to one container. A container's templates are offered first inside that container and are not offered elsewhere, and moving a template between the two is one action."
        },
        {
          "id": "IMP-001",
          "title": "Import jobs.",
          "text": "An import shows Queued, Running, Completed or Failed with counts of the records created, and produces a report naming every page, database, record, file and version created and every construct it did not map. It is idempotent by operation key (`SAVE-005`), reaches Completed only under `PORT-004`, and a failed import leaves no partially created tree: it is rolled back and can be retried. The source archive is retained with the job until a member deletes it, so nothing that failed to map is unrecoverable."
        },
        {
          "id": "IMP-002",
          "title": "Notion import.",
          "text": "Slate imports a Notion workspace export in its Markdown and CSV form: pages become pages with their tree and titles, icons and covers where the export carries them, page bodies become blocks of the equivalent types, links between exported pages resolve to the imported pages, CSV databases become databases with typed fields and records, and files become media blocks. Every page arrives live, and the report names what did not map. Its test fixture is an export produced by Notion itself, committed with the test; a fixture written by hand does not satisfy this clause, because the construct-by-construct mapping is only tested against what the incumbent actually emits."
        },
        {
          "id": "IMP-003",
          "title": "Confluence import.",
          "text": "Slate imports a Confluence space export in whichever form the product emits — Confluence Cloud's CSV table export, where `content.csv` carries the tree and the version ledger and `bodycontent.csv` carries storage-format bodies, or the single `entities.xml` a Server or Data Center export writes — and single documents in the forms each editor exports: Word's `.docx`, and a Google Docs document downloaded as `.docx` or as zipped HTML, Google Docs having no file form of its own. Pages become pages with their tree, their labels (`PAGE-001`), their versions with their comments and ordinals where the export carries them, their status, and their published state; page and attachment links resolve to the imported records; and named user and group restrictions remain named grants. A general grant is container-scoped, never workspace-wide (`SHARE-005`), and a personal container carries no general grant (`SHARE-004`). The report names what did not map, including whiteboards, blogs, calendars and folders, and a folder is imported as a page with an empty body so its tree survives. Its test fixtures are a space export produced by Confluence itself and one document downloaded from each of Word and Google Docs, committed with the test; a fixture written by hand does not satisfy this clause."
        },
        {
          "id": "IMP-004",
          "title": "Import safety.",
          "text": "Imported markup, documents and attachments are validated and sanitised under `BASE-INPUT-001` to `BASE-INPUT-003`. An import fetches nothing from the network: media comes from the archive, and a link to a file the archive does not contain is imported as a link and named in the report."
        },
        {
          "id": "IMP-005",
          "title": "Importing into a page.",
          "text": "A member can import a Markdown, HTML, Word, PDF, EPUB, plain text or CSV document into a page or as a set of pages, mapping its headings to blocks and its tables to tables. The same report and the same safety rules apply."
        },
        {
          "id": "PORT-001",
          "title": "Export contents.",
          "text": "An export contains workspace settings; members, groups and guests; every container, page, database, record, template, comment thread, notification, automation rule, AI run and activity entry as JSON, with each record's revision history and grants; and every uploaded file in its original form."
        },
        {
          "id": "PORT-002",
          "title": "Referential restore.",
          "text": "Importing an archive into an empty deployment restores identifiers, revisions, tree order, grants, links, mentions, comment anchors, public-link slugs and files, so every link from the source deployment resolves to the same record."
        },
        {
          "id": "PORT-003",
          "title": "Visible jobs.",
          "text": "Export and import show Queued, Running, Completed or Failed with record counts and, on failure, a report naming what failed. A failed job is never shown as complete."
        },
        {
          "id": "PORT-004",
          "title": "Search rebuild.",
          "text": "An import is not Completed until search has been rebuilt over the imported records and they satisfy `SEARCH-001` to `SEARCH-006`."
        },
        {
          "id": "PORT-005",
          "title": "Access boundary.",
          "text": "An export excludes sessions, sign-in links, pending invitations, stored operation-key responses and provider secrets. An import activates only the member named by `SETUP_OWNER_EMAIL`; every other imported member is an inactive historical actor until invited, and no imported page is published to the anonymous web until a member publishes it."
        },
        {
          "id": "PORT-006",
          "title": "Atomic import.",
          "text": "An import stages records and files outside the live namespace, validates the whole archive, and publishes it in one step. A failed import leaves the deployment empty and can be retried."
        },
        {
          "id": "PORT-007",
          "title": "Exporting a page or a container.",
          "text": "A member can export one page, a page with its descendants, or a whole container as Markdown, HTML or PDF, with records as CSV, in one archive with its media. The export is readable without Slate, and re-importing it under `IMP-005` reproduces the tree."
        },
        {
          "id": "LINK-001",
          "title": "Permanent URLs.",
          "text": "Every page, page revision, container, database, record, comment thread and template has a permanent URL. Renaming a record, moving it, changing its container, archiving it or restoring it never changes that URL, and every existing link, mention, citation, content query and search result continues to resolve to it."
        },
        {
          "id": "LINK-002",
          "title": "Tombstones.",
          "text": "A link to a deleted or erased record resolves to a tombstone naming the record kind and the time it was deleted, not an error page or an empty view, and exposes no erased content or personal metadata."
        },
        {
          "id": "LINK-003",
          "title": "References follow their target.",
          "text": "A mention or link to another record renders that record's current title and state, follows renames and moves, and is labelled archived, trashed or deleted rather than removed when its target is."
        },
        {
          "id": "LINK-004",
          "title": "Links respect access.",
          "text": "A mention or link to a record the reader may not open shows that a linked record exists and that access is needed, and shows no title, snippet, icon or author. Requesting access is one action that notifies a member who can grant it."
        },
        {
          "id": "AI-001",
          "title": "Enablement.",
          "text": "AI is off until an AI provider is configured (`ai.provider.v1`) and a workspace owner turns AI on in settings, which is recorded in activity. While AI is off no run can be started and no request reaches a provider."
        },
        {
          "id": "AI-002",
          "title": "Run lifecycle.",
          "text": "A member can start a selection edit, a page or comment summary, a difference summary, a workspace answer or an agent run. A run moves Queued → Running → Needs review → Completed. A member can cancel a Queued or Running run; a run in any non-terminal state becomes Failed on error, and a run that has been Running for 10 minutes becomes Failed; Completed, Cancelled and Failed are terminal. A run reaches Completed when a member accepts or discards its output, and discarding records the rejection."
        },
        {
          "id": "AI-003",
          "title": "Review before mutation.",
          "text": "A run produces a proposal. Until a member accepts it, no page, block, record, comment, status, grant, container or state changes."
        },
        {
          "id": "AI-004",
          "title": "Answer provenance.",
          "text": "Every generated answer, summary and agent output names the pages and revisions it drew on, and each citation opens that revision (`REV-005`). A statement Slate cannot cite is labelled as unsourced and is never presented as drawn from the workspace."
        },
        {
          "id": "AI-005",
          "title": "Acceptance.",
          "text": "Accepting a proposal applies only the changes the member selected and records the accepting member, those changes, the input record revisions, and the provider and model. The mutations and their activity entries commit together or not at all."
        },
        {
          "id": "AI-006",
          "title": "Stale inputs.",
          "text": "A proposal whose input record has changed revision since the run started cannot be accepted until Slate shows the difference and the member either regenerates the run or accepts explicitly against the current revision."
        },
        {
          "id": "AI-007",
          "title": "Untrusted content.",
          "text": "Text inside pages, blocks, comments, imported documents, link cards and uploaded files is treated as content, never as instructions: it cannot change a run's scope, tools, destination records, agent permissions or the member's selections."
        },
        {
          "id": "AI-008",
          "title": "Provider and output failure.",
          "text": "A provider error, a budget refusal (`AI-012`), a cancellation, or output that fails schema validation leaves the run Failed or Cancelled with the reason readable on the run, creates no product record or partial proposal, and leaves every manual action available."
        },
        {
          "id": "AI-009",
          "title": "Retry.",
          "text": "Only transient transport failures are retried, at most 3 attempts over 5 minutes; invalid or unusable output is never retried automatically. A provider response received twice for one run produces one proposal."
        },
        {
          "id": "AI-010",
          "title": "Usage record.",
          "text": "Every run records the provider, model, tokens or provider units, calculated cost, duration, status, and the feature that started it. Totals are shown in settings by day and by feature."
        },
        {
          "id": "AI-011",
          "title": "No autonomous authority.",
          "text": "AI cannot publish or unpublish a page, change a grant or an external-access switch, set or renew a verification, archive, trash or permanently delete anything, empty the trash, invite or remove a member, change a policy value, or send data outside the deployment. Only a member action does these."
        },
        {
          "id": "AI-012",
          "title": "Budget.",
          "text": "Before a run is queued Slate reserves its maximum estimated cost — the configured model's rate for the run's maximum output — against the current calendar month, counting reservations held by unfinished runs. A run that would exceed the month's budget is refused before any provider request, and the unused part of a reservation is released when the run settles. AI cannot be turned on without a monthly budget greater than zero (`AI-001`), and no policy value, extension or agent raises, waives or defers the reservation."
        },
        {
          "id": "AI-013",
          "title": "A generated answer respects the reader.",
          "text": "Every answer, summary, chat reply and agent output is produced from pages the asking member may read at the moment it is produced, and says so on the result. A page whose grant is revoked, or which is archived, trashed or another member's draft, is excluded from the next answer with no re-index step, and no cache, embedding, transcript or stored proposal re-exposes it. This holds under every policy value and cannot be overridden."
        },
        {
          "id": "AI-014",
          "title": "Agents.",
          "text": "An agent is a named record with instructions, a source scope and an owner. It runs under the access of the member who invoked it, never wider (`AI-013`), its writes are proposals under `AI-003`, and its runs appear in the usage record with the agent named. 1.0 carries the agent object; model choice, per-agent budgets and per-destination write levels do not ship."
        },
        {
          "id": "AI-015",
          "title": "Workspace answers.",
          "text": "A chat panel answers questions over the workspace, keeps the conversation, cites its sources (`AI-004`), and places the same generated answer above search results with the same citations and the same caveat. It answers \"I could not find that in what you can read\" rather than answering from the model alone."
        },
        {
          "id": "AI-016",
          "title": "Summaries.",
          "text": "A member can summarise a page, its comment threads, or the difference between two revisions. A summary names the revision it read and is offered as text to insert or copy, never written into the page without `AI-003`."
        },
        {
          "id": "AI-017",
          "title": "Writing with AI.",
          "text": "With text selected, a member can run a named action — improve, shorten, lengthen, proofread, explain, translate, change tone — or a free-form instruction. The result is shown against the selection with insert below, replace, refine and discard, and nothing changes until one is chosen."
        },
        {
          "id": "DEC-001",
          "title": "A decision is a layer over a page.",
          "text": "A page can carry one decision record: a question, the options considered, an outcome, a rationale, an owner, a decided date, an optional review date, and citations to page revisions (`REV-005`). The page stays a page: its body, comments, grants, versions and links are unchanged, and removing the decision record leaves the page intact."
        },
        {
          "id": "DEC-002",
          "title": "States.",
          "text": "A decision is Proposed, Accepted or Superseded. Proposed becomes Accepted, an Accepted decision is superseded by another decision, and Superseded is terminal. Every transition records the actor and the time and appears in activity."
        },
        {
          "id": "DEC-003",
          "title": "An accepted decision is immutable.",
          "text": "Accepting freezes the decision's content together with the revisions it cites. Later edits to a cited page add a warning that the citation is no longer current and never rewrite the accepted rationale; changing an accepted decision is done by superseding it, which links both decisions in both directions."
        },
        {
          "id": "DEC-004",
          "title": "Decision log.",
          "text": "Decisions are listed and filtered by state, container, owner, decided date, review date and whether their citations are current, and each result opens the exact accepted revision."
        },
        {
          "id": "DEC-005",
          "title": "Review dates.",
          "text": "A review date that passes labels the decision overdue and notifies its owner (`NOTIF-001`) until a member records a completed review — a note, an actor, a time and the next review date — or supersedes the decision. An overdue decision stays Accepted."
        },
        {
          "id": "DEC-006",
          "title": "A proposed decision is editable.",
          "text": "While a decision is Proposed, a member with edit access on its page can change its question, options, outcome, rationale, owner, decided date, review date and citations. Each change writes a revision (`REV-001`) keeping the previous content with its actor and time, resubmitting an unchanged proposal writes no revision, and the history is readable under `REV-002`. Accepting the decision ends this, and `DEC-003` governs from then on."
        }
      ],
      "hasReadme": false,
      "hasBaseline": true,
      "nonGoals": [
        "Plan ladders, trials, credits, seat counting, and in-product upgrade paths: one product with every feature on has nothing to gate.",
        "A vendor product shelf, app switcher, or cross-product chrome from a suite this product is not part of.",
        "A commercial marketplace or storefront of templates, apps, agents, or services; the extension surface ships and the shop does not.",
        "Deep coupling to one issue tracker: an issue URL is a link card like any other.",
        "A bundled screen recorder: a recording is a media block.",
        "More than one workspace or tenant per deployment.",
        "Real-time co-editing, shared cursors, presence beyond an avatar stack, and CRDT merge.",
        "A native mobile application: the operator app is one responsive web application, and it edits rather than reads on touch."
      ],
      "externals": [
        {
          "name": "cloudflare-platform",
          "required": true,
          "requiredWhen": "",
          "reason": "The supported deployment target supplies Worker compute, D1 records and search, R2 files and media, and Queues for import, export and automation jobs.",
          "data": [
            "Every page, record, comment, file and activity entry in the deployment",
            "Member email addresses and session identifiers",
            "Anonymous readers' requests to published pages"
          ],
          "adapters": []
        },
        {
          "name": "resend-transactional-email",
          "required": true,
          "requiredWhen": "",
          "reason": "Resend sends magic-link sign-in, member invitations and guest invitations through the mail.sender.v1 adapter; Cloudflare's owned primitives do not send email.",
          "data": [
            "The recipient's email address",
            "The workspace name and the inviting member's display name",
            "A single-use sign-in or invitation URL"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        },
        {
          "name": "ai-inference-provider",
          "required": false,
          "requiredWhen": "AI_ENABLED=true",
          "reason": "Selection editing, summaries, workspace answers and agents require model inference, which no owned primitive provides.",
          "data": [
            "The member's instruction or question",
            "The text of the pages and revisions the run is scoped to, which the asking member may already read",
            "Record identifiers used to cite the answer"
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        },
        {
          "name": "identity-provider",
          "required": false,
          "requiredWhen": "SSO_ENABLED=true",
          "reason": "Sign-in through the buyer's own identity provider cannot be served by the deployment's native session auth, which remains the default.",
          "data": [
            "The signing-in member's email address",
            "The authentication request and its returned assertion"
          ],
          "adapters": [
            "identity.provider.v1"
          ]
        },
        {
          "name": "pasted-url-origin",
          "required": false,
          "requiredWhen": "LINK_PREVIEWS_ENABLED=true and a member pastes a URL",
          "reason": "A link card shows the target's own title, description and icon, which only the target origin can supply; no owned primitive can know them.",
          "data": [
            "The URL a member pasted, sent to that URL's own origin",
            "The deployment's outbound IP address and user agent"
          ],
          "adapters": [
            "link.preview.v1"
          ]
        },
        {
          "name": "chat-notification-destination",
          "required": false,
          "requiredWhen": "a workspace owner connects a chat destination",
          "reason": "Delivering a notification into the buyer's chat tool requires that tool's own API; nothing owned can post into it.",
          "data": [
            "The event name and the actor's display name",
            "The page title and a link to it, never the page body"
          ],
          "adapters": [
            "chat.notifier.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "estimated and not measured; no deployment of this contract's product exists yet",
        "asOf": "2026-09-08",
        "assumptions": "Up to 25 active members, 20000 pages, 200000 blocks, 50000 records, 8 GB of media, moderate search, import and automation use, and AI off.",
        "cloudflareUsdPerMonth": "0 on Workers Free while the deployment stays inside the listed Free entitlements; a workspace at the estimated capacity above is expected to need Workers Paid once published pages or search traffic pass the request and D1 read allowances.",
        "workersFreeEntitlements": "100000 Worker requests/day at up to 10 ms CPU per invocation; D1 5000000 rows read/day, 100000 rows written/day, and 5 GB; Queues 10000 operations/day; up to 5 Cron triggers/account.",
        "workersPaidRequiredAt": "Move to Workers Paid before any declared workload exceeds 100000 Worker requests/day, 10 ms CPU in an invocation, 5000000 D1 reads/day, 100000 D1 writes/day, 5 GB D1 storage, 10000 Queue operations/day, or needs a sixth Cron trigger/account.",
        "r2PaidUsageAt": "R2's Free allowance is separate from Workers Paid: up to 10 GB-month storage, 1000000 Class A operations/month, and 10000000 Class B operations/month. The declared 8 GB media allowance sits inside it; exceeding an R2 allowance creates R2 paid usage, not a requirement to move the Worker to Workers Paid.",
        "emailUsdPerMonth": "Provider and volume dependent; excluded.",
        "aiUsdPerMonth": "Provider, model and accepted run volume dependent; excluded, and bounded in the deployment by the monthly budget AI-012 reserves against and refuses past.",
        "notes": "A bootstrap estimate, not a tested operating limit or a quote. The load test must measure real Worker CPU, D1, Queue, Cron and R2 consumption for a page product with a block editor and a search index before any paid-plan recommendation."
      },
      "extensionPoints": [
        "page.block-type.v1",
        "database.field-type.v1",
        "tree.node-type.v1",
        "container.section.v1",
        "workspace.additional-page.v1",
        "settings.additional-section.v1",
        "workspace.scheduled-job.v1",
        "page.lifecycle.v1",
        "page.access.v1",
        "page.public-access.v1",
        "page.version.v1",
        "page.status.v1",
        "record.page-parity.v1",
        "automation.scope.v1",
        "container.access.v1",
        "page.created.v1",
        "page.published.v1",
        "page.updated.v1",
        "page.moved.v1",
        "page.archived.v1",
        "page.access-changed.v1",
        "comment.created.v1",
        "comment.resolved.v1",
        "record.changed.v1",
        "container.created.v1",
        "member.invited.v1",
        "import.completed.v1",
        "ai.run-reviewed.v1",
        "workspace.navigation.after.v1",
        "home.after.v1",
        "page.header.actions.v1",
        "page.byline.after.v1",
        "page.detail.panel.after.v1",
        "tree.node.actions.v1",
        "discussions.panel.after.v1",
        "container.index.filters.after.v1",
        "record.row.actions.v1",
        "search.filters.after.v1",
        "settings.sections.after.v1",
        "mail.sender.v1",
        "ai.provider.v1",
        "identity.provider.v1",
        "chat.notifier.v1",
        "link.preview.v1"
      ],
      "limits": {
        "status": "estimated and unverified; every value here is a bootstrap estimate rewritten by the load test, and contract times are p95 at these values",
        "capacityEstimate": {
          "membersPerDeployment": 250,
          "guestsPerDeployment": 250,
          "containersPerWorkspace": 200,
          "pagesPerWorkspace": 100000,
          "pagesPerContainer": 20000,
          "treeDepth": 20,
          "blocksPerPage": 5000,
          "revisionsPerPage": 1000,
          "commentsPerPage": 1000,
          "databasesPerWorkspace": 2000,
          "recordsPerDatabase": 50000,
          "fieldsPerDatabase": 100,
          "viewsPerDatabase": 25,
          "automationRulesPerWorkspace": 200,
          "automationChainDepth": 5,
          "activityEntriesPerDeployment": 5000000
        },
        "attachmentStorageBytesPerDeployment": 8589934592,
        "uploadBytesPerFile": 104857600,
        "uploadTypes": [
          "application/pdf",
          "text/plain",
          "text/markdown",
          "text/csv",
          "text/html",
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
          "application/epub+zip",
          "application/zip",
          "image/png",
          "image/jpeg",
          "image/webp",
          "image/gif",
          "image/svg+xml",
          "video/mp4",
          "audio/mpeg"
        ],
        "exportArchiveBytes": 17179869184,
        "importArchiveBytes": 5368709120,
        "maximumUrlFetchRedirects": 3,
        "maximumUrlFetchBytes": 524288,
        "maximumUrlFetchSeconds": 5,
        "allowedFetchMediaTypes": [
          "text/html",
          "application/xhtml+xml"
        ],
        "concurrentAiRuns": 5,
        "operatorApiRequestsPerMinute": 600,
        "publicRequestsPerIpPerMinute": 120
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "a target, not a result: no audit has run against the spine's surfaces",
        "includes": [
          "operator app",
          "sign-in flow",
          "the anonymous read view of a published page"
        ]
      },
      "landing": null,
      "shots": [],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/slate",
        "release": "0.1.0",
        "manifestSha256": "bd68c05471dbddb4d9da809931d08dcb9b533f53c968912f56a98ee70d65c6d7"
      }
    },
    {
      "id": "vector",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Vector",
      "dir": "projects-vector",
      "category": "projects",
      "categoryLabel": "Projects",
      "spine": {
        "id": "projects",
        "record": "issue or task in a container"
      },
      "replaces": [
        {
          "name": "Linear",
          "edition": null
        },
        {
          "name": "Asana",
          "edition": null
        }
      ],
      "version": "0.0.0",
      "oneLiner": "A self-hosted, decision-centred issue tracker for one business, with evidence-backed intake, a unified Current stream, personal work queues, and…",
      "summary": "A self-hosted, decision-centred issue tracker for one business, with evidence-backed intake, a unified Current stream, personal work queues, and explainable project risk.",
      "scope": "",
      "maturity": "prototype-contract-draft",
      "clauseCount": 88,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Owner identity plate.",
          "text": "The public landing shows the configured owner's name, one configured statement of purpose, and a sign-in link. It contains no pricing, feature tour, customer testimonials, or invented metrics. The configured “Powered by <product> · runeditrun.com” credit is shown only when enabled."
        },
        {
          "id": "OPS-001",
          "title": "Equal operators.",
          "text": "Every operator can see and act on every team, issue, intake item, cycle, project, label, and shared saved view, and on every notification addressed to them. There are no roles. Team membership organises work and never grants or restricts access. A private saved view (VIEW-002) is the only record one operator cannot see."
        },
        {
          "id": "OPS-002",
          "title": "Operator management.",
          "text": "Operators are invited and removed in settings. The operator configured at setup can be removed only by themselves. *Policy: `operator.management.v1`; default: any operator may invite or remove.*"
        },
        {
          "id": "OPS-003",
          "title": "Workspace time.",
          "text": "Settings hold one IANA workspace timezone. Every calendar day, due reminder, cycle start and end instant, and seven-day window named in this contract is evaluated in that timezone. Changing it never rewrites stored instants, closed cycle snapshots, or reminders already delivered."
        },
        {
          "id": "OPS-004",
          "title": "Safe removal.",
          "text": "Vector rejects removal of the last operator. A successful removal revokes that operator's sessions, unassigns their non-terminal issues, transfers each project they lead and each shared saved view they own to the operator performing the removal — or to the longest-standing remaining operator when they removed themselves — deletes their private saved views and their notifications, anonymises their references in retained comments, activity, and audit records, and writes one activity entry per changed issue and project."
        },
        {
          "id": "TEAM-001",
          "title": "Team identity.",
          "text": "A team has a name and an identifier prefix, each unique across archived and non-archived teams. The prefix is two to ten uppercase letters and cannot be changed once the team has an issue, so existing identifiers stay resolvable. Every issue belongs to exactly one team."
        },
        {
          "id": "TEAM-002",
          "title": "Team workflow.",
          "text": "Every team has an ordered list of workflow statuses with at least one status in each of the Intake, Backlog, Unstarted, Started, Completed, Canceled, and Duplicate categories, and status names unique within the team. Operators can add, rename, and reorder statuses; a status never changes category, and no category is added or removed. A team's *default status* in a category is the first status of that category in workflow order, and every clause naming a category as a destination moves the issue there."
        },
        {
          "id": "TEAM-003",
          "title": "Team archiving.",
          "text": "Archiving a team stops new issues, intake, and issue moves from targeting it while preserving its issues, cycles, projects, identifiers, and history; a signal source request naming an archived team creates nothing and returns 409. Unarchiving restores normal use. Teams are never deleted; archiving is the only terminal state."
        },
        {
          "id": "TEAM-004",
          "title": "Status removal.",
          "text": "Removing a workflow status requires a destination status in the same category and atomically moves every issue to it. Vector rejects removal of a category's last status and records the status mapping in audit history."
        },
        {
          "id": "ISSUE-001",
          "title": "Creation.",
          "text": "An operator can create an issue with a required title and team plus an optional Markdown description, assignee, project, cycle, priority, labels, estimate, due date, expected outcome, customer impact, and custom fields. It starts in the team's default Backlog status unless another non-Intake status is explicitly chosen. *Policy: `issue.defaults.v1`; default: unassigned, no priority, no cycle, and no project.*"
        },
        {
          "id": "ISSUE-002",
          "title": "Stable identifiers.",
          "text": "An issue identifier is its team's prefix, a hyphen, and an integer one higher than the highest ever allocated under that prefix, so a number is never reused after an issue is deleted. The identifier never changes, including when the issue moves teams (ISSUE-012); a prefix therefore records where an issue started, not where it lives."
        },
        {
          "id": "ISSUE-003",
          "title": "Priority.",
          "text": "An issue has exactly one priority: Urgent, High, Medium, Low, or No priority. No priority is the default and sorts after Low."
        },
        {
          "id": "ISSUE-004",
          "title": "Status transitions.",
          "text": "A version-checked operator mutation (ISSUE-006) can move an issue in any direction among the Backlog, Unstarted, Started, Completed, and Canceled statuses of its team's workflow. Vector rejects moving any issue into an Intake status. Leaving Intake uses INTAKE-005, INTAKE-007, or REL-003; entering or leaving Duplicate uses REL-003 or REL-005. Entering Completed records the completion time and entering Canceled records the cancellation time; leaving one of those categories clears the time it recorded and removes no activity."
        },
        {
          "id": "ISSUE-005",
          "title": "Assignment.",
          "text": "An issue has at most one assignee. Assigning, reassigning, or unassigning it takes effect immediately and records the actor, the previous assignee, and the new assignee."
        },
        {
          "id": "ISSUE-006",
          "title": "Concurrent edits.",
          "text": "Every issue carries an integer version that increases by one on each mutation that appends an activity entry under ISSUE-007. An issue mutation carries the version the client last read; if the stored version is higher, Vector rejects the mutation with 409 and returns the current issue and version, changing nothing. Adding a comment, evidence revision, follower, or notification does not change the version and is never rejected as stale."
        },
        {
          "id": "ISSUE-007",
          "title": "Activity history.",
          "text": "Creation and every change to title, description, team, status, priority, assignee, project, cycle, estimate, due date, labels, expected outcome, customer impact, custom fields, duplicate target, parent, or dependency append an immutable activity entry recording the time, the previous value, the new value, and the actor — the operator, or the named automation when a cycle closure, deferral return, or import acted."
        },
        {
          "id": "ISSUE-008",
          "title": "Comments.",
          "text": "Any operator can add a Markdown comment, and can edit or delete any comment; the entry records who acted. An edited comment shows its edit time. Deleting a comment removes its body, attachments, links, and search entry, leaves a tombstone and an activity entry in the thread, and does not remove the follower state it created."
        },
        {
          "id": "ISSUE-009",
          "title": "Attachments and links.",
          "text": "Operators can attach files and named HTTPS links to an issue or a comment. Removing either records the action; removing a file also deletes its blob under `BASE-DATA-003`."
        },
        {
          "id": "ISSUE-010",
          "title": "Deletion.",
          "text": "Deleting an issue removes its comments, attachments, links, evidence revisions, dependencies, parent link, follower state, notifications, activity, and search entry; its children become root issues, and a duplicate is removed from its canonical issue's list. Vector rejects deleting a canonical issue that still has duplicates. Closed cycle snapshots (CYCLE-005) and audit entries keep only its identifier, and its source idempotency records (INTAKE-002) are retained so a re-delivery cannot resurrect it. *Policy: `issue.can-delete.v1`; default: any operator.*"
        },
        {
          "id": "ISSUE-011",
          "title": "Estimates.",
          "text": "An issue estimate is absent or an integer from 1 through 100 points. Changing an estimate never rewrites a closed cycle snapshot."
        },
        {
          "id": "ISSUE-012",
          "title": "Moving teams.",
          "text": "Moving an issue requires a non-archived target team and an explicit target status in the same category as its current status, including Intake so a misrouted intake item reaches the right team without a decision. The move preserves the identifier (ISSUE-002), uncommits the issue from any cycle (CYCLE-002), keeps its project, assignee, labels, and history, and writes one activity entry. All of it happens or none of it does."
        },
        {
          "id": "ISSUE-013",
          "title": "Mentions.",
          "text": "Typing `@` in an issue description or comment offers matching operators, and choosing one records a mention of that operator. Text resolving to no operator is stored and shown literally and mentions nobody. A mention adds a follower (NOTIFY-002) and notifies once (NOTIFY-001); editing text to add a mention notifies only the newly mentioned operator, and removing a mention notifies nobody and withdraws no delivered notification."
        },
        {
          "id": "ISSUE-014",
          "title": "Labels.",
          "text": "A label belongs to the workspace and has a name unique among labels and a colour. Any operator can create, rename, recolour, and delete one; deleting a label removes it from every issue and from the filters of saved views naming it, and deletes no issue. An issue carries up to the number of labels in `seed.json → limits`."
        },
        {
          "id": "ISSUE-015",
          "title": "Unsent drafts.",
          "text": "An unsubmitted new-issue form is retained in the operator's browser and restored the next time they open new issue there. A draft is not a record: it appears in no list, search result, notification, or export, and becomes an issue only when that operator creates it."
        },
        {
          "id": "REL-001",
          "title": "Dependencies.",
          "text": "An operator can record that one issue blocks another, across teams, and can remove the link. Vector rejects self-links, a link that already exists in either direction, and any link that would create a directed cycle."
        },
        {
          "id": "REL-002",
          "title": "Blocked state.",
          "text": "An issue is shown as blocked while any issue that blocks it is outside a Completed, Canceled, or Duplicate category. Blocking never changes the issue's workflow status by itself."
        },
        {
          "id": "REL-003",
          "title": "Duplicate decision.",
          "text": "Marking an issue as a duplicate requires one canonical issue that is neither itself nor already a duplicate, moves it to the team's default Duplicate status, and preserves its description, comments, evidence, attachments, and activity under the duplicate issue."
        },
        {
          "id": "REL-004",
          "title": "Canonical duplicates.",
          "text": "A canonical issue lists every direct duplicate. If it is later marked duplicate, Vector atomically repoints its duplicates to the new canonical issue, so a duplicate chain or cycle is never exposed."
        },
        {
          "id": "REL-005",
          "title": "Undo duplicate.",
          "text": "Undoing a duplicate decision removes the canonical link and restores the issue's most recent non-terminal status, or its team's default Backlog status if it has none, while preserving both decision activity entries."
        },
        {
          "id": "REL-006",
          "title": "Parent and child.",
          "text": "An issue has at most one parent. Vector rejects self-parenting and ancestry cycles; changing a parent never changes status, team, project, cycle, or assignee, and deleting a parent makes each direct child a root issue."
        },
        {
          "id": "INTAKE-001",
          "title": "Intake creation.",
          "text": "A registered signal source (INTAKE-011) creates an intake issue with a title, team, observed time, its source ID, source display name, source record identifier, source revision identifier, summary, link, and optional structured measurements and customer reports. An operator creates one with a title and team and any of the same descriptive fields, attributed to that operator instead of a source. Either way it starts in the team's default Intake status and appears in Needs decision (CURR-002) within 5 seconds."
        },
        {
          "id": "INTAKE-002",
          "title": "Idempotent sources.",
          "text": "Re-delivery of the same source ID, source record identifier, source revision identifier, and normalised payload returns the original result and creates nothing. Re-delivery of an identity whose issue has since been deleted (ISSUE-010) returns 410 and creates nothing. Reusing an identity with a different payload returns 409 and changes nothing."
        },
        {
          "id": "INTAKE-003",
          "title": "Evidence revisions.",
          "text": "A new source revision identifier for an existing source record appends immutable dated evidence even when revisions arrive out of order. The issue orders evidence by observed time, then receipt time, and shows a field-level change from the preceding observed revision."
        },
        {
          "id": "INTAKE-004",
          "title": "Inspectable assessment.",
          "text": "An intake issue shows the evidence behind its attention reason, impact measurements, likely cause, recommended assignee, and suggested priority. Each assertion names whether it came from a source, an operator, or a policy; Vector does not invent or silently infer assertions in core."
        },
        {
          "id": "INTAKE-005",
          "title": "Accept.",
          "text": "Accepting an intake issue atomically records the actor, the time, and an optional reason; applies the chosen assignee, priority, cycle, and Started or Unstarted status; and clears any deferral. It leaves Needs decision and reaches its new Current and My work sections within the CURR-008 window."
        },
        {
          "id": "INTAKE-006",
          "title": "Defer.",
          "text": "Deferring an intake issue records the operator, an absolute return time strictly in the future, and an optional reason, and removes it from Needs decision. It returns there exactly once — within 5 minutes of the return time, or within 5 seconds of a new evidence revision (INTAKE-003), whichever comes first — and the return atomically clears the deferral and leaves the earlier decision visible. An operator can defer it again or end a deferral early."
        },
        {
          "id": "INTAKE-007",
          "title": "Decline.",
          "text": "Declining an intake issue requires a reason, moves it to the team's default Canceled status, and removes it from Needs decision without deleting its evidence or decision history."
        },
        {
          "id": "INTAKE-008",
          "title": "Evidence after a terminal decision.",
          "text": "Evidence received after an issue is Canceled or Duplicate is appended and visible but never reopens, reassigns, reprioritises, or resurfaces the issue."
        },
        {
          "id": "INTAKE-009",
          "title": "Source failure.",
          "text": "A signal source request from an unknown or revoked source, or carrying a wrong signature, returns 401; one whose body fails validation returns 400; one naming an archived team returns 409 (TEAM-003). None of them creates anything. A valid request that cannot be stored durably returns 503 so the source retries. No request is acknowledged before its idempotency record and evidence are durable."
        },
        {
          "id": "INTAKE-010",
          "title": "Decision policy.",
          "text": "Suggested assignee, priority, and start state are selected by the intake recommendation policy and are always editable before acceptance. *Policy: `intake.recommendation.v1`; default: use an explicitly supplied team and assignee, otherwise leave the issue unassigned with No priority and Unstarted status.*"
        },
        {
          "id": "INTAKE-011",
          "title": "Source registration.",
          "text": "An operator registers a signal source with a display name, a source ID unique in the workspace, and a signing secret; the `signal.source.v1` adapter normalises that source's signed request into INTAKE-001. Rotating a secret accepts both secrets for a configurable overlap of at most 24 hours and then rejects the previous one. Revoking a source rejects every later request under INTAKE-009 and deletes no evidence. A secret is shown once at registration or rotation and never again."
        },
        {
          "id": "CURR-001",
          "title": "Disjoint stream.",
          "text": "Current places an issue in at most one section using this precedence: Needs decision, Now, Next, then Recently done. Canceled, Duplicate, deleted, and actively deferred issues do not appear."
        },
        {
          "id": "CURR-002",
          "title": "Needs decision.",
          "text": "Needs decision contains every Intake-category issue whose deferral has expired or is absent and which has not been accepted, declined, or marked duplicate."
        },
        {
          "id": "CURR-003",
          "title": "Now.",
          "text": "Now contains non-terminal issues outside the Intake category that are overdue or in a Started status, excluding anything already in Needs decision. An issue is *overdue* when its due date is earlier than the current calendar day in the workspace timezone (OPS-003)."
        },
        {
          "id": "CURR-004",
          "title": "Next.",
          "text": "Next contains non-terminal issues in an Unstarted status that are committed to their team's active or next cycle (CYCLE-001) or due within the next seven calendar days, excluding anything already in a higher-precedence section."
        },
        {
          "id": "CURR-005",
          "title": "Recently done.",
          "text": "Recently done contains issues moved to a Completed category in the previous seven calendar days, newest completion first."
        },
        {
          "id": "CURR-006",
          "title": "Explainable attention.",
          "text": "Every issue in Current shows one primary inclusion reason — Awaiting decision, Started work, Overdue, Cycle commitment, Due within seven days, or Completed within seven days — and every applicable context reason from Customer escalation, Unresolved dependency, Unassigned urgent work, Project risk, and New evidence since the most recent intake decision."
        },
        {
          "id": "CURR-007",
          "title": "Ranking.",
          "text": "Within a section the default order sorts by priority (Urgent first, No priority last), then by due date (overdue first, then earliest first, then issues with no due date), then by most recent update, and finally by stable identifier, so the order is total and stable. *Policy: `current.ranking.v1`.*"
        },
        {
          "id": "CURR-008",
          "title": "Freshness.",
          "text": "A decision or issue mutation that changes an issue's Current membership, section, or reasons is reflected in an already open Current view within 5 seconds without an operator-initiated reload, and in the next response to any other request."
        },
        {
          "id": "CURR-009",
          "title": "Stream controls.",
          "text": "Operators can filter Current by team, project, priority, item type, assignee, and attention reason. The default includes all teams and all operators and shows Recently done."
        },
        {
          "id": "WORK-001",
          "title": "Personal queue.",
          "text": "My work contains every non-terminal issue assigned to the signed-in operator outside the Intake category, grouped disjointly in this precedence: Waiting for issues blocked under REL-002, Now for those the CURR-003 rule would place in Now, and Next for every issue still remaining, each group ordered by CURR-007. An issue assigned to the operator while in an Intake status appears in Current's Needs decision, not here."
        },
        {
          "id": "WORK-002",
          "title": "Personal defaults.",
          "text": "Completed, Canceled, Duplicate, and actively deferred issues never appear in My work; an operator reads their finished work from Current's Recently done filtered to themselves (CURR-005, CURR-009). When no issue qualifies, the view states the operator is caught up and offers new issue and Current as next actions."
        },
        {
          "id": "WORK-003",
          "title": "Cycle context.",
          "text": "My work shows one row for each team that has an active cycle containing an issue assigned to the operator, giving that cycle's name, start and end dates, and progress, with completed and total committed scope labelled in the unit CYCLE-003 used."
        },
        {
          "id": "PROJ-001",
          "title": "Project record.",
          "text": "A project has a name unique in the workspace, a Markdown description, exactly one lead who is an operator, an optional target date, a lifecycle, an optional colour, an optional icon, and custom fields. Its lifecycle is Planned, Active, Completed, or Canceled, and it starts as Planned."
        },
        {
          "id": "PROJ-002",
          "title": "Manual lifecycle.",
          "text": "Only an operator changes a project's lifecycle. Completing or canceling all of its issues never completes or cancels the project."
        },
        {
          "id": "PROJ-003",
          "title": "Project membership.",
          "text": "An issue belongs to at most one project. Removing an issue from a project or archiving a project never deletes the issue; deleting a project detaches all of its issues but deletes none of them."
        },
        {
          "id": "PROJ-004",
          "title": "Progress.",
          "text": "Project progress counts issues: completed project issues divided by all project issues outside the Canceled and Duplicate categories. The interface shows the completed and total counts beside the percentage and reports no progress when the denominator is zero."
        },
        {
          "id": "PROJ-005",
          "title": "Health.",
          "text": "Health is reported only for projects in the Active lifecycle; every other lifecycle reports no health and is excluded from health filters and counts. A *remaining issue* is a project issue outside the Completed, Canceled, and Duplicate categories. An Active project is Blocked, At risk, On track, or Unknown, evaluated in that order. The default reports Blocked when at least one remaining issue exists and either an Urgent remaining issue has an unresolved blocker (REL-002) or every remaining issue is blocked; At risk when its target date is in the past, a remaining Urgent issue is unassigned, or its remaining estimated points committed to any team's active cycle exceed that cycle's capacity; On track when a target date in the future, every applicable capacity, and every remaining active-cycle estimate exist and no earlier rule matches; and Unknown otherwise. Remaining scope exactly equal to capacity is not At risk. *Policy: `project.health.v1`.*"
        },
        {
          "id": "PROJ-006",
          "title": "Risk explanation.",
          "text": "Every Blocked or At risk project shows the exact issues and measurements that triggered its health, including unresolved dependencies, unowned urgent work, target-date exposure, and committed scope against capacity."
        },
        {
          "id": "PROJ-007",
          "title": "Portfolio.",
          "text": "The Projects view shows the count of projects in each health and lifecycle state and can filter by lifecycle, health, lead, team, target date, and label. Counts and rows use the same active filters."
        },
        {
          "id": "PROJ-008",
          "title": "Historical truth.",
          "text": "Later issue edits update live project progress and health but never rewrite a closed cycle snapshot shown in that project's history."
        },
        {
          "id": "PROJ-009",
          "title": "Project archiving.",
          "text": "Archiving a project removes it from the default portfolio and stops new issues from joining it while preserving its lifecycle, issues, history, and closed-cycle snapshots. Operators can include archived projects in filters or unarchive one."
        },
        {
          "id": "CYCLE-001",
          "title": "Time box.",
          "text": "A cycle belongs to one team and has a name unique within that team, a start instant, an end instant after the start, and an optional capacity in estimate points, all interpreted in the workspace timezone (OPS-003). Vector rejects a cycle whose dates overlap another cycle of the same team, so a team's *active cycle* — the one whose start is at or before now and whose end is after now — is unique, and its *next cycle* is the cycle of that team with the earliest start at or after the active cycle's end."
        },
        {
          "id": "CYCLE-002",
          "title": "Commitment.",
          "text": "A non-terminal issue can be committed to at most one cycle, and only to a cycle of its own team. Committing, moving, and uncommitting record both cycle identifiers and update both cycles atomically."
        },
        {
          "id": "CYCLE-003",
          "title": "Progress and capacity.",
          "text": "Cycle progress is completed committed estimate points divided by all non-Canceled, non-Duplicate committed estimate points when every included issue is estimated; otherwise it uses issue counts and labels the result `by issue count`. With no included issues it shows `No committed work` and no percentage. Capacity risk is Unknown unless capacity and every remaining estimate exist, At risk when remaining points exceed capacity, and Within capacity otherwise."
        },
        {
          "id": "CYCLE-004",
          "title": "Closure and rollover.",
          "text": "Within 5 minutes of a cycle's end instant Vector closes it exactly once however many times the closing job runs, and by default moves committed issues in an Unstarted or Started status to the team's next cycle (CYCLE-001) and uncommits committed issues in an Intake, Backlog, Completed, Canceled, or Duplicate status. A closed cycle's name, dates, capacity, and snapshot cannot be changed, and a closed cycle is never reopened. *Policy: `cycle.rollover.v1`.*"
        },
        {
          "id": "CYCLE-005",
          "title": "Closure snapshot.",
          "text": "Closing a cycle records committed and completed issue IDs, each issue's project ID at close, estimate totals, capacity, progress, additions and removals during the cycle, and the close time. The snapshot is immutable."
        },
        {
          "id": "CYCLE-006",
          "title": "Missing next cycle.",
          "text": "If rollover is required and no next cycle exists, the cycle still closes, its unfinished issues become uncommitted, and one visible failed-operation entry names the affected issues; creating a later cycle does not move them."
        },
        {
          "id": "SEARCH-001",
          "title": "Global search.",
          "text": "Search covers issue identifiers, titles, descriptions, expected outcomes, customer impact, comments, evidence summaries, project names, and label names, and returns within 500ms. Issues of archived teams and projects are excluded unless the operator asks for them."
        },
        {
          "id": "SEARCH-002",
          "title": "Exact identifier.",
          "text": "An exact issue identifier lookup reads the primary record and returns it even while full-text indexing is delayed or failed."
        },
        {
          "id": "SEARCH-003",
          "title": "Index freshness.",
          "text": "Created or changed searchable text appears in full-text results within 10 seconds. A failed indexing job is visible to operators with its last successful index time; Vector never presents known-stale results as complete."
        },
        {
          "id": "VIEW-001",
          "title": "Issue controls.",
          "text": "Issue lists can filter by status, status category, priority, assignee, team, project, cycle, label, due date, and blocked state; sort by newest, oldest, Urgent-to-No-priority, workflow status order, due date, or last update; and group by none, status, assignee, priority, project, cycle, or team. Completed issues are hidden by default outside Current."
        },
        {
          "id": "VIEW-002",
          "title": "Saved views.",
          "text": "An operator can save a named combination of filters, sort, and grouping as private or shared, then update, duplicate, or delete it. A private view is visible and editable only by its owner; any operator can open or duplicate a shared view, but only its owner can update or delete it, and ownership changes only under OPS-004. Deleting a view never changes an issue."
        },
        {
          "id": "VIEW-003",
          "title": "Control persistence.",
          "text": "The filters, sort, and grouping an operator last applied to Current, My work, the Projects portfolio, and each issue list are stored for that operator and restored on their next visit from any browser, and one visible control resets them to the defaults. Opening a saved view never changes the stored controls of the list it was opened from."
        },
        {
          "id": "VIEW-004",
          "title": "Bulk edit.",
          "text": "An operator can select several issues in a list and set status, assignee, priority, project, cycle, or labels on all of them in one operation, up to the batch limit in `seed.json → limits`. The operation is atomic: if any selected issue fails ISSUE-004, ISSUE-006, or CYCLE-002, nothing is written and the response names the failing issues by identifier. A successful operation writes one activity entry per issue."
        },
        {
          "id": "CMD-001",
          "title": "Command palette.",
          "text": "The command palette searches issues, projects, saved views, and available commands from one input. The up and down arrows move the highlight, Enter opens the highlighted record or performs the highlighted command, and Escape closes the palette without acting."
        },
        {
          "id": "CMD-002",
          "title": "Keyboard navigation.",
          "text": "Outside editable controls, Command/Ctrl-K opens the palette, C opens new issue, G then M opens My work, G then C opens Current, G then P opens Projects, and Escape closes the topmost transient surface. Every command also has a visible, keyboard-operable control."
        },
        {
          "id": "CMD-003",
          "title": "Focus and selection.",
          "text": "The up and down arrows move a visible focus indicator through the rows of the active list, Enter opens the focused row, X adds or removes it from the selection VIEW-004 acts on, and Escape returns focus from an open detail pane to the row it was opened from. Changing a filter, sort, or grouping keeps the focused row focused while it remains in the list and focuses the first row when it does not."
        },
        {
          "id": "NOTIFY-001",
          "title": "Inbox events.",
          "text": "Vector creates an in-app notification, visible within 5 seconds, for assignment of an issue to the operator, a direct mention (ISSUE-013), a comment on an issue they follow, the return of an intake item they deferred, the resolution of the last blocker on an issue assigned to them, and a due reminder (NOTIFY-005). Vector delivers notifications on no other channel; the only mail it sends is the sign-in link `BASE-ACCESS-001` requires."
        },
        {
          "id": "NOTIFY-002",
          "title": "Followers.",
          "text": "Issue creators, assignees, commenters, and directly mentioned operators follow the issue automatically; an operator can follow or unfollow it explicitly without changing anyone else's following."
        },
        {
          "id": "NOTIFY-003",
          "title": "No self-notification.",
          "text": "An action never notifies its actor. One event creates at most one notification per recipient and issue even when the recipient qualifies through several follower rules."
        },
        {
          "id": "NOTIFY-004",
          "title": "Read and mute.",
          "text": "Opening a notification marks it read, an operator can mark all of theirs read in one action, and the interface shows their unread count. An operator can mute an issue or an event type, which suppresses only their own later matching notifications and alters neither activity history nor another operator's notifications."
        },
        {
          "id": "NOTIFY-005",
          "title": "Due reminders.",
          "text": "Within 5 minutes of the configured daily reminder time — 09:00 in the workspace timezone by default — Vector notifies the assignee once for each incomplete issue assigned to them that is due that day, and once more on the first run after it becomes overdue. Changing a due date makes the new date eligible and never repeats a reminder already delivered for the same issue and date."
        },
        {
          "id": "DATA-001",
          "title": "Export contents.",
          "text": "The portable export contains a manifest, original attachment bytes, and every operator, team, workflow status, issue, comment, attachment metadata, link, relation, evidence revision, intake decision, project, cycle and snapshot, label, saved view, notification, custom field, non-secret setting, idempotency record, failed operation, and audit entry. It never contains a session, signing, source, or provider secret."
        },
        {
          "id": "DATA-002",
          "title": "Mapped import preflight.",
          "text": "Separately from portable restore, an operator can preflight an import of issues supplied as CSV rows or as a JSON array of objects with the same field names, before anything is written. The fields are title, description, team, status, priority, assignee, project, cycle, labels, estimate, due date, expected outcome, customer impact, source ID, and source record ID. The preflight validates the mapping, the timestamps, and every team, status, assignee, project, cycle, label, duplicate, and dependency reference, and reports every row that would fail with its row number and reason."
        },
        {
          "id": "DATA-003",
          "title": "Atomic mapped import.",
          "text": "Applying a successful preflight writes the batch atomically, preserves supplied creation and update times, and records an import-run identifier. A failed apply writes none of the batch."
        },
        {
          "id": "DATA-004",
          "title": "Idempotent mapped import.",
          "text": "Reapplying the same import-run identifier and source record identifiers creates no duplicates and returns the first run's result. A mapped import creates normal activity and one audit entry for the run, and no operator notifications."
        },
        {
          "id": "DATA-005",
          "title": "Exact portable restore.",
          "text": "`pnpm import` restores a portable DATA-001 archive without synthesising activity, audit entries, notifications, or new timestamps; its records are equivalent after export, import, and export as `BASE-DATA-001` requires."
        }
      ],
      "hasReadme": false,
      "hasBaseline": true,
      "nonGoals": [
        "Multiple workspaces, tenants, or organisations",
        "Roles, per-team visibility, and granular permissions",
        "External customer accounts, portals, forms, or public roadmaps",
        "Source-specific Sentry, Slack, GitHub, GitLab, Jira, or email integrations in core",
        "AI triage, deduplication, ranking, or project-health inference",
        "Custom workflow categories",
        "Manually drag-ranked backlogs; every order in Vector is derived and explainable",
        "Initiatives, roadmaps, Gantt charts, critical-path planning, time tracking, or resource management",
        "Per-issue checklists",
        "Multiple assignees",
        "Notification delivery by email, chat, or push; notifications are in-app only",
        "Native mobile applications",
        "Real-time collaborative text editing"
      ],
      "externals": [
        {
          "name": "transactional-email",
          "required": true,
          "requiredWhen": "",
          "reason": "BASE-ACCESS-001 requires delivery of single-use magic links; Cloudflare D1, R2, and Queues do not deliver internet email. It is the only outbound channel: NOTIFY-001 keeps every product notification in-app.",
          "data": [
            "operator email address",
            "single-use sign-in URL"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "estimate",
        "monthlyUsd": "0-15",
        "assumptions": "Up to 20 active operators, 100000 issues, 5 GB attachments, and moderate queue traffic on Cloudflare; transactional email is billed separately by the selected provider."
      },
      "extensionPoints": [
        "app.page.v1",
        "app.navigation.v1",
        "settings.section.v1",
        "api.route.v1",
        "job.consumer.v1",
        "schedule.cron.v1",
        "operator.management.v1",
        "issue.defaults.v1",
        "issue.can-delete.v1",
        "intake.recommendation.v1",
        "current.ranking.v1",
        "project.health.v1",
        "cycle.rollover.v1",
        "issue.created.v1",
        "issue.updated.v1",
        "issue.completed.v1",
        "comment.created.v1",
        "intake.received.v1",
        "intake.decided.v1",
        "project.updated.v1",
        "cycle.closed.v1",
        "notification.created.v1",
        "import.completed.v1",
        "navigation.after.v1",
        "current.header.after.v1",
        "current.item.actions.v1",
        "my-work.header.after.v1",
        "issue.header.after.v1",
        "issue.sidebar.after.v1",
        "issue.detail.tabs.v1",
        "project.row.actions.v1",
        "project.detail.tabs.v1",
        "settings.sections.after.v1",
        "mail.sender.v1",
        "signal.source.v1"
      ],
      "limits": {
        "status": "estimated-not-load-tested",
        "operators": 100,
        "concurrentOperators": 50,
        "teams": 50,
        "issues": 250000,
        "projects": 2000,
        "cyclesPerTeam": 500,
        "commentsPerIssue": 1000,
        "evidenceRevisionsPerIssue": 500,
        "savedViewsPerOperator": 100,
        "labels": 500,
        "labelsPerIssue": 20,
        "dependencyLinksPerIssue": 50,
        "signalSources": 50,
        "bulkEditIssuesPerOperation": 250,
        "importRecordsPerRun": 5000,
        "attachmentBytesPerFile": 26214400,
        "attachmentBytesTotal": 10737418240,
        "attachmentMimeTypes": [
          "image/png",
          "image/jpeg",
          "image/webp",
          "image/gif",
          "application/pdf",
          "text/plain",
          "text/csv",
          "application/json",
          "application/zip"
        ],
        "requestBodyBytes": 1048576,
        "authenticatedRequestsPerMinutePerOperator": 600,
        "publicRequestsPerMinutePerIp": 120,
        "magicLinkRequestsPer15MinutesPerIp": 5,
        "signalSourceRequestsPerMinutePerSource": 300,
        "signalSourceRequestsPerMinutePerIp": 600
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target; automated level AA, not yet verified on the prototype"
      },
      "landing": "assets/seeds/vector/00-owner-landing.jpg",
      "shots": [
        "assets/seeds/vector/00-owner-landing.jpg",
        "assets/seeds/vector/01-current-desktop.jpg",
        "assets/seeds/vector/02-issue-evidence.jpg",
        "assets/seeds/vector/03-my-work.jpg",
        "assets/seeds/vector/04-projects.jpg",
        "assets/seeds/vector/05-project-risk.jpg"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/vector",
        "release": "0.0.0",
        "manifestSha256": "b0a04e9e39d93c13090dc03a08ed8e74b232c567fff6d4f0cc177f78b43a83f9"
      }
    },
    {
      "id": "cradle",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Cradle",
      "dir": "records-cradle",
      "category": "records",
      "categoryLabel": "Records",
      "spine": {
        "id": "records",
        "record": "table with typed fields and views"
      },
      "replaces": [
        {
          "name": "Airtable",
          "edition": null
        }
      ],
      "version": "0.0.0-bootstrap",
      "oneLiner": "A self-hosted launch-portfolio operating system for one business, focused on campaign readiness, risks, milestones, decisions, saved lenses, and…",
      "summary": "A self-hosted launch-portfolio operating system for one business, focused on campaign readiness, risks, milestones, decisions, saved lenses, and visible automation.",
      "scope": "one business, one workspace, equal operators",
      "maturity": "ui-ported-thin-data-spine",
      "clauseCount": 97,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Owner identity plate.",
          "text": "At `/`, this internal tool presents a public identity plate using values in `src/ext/config.ts` and the configured theme tokens: the owner's configured name, one configurable description of their launch workspace, and a link to `/signin`. Its optional footer credit is visible by default and config can turn it off; when visible it names the configured product and links statically to `https://runeditrun.com`. The credit adds no telemetry or phone-home. The landing contains no record content, public views, pricing, feature tour, testimonials, or metrics."
        },
        {
          "id": "OPS-001",
          "title": "Equal operators.",
          "text": "Every operator can read every record in the workspace and perform every mutation in this contract. The only restrictable action is inviting and removing operators, under `operator.management.v1`. There are no roles."
        },
        {
          "id": "OPS-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. The operator configured at setup can be removed only by themselves, so every operator record stays removable and erasable by an operator. *Policy: `operator.management.v1`; default: any operator may invite or remove.*"
        },
        {
          "id": "OPS-003",
          "title": "Workspace timezone.",
          "text": "Setup requires a valid IANA workspace timezone and rejects any other value with a validation error. Changing it re-renders every displayed time and moves the next fire time of every schedule in this contract; it leaves stored UTC timestamps byte-identical and moves no run already queued and no email already sent."
        },
        {
          "id": "OPS-004",
          "title": "Removing an operator.",
          "text": "Removing an operator ends their sessions within 1 minute, drops them from digest recipients, and cancels their outstanding readiness requests. Removal is rejected, naming each blocking record, while that operator owns an active campaign or is the assignee of an unresolved decision. Risks, deliverables, milestones, comments, activity entries, and AI analyses keep their attribution to the removed operator, and a lens condition naming them returns zero results rather than an error."
        },
        {
          "id": "OPS-005",
          "title": "Erasing an operator.",
          "text": "A removed operator can be erased under `BASE-DATA-003`. Erasure replaces their name and email with a stable anonymous label wherever they appear, deletes their notifications and delivery records, keeps the comments, activity entries, and decision outcomes they authored under that label, completes within 5 minutes, is confirmed when complete, and cannot be undone."
        },
        {
          "id": "OPS-006",
          "title": "Money.",
          "text": "Setup requires an ISO 4217 workspace currency. Budget and spend are stored as minor-unit integers in that currency and are displayed, summed, and exported in it. Changing the currency relabels stored amounts and never converts them."
        },
        {
          "id": "VIEW-001",
          "title": "Operating surfaces.",
          "text": "The operator app exposes Workspace Home, Portfolio Overview, Campaign Grid, Risks Lens, Timeline Lens, Campaign Detail, Lens Builder, Decision Queue, and Automation Monitor, each at its own route and each reachable from one navigation shell present on all of them."
        },
        {
          "id": "VIEW-002",
          "title": "Selected detail.",
          "text": "Selecting a campaign, risk, milestone, or decision shows that record's own fields and the records linked to it. No field and no identifier of a previously selected record stays visible."
        },
        {
          "id": "VIEW-003",
          "title": "Cross-view consistency.",
          "text": "A read issued after a mutation's success response returns that mutation's committed state and the counts derived from it on Workspace Home, Portfolio Overview, Campaign Grid, Campaign Detail, every lens, Decision Queue, and Automation Monitor."
        },
        {
          "id": "VIEW-004",
          "title": "Explicit states.",
          "text": "Every collection and detail surface renders exactly one of loading, empty, not found, signed out, or failed. The failed state names the operation that failed and offers a retry; it renders no rows, no sample data, and no result of an earlier successful read. An action a policy forbids is refused with a stated reason and changes nothing."
        },
        {
          "id": "VIEW-005",
          "title": "Search.",
          "text": "Global search matches a case-insensitive token prefix of a campaign name, code, or summary, or of a deliverable, risk, milestone, or decision title, returns at most 20 results per record type each linking to that record's detail, and responds within 500ms. Comment text is not searched. Records on archived campaigns are returned only when the search explicitly includes archived campaigns."
        },
        {
          "id": "VIEW-006",
          "title": "Concurrent edits.",
          "text": "Every mutation of a campaign, deliverable, milestone, risk, decision, comment, lens, custom-field definition, automation, or workspace setting supplies that record's current version. A mutation carrying an older version returns 409, names the current version, and changes nothing."
        },
        {
          "id": "VIEW-007",
          "title": "Paging and order.",
          "text": "A collection response returns at most 200 records with a cursor, applies the requested sort with the record ID as its final tiebreak so paging repeats and skips nothing, and reports an exact total up to 10,000 matches and `10,000+` beyond it. A result larger than one page states how many records are shown out of that total."
        },
        {
          "id": "CAMP-001",
          "title": "Record.",
          "text": "An operator can create, edit, archive, and delete a campaign. It requires a code, a name, an owner, a start date, and a launch date, and it also carries an objective, a status summary of at most 280 characters, a phase, a status, health, budget, spend, tags, and custom fields."
        },
        {
          "id": "CAMP-002",
          "title": "Lifecycle.",
          "text": "A campaign is active or archived, and any operator can archive or restore one. Archiving deletes nothing. *Policy: `campaign.lifecycle.v1`; default phases: Planning, Build, Production, Review, Launch prep; default statuses: On track, Watch, At risk, Blocked; default launch-preparation phase: Launch prep.*"
        },
        {
          "id": "CAMP-003",
          "title": "Validation.",
          "text": "A campaign's launch date must not fall before its start date, its health must be a whole number from 0 to 100, and its budget and spend must be zero or greater. Campaign codes are trimmed and unique case-insensitively. A mutation breaking any of these, or clearing the owner, start date, or launch date of an active campaign, is rejected with a validation error naming the field and changes nothing."
        },
        {
          "id": "CAMP-004",
          "title": "Detail and rollups.",
          "text": "Campaign detail shows the campaign's fields, its complete and total deliverable counts, its milestones, its open risks, its unresolved decisions, its comments, and its activity. Those counts are derived: they change only through a mutation to a linked record."
        },
        {
          "id": "CAMP-005",
          "title": "Health.",
          "text": "Health is shown with the operator who last set it and when. A campaign whose health has not been set in the last 14 days is marked stale. *Policy: `campaign.health.v1`; default: health and status change only through an explicit operator edit or an import row that maps the field. A replacement may derive health from linked records; no replacement may set it from AI output, which AI-003 forbids.*"
        },
        {
          "id": "CAMP-006",
          "title": "Portfolio overview.",
          "text": "Portfolio Overview shows Live campaigns as the count of non-archived campaigns, Needs attention as the count of non-archived campaigns whose status is At risk or Blocked, Open decisions as the count of unresolved decisions on non-archived campaigns, and Portfolio health as the arithmetic mean of non-archived campaign health rounded half up to a whole number, or No data when there are none. It states how many campaigns in that mean are stale under CAMP-005."
        },
        {
          "id": "CAMP-007",
          "title": "Grid.",
          "text": "Any operator can filter, group, sort, and choose visible fields in the campaign grid. Three built-in views ship: All active, every non-archived campaign; Launch prep, every non-archived campaign in the lifecycle policy's launch-preparation phase; and My campaigns, every non-archived campaign owned by the signed-in operator. Each re-queries on open and on refresh, and opening, filtering, grouping, or sorting a view changes no record."
        },
        {
          "id": "CAMP-008",
          "title": "Custom fields.",
          "text": "An operator can define, edit, and delete campaign custom fields of the types in `seed.json → customFields`, and those fields are available in grid display, filtering, lens conditions, CSV import, and CSV export. Deleting a definition deletes its stored values and is confirmed with the number of campaigns holding one."
        },
        {
          "id": "CAMP-009",
          "title": "Archiving.",
          "text": "Archiving a campaign is allowed while it has open deliverables, milestones, risks, and decisions, and withdraws its work in one commit: its unresolved decisions leave Decision Queue, its risks and milestones leave every lens and the digest, its incomplete deliverables and milestones stop counting as overdue, it leaves the counts in CAMP-006 and every search that does not ask for archived campaigns, its outstanding readiness requests and every queued automation run whose subject is that campaign or one of its records are cancelled without executing an action, and email held under NOTIFY-002 for that campaign is dropped. Archiving completes and resolves nothing."
        },
        {
          "id": "CAMP-010",
          "title": "Restoring.",
          "text": "Restoring returns the campaign and every record archived with it to the state each had at archiving, and creates no notification, readiness request, or automation run for the archived period. CAMP-003 is applied at restore: a campaign whose owner was removed while it was archived must be given a current owner before it becomes active."
        },
        {
          "id": "CAMP-011",
          "title": "Deletion.",
          "text": "Deleting a campaign removes its deliverables, its milestones and their dependencies, its risks, decisions, comments, campaign activity, AI analyses, and import references under `BASE-DATA-003`. Deletion requires typing the campaign code to confirm and cannot be undone. Audit records survive with the campaign reference anonymised."
        },
        {
          "id": "CAMP-012",
          "title": "Risk rating.",
          "text": "A campaign's risk rating is derived as the highest severity among its open risks, or None when it has none, and is shown wherever the campaign is listed."
        },
        {
          "id": "CAMP-013",
          "title": "Bulk edit.",
          "text": "In the campaign grid and in any Grid or Cards lens an operator can select up to 500 records, including every record matching the current conditions, and apply one change to all of them: owner, phase, status, a tag added or removed, a custom-field value, archive, or restore. A bulk change commits for every selected record or none, is rejected naming the first record that fails validation, writes one activity entry on each changed record and one bulk entry naming the operator and the count, and can be undone once within 1 hour, restoring the prior value of every record it changed."
        },
        {
          "id": "CAMP-014",
          "title": "Duplicating.",
          "text": "Duplicating a campaign copies its fields, tags, custom-field values, deliverables, and milestones under a new unique code, and copies no risks, decisions, comments, activity, notifications, or AI analyses. When the operator supplies a new launch date, every copied date shifts by the difference between it and the original launch date; otherwise dates are copied unchanged. The copy triggers no automation."
        },
        {
          "id": "DELIV-001",
          "title": "Record.",
          "text": "An operator can create, edit, and delete a deliverable. It belongs to exactly one campaign, requires a title and an owner, has an optional due date, and has a state of Open or Complete that starts Open."
        },
        {
          "id": "DELIV-002",
          "title": "Completion.",
          "text": "Completing or reopening a deliverable records the operator and the time, adds a campaign activity entry, and changes the campaign's derived completion count in the same commit."
        },
        {
          "id": "DELIV-003",
          "title": "Overdue.",
          "text": "An Open deliverable with a due date before the current date in the workspace timezone is marked overdue on campaign detail, in every lens and search result that contains it, and in CSV export. A deliverable due today is not overdue."
        },
        {
          "id": "MILE-001",
          "title": "Record.",
          "text": "An operator can create, edit, and delete a milestone. It belongs to exactly one campaign, requires a title and an owner, has a type of Launch, Review, Approval, Decision, Event, or Publish, has a status of Not started, On track, Watch, At risk, Blocked, or Complete that starts Not started, and has either a single date or a start and end date whose end does not fall before its start."
        },
        {
          "id": "MILE-002",
          "title": "Timeline lens.",
          "text": "As shipped, Timeline Lens contains every milestone of every non-archived campaign, ordered by date ascending using the start date for a range and then by campaign code. A range is rendered as a range with both dates and a point as a point with one date. Month scale renders one column per calendar month and Quarter scale one per quarter in the workspace timezone; switching scale changes only the columns, never the milestone set. Every item links to its campaign detail and to its milestone detail."
        },
        {
          "id": "MILE-003",
          "title": "Dependencies.",
          "text": "An operator can name existing milestones in any campaign as predecessors of a milestone. A predecessor set that would create a cycle is rejected with a validation error and changes nothing. When a milestone starts before a predecessor ends, both carry a dependency-conflict marker on milestone detail and in Timeline Lens, and setting the later milestone's status to On track while that marker stands requires a reason of at least one non-whitespace character, recorded on the milestone and in campaign activity. Changing one milestone's date never changes another's."
        },
        {
          "id": "MILE-004",
          "title": "Readiness request.",
          "text": "A readiness request is an in-app notification addressed to a milestone's owner, Outstanding until that owner acknowledges it. Within 15 minutes of 08:00 each day in the workspace timezone the shipped readiness automation creates exactly one Outstanding request for every milestone of type Launch that is not Complete, whose campaign is non-archived, and whose date falls within the next 5 days, and that has no Outstanding request. A repeated evaluation creates no duplicate."
        },
        {
          "id": "MILE-005",
          "title": "Readiness rescheduling.",
          "text": "Moving a Launch milestone's date out of the five-day window cancels its Outstanding request, and moving it inside the window creates one at the next daily evaluation. Completing or deleting the milestone, or archiving its campaign, cancels an Outstanding request. A milestone whose date has already passed receives no request."
        },
        {
          "id": "MILE-006",
          "title": "Overdue.",
          "text": "An incomplete milestone whose date, or whose range end, falls before the current date in the workspace timezone is marked overdue wherever it appears."
        },
        {
          "id": "RISK-001",
          "title": "Record.",
          "text": "An operator can create, edit, and delete a risk. It belongs to exactly one campaign, requires a title and an owner, has a severity of Low, Medium, High, or Critical, has an optional due date, carries a signal and a mitigation as free text of at most 2,000 characters each — the signal being the evidence that raised the risk — and has a workflow state."
        },
        {
          "id": "RISK-002",
          "title": "Lifecycle.",
          "text": "An open risk is Watching, In progress, Needs decision, or Escalated. Resolving it requires a resolution note of at least one non-whitespace character and records the operator and the time. Reopening a resolved risk returns it to Watching and keeps the previous resolution in activity."
        },
        {
          "id": "RISK-003",
          "title": "Escalation.",
          "text": "An operator can escalate an open risk with a reason. In one commit the escalation sets the risk's state to Escalated, records the operator, reason, and time, and applies the escalation policy; the response carries any decision the policy created. *Policy: `risk.escalation.v1`; default: escalating a risk of severity Critical creates one unresolved decision of priority Critical linked to that risk unless an unresolved decision is already linked to it, and notifies the campaign owner. Escalating a lower severity creates no decision.*"
        },
        {
          "id": "RISK-004",
          "title": "Isolation.",
          "text": "Escalating, updating, or resolving a risk changes that risk, the campaign activity entry ACT-001 requires, the campaign's derived rollups, and — when `risk.escalation.v1` fires — the one decision and one notification it creates. No other record changes; a linked decision changes only through `decision.resolution.v1`."
        },
        {
          "id": "RISK-005",
          "title": "Shipped lens.",
          "text": "As shipped and before any operator edit, Risks Lens contains exactly the open risks of severity High or Critical whose campaign is non-archived and whose launch date is no later than 30 days from today in the workspace timezone, so a campaign already past its launch date stays in the lens. It orders Critical before High, then earliest due date, then risks without a due date, then campaign code, and shows severity, state, owner, due date, signal, mitigation, and campaign."
        },
        {
          "id": "DEC-001",
          "title": "Record.",
          "text": "An operator can create, edit, and delete a decision. It belongs to exactly one campaign, links to at most one risk and to at most one decision it supersedes, requires a title and a requester, and has a priority of Critical, High, Medium, or Low, a due time, a recommendation, an impact statement, an optional assignee, and a state of Awaiting approval, In review, Approved, Rejected, or Cancelled. Awaiting approval and In review are unresolved; the other three are terminal."
        },
        {
          "id": "DEC-002",
          "title": "Queue.",
          "text": "Decision Queue contains exactly the unresolved decisions on non-archived campaigns. *Policy: `decision.queue-order.v1`; default order: overdue first, then priority Critical, High, Medium, Low, then earliest due time, then earliest creation time.*"
        },
        {
          "id": "DEC-003",
          "title": "Approval.",
          "text": "Approving an unresolved decision records the approver, the time, the outcome, and an optional rationale, adds a campaign activity entry, applies `decision.resolution.v1`, and removes the decision from Decision Queue, all in one commit."
        },
        {
          "id": "DEC-004",
          "title": "First outcome wins.",
          "text": "A request that approves, rejects, or cancels a decision is a mutation under VIEW-006. The first such outcome to commit is the decision's outcome; a later one against the older version returns 409 and changes nothing."
        },
        {
          "id": "DEC-005",
          "title": "Assignment.",
          "text": "Any operator can assign or reassign an unresolved decision to a current operator, which records the operator and the time and notifies the new assignee. Assignment changes no other field."
        },
        {
          "id": "DEC-006",
          "title": "Changes, rejection, and cancellation.",
          "text": "Requesting changes on an unresolved decision requires a rationale of at least one non-whitespace character, records the operator and the time, and sets the decision to In review. Rejecting or cancelling an unresolved decision also requires such a rationale and is a terminal outcome under DEC-004."
        },
        {
          "id": "DEC-007",
          "title": "Linked-risk effect.",
          "text": "A decision outcome changes its linked risk only through the resolution policy, and no other decision action changes a risk. *Policy: `decision.resolution.v1`; default: approving moves a linked risk from Needs decision or Escalated to In progress; requesting changes, rejecting, and cancelling leave it unchanged. No decision outcome resolves a risk.*"
        },
        {
          "id": "DEC-008",
          "title": "Finality.",
          "text": "A terminal decision cannot be edited into another outcome; such a request is rejected and changes nothing. An operator can create a new decision that supersedes it under DEC-001, and each decision links to the other on both details."
        },
        {
          "id": "LENS-001",
          "title": "Definition.",
          "text": "A lens has a name, a description, exactly one record type from Campaign, Risk, Milestone, or Decision, at most 20 conditions, a sort order, visible fields, and a display mode valid for its type: Grid for Campaign, Cards for Risk or Decision, Timeline or Agenda for Milestone. A lens with a mode its type does not allow is rejected on save."
        },
        {
          "id": "LENS-002",
          "title": "Preview.",
          "text": "Lens preview evaluates the unsaved definition against non-archived records, reports the exact matching count under VIEW-007 within 2 seconds, changes no record, and shows an explicit zero-results state."
        },
        {
          "id": "LENS-003",
          "title": "Save and share.",
          "text": "Saving a lens makes it available to every operator and records who saved it and when. Sharing produces a URL to the saved lens that requires an operator session; no lens URL is readable without one."
        },
        {
          "id": "LENS-004",
          "title": "Edit and delete.",
          "text": "An operator can edit a lens definition or delete the lens. Neither changes, archives, or deletes any record the lens displays or hides. A shipped lens cannot be deleted."
        },
        {
          "id": "LENS-005",
          "title": "Current results.",
          "text": "Opening or refreshing a lens re-evaluates it against non-archived records, and its result list, count, condition summary, and selected detail all describe that one evaluation."
        },
        {
          "id": "LENS-006",
          "title": "Shipped views.",
          "text": "Risks Lens and Timeline Lens ship as saved lenses. Operators can duplicate or edit them, and restoring either returns its definition to exactly the one in RISK-005 or MILE-002 without changing a record."
        },
        {
          "id": "LENS-007",
          "title": "Conditions.",
          "text": "A condition names one field of the lens's record type — campaign custom fields only on a Campaign lens — and one operator from equals, not equals, one of, contains, is empty, is not empty, before, after, and within the next N days, typed to that field: contains applies to text fields only, and the date operators to date fields only. Conditions combine with AND. A condition naming an unknown field, or an operator its field's type does not support, is rejected on preview and on save."
        },
        {
          "id": "ACT-001",
          "title": "Campaign activity.",
          "text": "Campaign activity is ordered newest first. It records every create, field change, archive, restore, and delete of the campaign and of its deliverables, milestones, risks, decisions, and comments, plus every import and every mutation made by an automation. Each entry carries its type, its subject's ID, the operator or the automation run that acted, the time, and for a field change the field name with its previous and new value. Campaign activity is the operator-visible view of the audit records in `BASE-OPS-005`: deleting a campaign removes this view, and those audit records survive with the campaign reference anonymised."
        },
        {
          "id": "ACT-002",
          "title": "Comments.",
          "text": "An operator can add a plain-text comment to a campaign, and it appears in campaign activity. A comment changes no field of any record and triggers no automation and no AI call. An operator can delete a comment, which leaves a deletion entry in activity."
        },
        {
          "id": "ACT-003",
          "title": "Dates and times.",
          "text": "Every timestamp in the API and the export is ISO 8601 in UTC. Campaign, deliverable, and milestone dates are calendar dates evaluated from the start of that day in the workspace timezone; decision due times are instants. Every relative time exposes its absolute time as its accessible name."
        },
        {
          "id": "ACT-004",
          "title": "Mentions.",
          "text": "A comment can mention a current operator, which notifies exactly that operator and changes no field of any record. A mention of a removed or erased operator renders as plain text and notifies nobody."
        },
        {
          "id": "AUTO-001",
          "title": "Definition.",
          "text": "An automation has a name, one trigger, an ordered list of actions, an enabled state, and a definition version that increments on every edit. A trigger is a product event declared in `seed.json → extensionPoints.events` or a schedule of at most one run per hour, and a shorter schedule is rejected on save. An action is one of `escalate-risk`, `create-decision`, `notify-operator`, `send-email`, `request-ai-analysis`, and `add-activity`, plus any command contributed through `jobs.commands.v1`."
        },
        {
          "id": "AUTO-002",
          "title": "Commit boundary.",
          "text": "An event automation is dispatched within 10 seconds of the mutation that raised its event committing. A rolled-back mutation dispatches nothing. A disabled automation runs on neither events nor schedules and records no run."
        },
        {
          "id": "AUTO-003",
          "title": "One run per trigger.",
          "text": "One run exists per automation, definition version, and trigger event or scheduled instant; transport retries are attempts inside that run. Under `BASE-OPS-003`, a redelivered message reuses each action's idempotency key, so no decision, notification, analysis, or activity entry is created twice."
        },
        {
          "id": "AUTO-004",
          "title": "Definition changes.",
          "text": "A run records the definition version that created it. Editing an automation affects only later triggers. Disabling one cancels queued runs that have not started, recording each as Cancelled with no action executed."
        },
        {
          "id": "AUTO-005",
          "title": "Loop protection.",
          "text": "Every mutation made by an automation carries the ID of the run that made it, and a run inherits its cause's chain of run IDs. An event whose chain already contains a given automation starts no new run of it, and the suppression is recorded on the causing run. A chain is at most 10 runs deep, and an eleventh is recorded as Suppressed and not dispatched. Two runs triggered by the same event may finish in either order and neither waits for the other."
        },
        {
          "id": "AUTO-006",
          "title": "Run history.",
          "text": "Every run records its automation version, trigger, subject, queued, started, and finished times, each attempted action with its result, the number of records it affected, and the error code and message of any failure. Run history is retained in full and paged under VIEW-007, and Automation Monitor shows each automation's success rate as finished successful runs divided by finished runs over the trailing 30 days, stating that window."
        },
        {
          "id": "AUTO-007",
          "title": "Transport retry.",
          "text": "An action that fails with a transport error is retried at 1, 5, 15, 30, and 60 minutes after its first failure. A retry re-executes only actions that have not completed and reuses their idempotency keys. When the fifth retry fails the run is marked Failed."
        },
        {
          "id": "AUTO-008",
          "title": "Failure that retrying cannot fix.",
          "text": "A validation error, a policy rejection, a schema failure, or a provider refusal fails the run immediately with no retry. Every Failed run is listed in Automation Monitor with its error code and message under `BASE-OPS-004`."
        },
        {
          "id": "AUTO-009",
          "title": "Manual retry.",
          "text": "An operator can retry a Failed run. The retry is a new run linked to the original by `retryOf` that reuses the original action idempotency keys, so completed actions do not repeat, and the original run stays Failed and unchanged."
        },
        {
          "id": "AUTO-010",
          "title": "Fan-out.",
          "text": "A mutation that changes many records at once — a bulk edit under CAMP-013 or a committed import under IMPORT-003 — dispatches at most one run per enabled automation, carrying the affected record IDs, rather than one run per record."
        },
        {
          "id": "AUTO-011",
          "title": "Shipped automations.",
          "text": "The seed ships four automations. Critical risk escalation triggers when a risk's severity becomes Critical while the risk is open and not already Escalated, and its one action escalates that risk under RISK-003; it is enabled by default. Launch readiness runs on the schedule in MILE-004 and is enabled by default; its lead time, 5 days by default, and its evaluation hour, 08:00 by default, are editable in settings. Campaign brief triggers when a campaign is created or updated, at most once per campaign per hour, skipped when nothing in its AI-002 context changed since the last brief; it is enabled by default and dispatched only while AI is enabled under SET-002. The weekly portfolio digest runs on the schedule in NOTIFY-004 and is disabled by default. No other parameter of a shipped automation is editable, and each can be disabled."
        },
        {
          "id": "NOTIFY-001",
          "title": "In-app events.",
          "text": "An operator receives an in-app notification when a decision is assigned to them, when they are mentioned in a comment, when a Critical risk is escalated on a campaign they own, when a readiness request is created for them, and when an automation run they started fails. A Failed run that no operator started notifies the owner of the campaign it acted on, or every operator when the run has no campaign subject."
        },
        {
          "id": "NOTIFY-002",
          "title": "Email delivery.",
          "text": "Every in-app notification has an email counterpart routed by the delivery policy through `mail.sender.v1` against the workspace quiet hours in settings, which default to 18:00–08:00 in the workspace timezone. Authentication email and the weekly digest are never held. *Policy: `notification.delivery.v1`; default: a Critical risk escalation or a decision assignment is emailed within 60 seconds at any hour; every other email raised during quiet hours is held and sent within 60 seconds of quiet hours ending, as one message per operator.*"
        },
        {
          "id": "NOTIFY-003",
          "title": "Delivery failure.",
          "text": "Every outbound email has a delivery record linked to its notification and, when an automation sent it, to that run. A failed send, including an asynchronous bounce, is retried on the AUTO-007 schedule and then marked failed on both the delivery record and the notification, where settings lists it. The in-app notification stays available."
        },
        {
          "id": "NOTIFY-004",
          "title": "Digest.",
          "text": "When enabled, the weekly digest is sent within 15 minutes of 09:00 each Monday in the workspace timezone to the configured recipients, defaulting to every current operator, and once per recipient address however often that address appears. It itemises non-archived campaigns, launches in the next 30 days, open High and Critical risks, unresolved decisions, and runs that failed in the previous 7 days."
        },
        {
          "id": "NOTIFY-005",
          "title": "Inbox.",
          "text": "The notification inbox lists an operator's notifications newest first and the navigation shell shows their unread count. A notification is unread for an operator until that operator opens it, an operator can mark one or all of their notifications read, and one operator's read state never changes another's. A notification whose subject has been archived or deleted is shown as unavailable rather than linking to a missing record."
        },
        {
          "id": "NOTIFY-006",
          "title": "One per event.",
          "text": "An operator receives at most one in-app notification and one email for a single product event however many of its roles they hold, is never notified of an action they took themselves, and a repeat of the same event on the same subject within 60 minutes updates the existing unread notification instead of creating another."
        },
        {
          "id": "NOTIFY-007",
          "title": "Muting.",
          "text": "In settings each operator can mute any notification kind for themselves. Muting stops the email for that kind for that operator only; the in-app notification is still created, and delivery to every other operator is unchanged."
        },
        {
          "id": "NOTIFY-008",
          "title": "Overdue decisions.",
          "text": "A decision still unresolved 15 minutes after its due time notifies its assignee once, or its requester once when it has no assignee. Moving the due time later re-arms that notification once for the new time."
        },
        {
          "id": "AI-001",
          "title": "Analyses.",
          "text": "An operator, or the shipped campaign-brief automation, can request a campaign brief, a risk readout, or a lens proposal. Each result is stored as an immutable AI analysis linked to its campaign, to the lens it was invoked from, or to its lens draft, marked as AI-generated wherever it appears, stamped with the time it was produced, and citing the ID and version of every entity in its input."
        },
        {
          "id": "AI-002",
          "title": "Context.",
          "text": "*Policy: `ai.campaign-context.v1`; default: a campaign brief receives the campaign's standard fields and its linked deliverables, milestones, open risks, and unresolved decisions; a risk readout receives the result set of the lens it was invoked from, capped at 200 risks; a lens proposal receives the supported field definitions and the operator's prompt. Comment text and custom-field values go to none of the three.*"
        },
        {
          "id": "AI-003",
          "title": "Human control.",
          "text": "AI output never changes a field of any record: it never completes a deliverable, moves a milestone, escalates or resolves a risk, resolves a decision, saves a lens, or sends a notification. A lens proposal stays an editable draft until an operator saves it."
        },
        {
          "id": "AI-004",
          "title": "Provider and failure.",
          "text": "AI calls go through `ai.provider.v1` with a 60-second timeout. A timeout, a response that fails its schema, or an unconfigured or unavailable provider stores no analysis and changes no field of any record. An operator-initiated request shows the provider's error code and message where it was requested; an automation-initiated one fails that run under AUTO-008. Every other automation, notification, mutation, and read proceeds unaffected."
        },
        {
          "id": "AI-005",
          "title": "Usage.",
          "text": "Every AI call records its feature, model, input and output token counts, reported cost, subject, requesting operator or automation run, duration, and result. Settings shows call counts, token counts, and cost by day and by feature."
        },
        {
          "id": "AI-006",
          "title": "Daily ceiling.",
          "text": "Settings holds a daily ceiling on AI calls, 200 by default, resetting at 00:00 in the workspace timezone. A call that would exceed it is refused with an error naming the ceiling, stores no analysis, and fails any run that requested it under AUTO-008. Non-AI work is unaffected."
        },
        {
          "id": "IMPORT-001",
          "title": "CSV preview.",
          "text": "An operator can import a campaign, deliverable, or milestone CSV within the upload limits under `BASE-INPUT-003` and the row limit in `seed.json → limits`. The file must be UTF-8, comma-delimited, and carry a header row; a byte-order mark is accepted and anything else is rejected with a parse error. Import requires an explicit mapping from columns to fields and, before any record changes and within 60 seconds at the row limit, shows the number of creates, updates, and unchanged rows, every row error, and for each updated row the fields that will change with their stored and incoming values."
        },
        {
          "id": "IMPORT-002",
          "title": "Merge keys.",
          "text": "A campaign row creates or updates a campaign by its unique code; a deliverable or milestone row creates or updates by campaign code plus title, and is a row error when that campaign is not already in the workspace. A merge key repeated inside the file, and a reference to an owner or select value that does not exist, are row errors. A value that is empty or only whitespace never overwrites a stored value."
        },
        {
          "id": "IMPORT-003",
          "title": "Commit.",
          "text": "Confirm is available only when every row is valid; one invalid row blocks the whole file and there is no partial import. Confirming commits every previewed row in one transaction, records an import record, and writes one activity entry on every campaign it created, updated, or whose deliverables or milestones it changed; any failure rolls back every row. A committed import cannot be rolled back, which is why IMPORT-001 shows every field it will change."
        },
        {
          "id": "IMPORT-004",
          "title": "Replay and staleness.",
          "text": "Preview returns a token that expires 30 minutes after it is issued and that confirm requires, together with an idempotency key. A record matched by the preview that changed before confirm makes confirm return 409 and require a new preview. Replaying a confirm with the same key within 24 hours returns the original result and creates no second record and no second activity entry."
        },
        {
          "id": "IMPORT-005",
          "title": "Stored file.",
          "text": "The uploaded file is stored with its import record and is downloadable by any operator. Deleting the import record deletes the file within 5 minutes."
        },
        {
          "id": "SET-001",
          "title": "Settings area.",
          "text": "Settings is one operator-only area with sections for the workspace (name, timezone, currency, quiet hours), operators, notification preferences, custom-field definitions, automations and digest recipients, AI, imports and failed email deliveries, and current usage against every limit in `seed.json → limits`. Changing a setting records the operator and the time and applies to work dispatched after the change; work already dispatched is unaffected."
        },
        {
          "id": "SET-002",
          "title": "AI configuration.",
          "text": "AI is off until an operator enables it in settings while the configured `ai.provider.v1` adapter has every environment value in `seed.json → env`. While AI is off, an AI request is rejected with a message naming what is missing, the campaign-brief automation is not dispatched, and every other feature works unchanged."
        },
        {
          "id": "SET-003",
          "title": "First run and sample data.",
          "text": "A new deployment starts with the operator configured at setup, the configured timezone and currency, the two shipped lenses, the four shipped automations, and no campaigns, and every surface shows the empty state in VIEW-004. An operator can install and remove sample data in one action from settings; removal deletes only records the sample created and is refused, naming them, once any of those records has been edited."
        },
        {
          "id": "DATA-001",
          "title": "Export contents.",
          "text": "In addition to the archive guarantees in `BASE-DATA-001`, the export contains every workspace setting, operator, campaign, tag, custom-field definition and value, deliverable, milestone and dependency, risk, decision and supersede link, lens, comment, activity entry, notification, delivery record, automation definition and run, import record, AI analysis, and AI usage record as JSON, and every stored import file in its original form."
        },
        {
          "id": "DATA-002",
          "title": "CSV export.",
          "text": "An operator can export the campaign grid or any lens or queue result as CSV containing exactly the visible fields in their displayed order and exactly the records the current conditions select, with dates in the workspace timezone and money in the workspace currency, naming the view, the currency, and the instant it was taken. It is a one-way download and creates no sync relationship with any external system."
        },
        {
          "id": "DATA-003",
          "title": "Capacity limits.",
          "text": "A create, invitation, or import that would take the workspace past a limit in `seed.json → limits` is rejected with 409 naming the limit, its value, and the current count, and changes nothing. No record, row, or field is silently dropped or trimmed."
        }
      ],
      "hasReadme": false,
      "hasBaseline": true,
      "nonGoals": [
        "multiple workspaces or tenants",
        "roles or granular permissions",
        "arbitrary tables, schema design, or user-defined formula and rollup fields",
        "general app building, scripting, or generic webhooks",
        "public or embeddable views",
        "bidirectional Airtable, spreadsheet, or SaaS sync",
        "automatic dependency rescheduling or critical-path optimisation",
        "resource or capacity planning",
        "general task management",
        "autonomous AI mutations or outbound communication",
        "notification channels other than in-app and email",
        "file attachments on records",
        "editorial content calendars and customer-feedback tracking",
        "native mobile applications"
      ],
      "externals": [
        {
          "name": "transactional-mail",
          "required": true,
          "requiredWhen": "",
          "reason": "Magic-link sign-in and the operator notifications in NOTIFY-002 must reach email addresses outside the deployment.",
          "data": [
            "recipient email",
            "magic-link token or notification subject and body"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        },
        {
          "name": "ai-provider",
          "required": false,
          "requiredWhen": "",
          "reason": "The campaign briefs, risk readouts, and lens proposals in AI-001 require a model; with AI off under SET-002 the rest of the product is unchanged.",
          "data": [
            "context allowed by ai.campaign-context.v1",
            "operator request"
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "estimated on 2026-08-31 from published Cloudflare pricing, not measured against a running deployment",
        "currency": "USD",
        "estimate": "$0 per month on Workers Free for the default deployment, excluding usage charged by the selected mail and AI providers",
        "assumptions": "Static assets, up to 100,000 Worker requests/day, 5 million D1 rows read/day, 100,000 D1 rows written/day, 5 GB total D1 storage, 10,000 Queue operations/day, five Cron Triggers, SQLite-backed Durable Objects (including alarms), and the R2 Standard free tier are available without a Workers Paid subscription. The declared record-count limits are capacity targets and do not by themselves require Workers Paid. Upgrade before production traffic or processing crosses a Workers Free hard cap—most likely 100,000 Worker requests/day, 5 million D1 rows read/day, 100,000 D1 rows written/day, 5 GB D1 storage, or 10,000 Queue operations/day. R2 usage beyond 10 GB-month, 1 million Class A operations, or 10 million Class B operations/month is billed separately. Adapter usage varies by provider.",
        "sources": [
          "https://developers.cloudflare.com/workers/platform/pricing/",
          "https://developers.cloudflare.com/workers/platform/limits/",
          "https://developers.cloudflare.com/d1/platform/pricing/",
          "https://developers.cloudflare.com/r2/pricing/",
          "https://developers.cloudflare.com/queues/platform/pricing/",
          "https://developers.cloudflare.com/durable-objects/platform/pricing/"
        ]
      },
      "extensionPoints": [
        "app.routes.v1",
        "app.pages.v1",
        "app.navigation.v1",
        "settings.sections.v1",
        "jobs.commands.v1",
        "schedules.cron.v1",
        "queues.consumers.v1",
        "operator.management.v1",
        "campaign.lifecycle.v1",
        "campaign.health.v1",
        "risk.escalation.v1",
        "decision.queue-order.v1",
        "decision.resolution.v1",
        "notification.delivery.v1",
        "ai.campaign-context.v1",
        "campaign.created.v1",
        "campaign.updated.v1",
        "campaign.archived.v1",
        "campaign.restored.v1",
        "campaign.deleted.v1",
        "deliverable.completed.v1",
        "deliverable.reopened.v1",
        "milestone.updated.v1",
        "risk.created.v1",
        "risk.updated.v1",
        "risk.escalated.v1",
        "risk.resolved.v1",
        "decision.requested.v1",
        "decision.assigned.v1",
        "decision.resolved.v1",
        "lens.saved.v1",
        "comment.created.v1",
        "import.committed.v1",
        "operator.removed.v1",
        "automation.run-failed.v1",
        "navigation.after.v1",
        "dashboard.metrics.after.v1",
        "campaign.grid.toolbar.after.v1",
        "campaign.row.actions.v1",
        "campaign.detail.after.v1",
        "campaign.detail.sidebar.after.v1",
        "risk.card.actions.v1",
        "decision.detail.after.v1",
        "lens.builder.after.v1",
        "automation.card.after.v1",
        "settings.panel.after.v1",
        "mail.sender.v1",
        "ai.provider.v1"
      ],
      "limits": {
        "status": "ui-ported-estimates-not-load-tested",
        "operators": 25,
        "campaigns": 50000,
        "campaignCustomFields": 50,
        "deliverables": 500000,
        "milestones": 250000,
        "risks": 250000,
        "decisions": 250000,
        "savedLenses": 200,
        "lensConditions": 20,
        "automations": 100,
        "bulkSelectionRecords": 500,
        "csvImportBytes": 10485760,
        "csvImportRows": 10000,
        "csvImportMimeTypes": [
          "text/csv",
          "application/vnd.ms-excel",
          "text/plain"
        ],
        "requestBodyBytes": 1048576,
        "publicRequestsPerIpPerMinute": {
          "/": 120,
          "/health": 120,
          "/signin": 120,
          "/auth/request": 20,
          "/auth/callback": 20,
          "/assets/": 600,
          "/favicon.ico": 120
        }
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "ui-ported-not-yet-audited"
      },
      "landing": "assets/seeds/cradle/01-landing.png",
      "shots": [
        "assets/seeds/cradle/01-landing.png",
        "assets/seeds/cradle/02-workspace-home.png",
        "assets/seeds/cradle/03-campaign-grid.png",
        "assets/seeds/cradle/04-risks-lens.png",
        "assets/seeds/cradle/05-timeline-lens.png",
        "assets/seeds/cradle/06-settings.png"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/cradle",
        "release": "0.0.0-bootstrap",
        "manifestSha256": "6e6beb85d134dab9cd37c75680c0a69bc6afd405d29e966d77e7e009d1e96a46"
      }
    },
    {
      "id": "ferry",
      "authoring": {
        "mode": "edition-owned",
        "agentSource": "AGENTS.md",
        "id": "runeditrun/ferry",
        "parent": {
          "repository": "https://github.com/runeditrun/sales-ferry.git",
          "revision": "260ab4993b3b8cf69a137ca287b549cbd71b0680"
        },
        "toolchain": {
          "id": "runeditrun/edition-authoring",
          "version": "1.1.0",
          "sha256": "a13d7d0bcf174618923ca5d1134575c149ea6b57c89d1640c03c1ebf091ab372"
        },
        "components": [
          {
            "id": "runeditrun/base",
            "role": "foundation",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/deployment/health.mjs",
                "scripts/deployment/base-secret-json.mjs",
                "tools/seed-schema/SEED.schema.json",
                "tools/seed-schema/json-schema.mjs",
                "tools/seed-schema/validate.mjs",
                "tools/seed-schema/RELEASE.schema.json",
                "scripts/operations.mjs"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "bc27ecf55693a68bfa7e8a6aba9fe3ce8883bcfe",
                "path": ".",
                "paths": [
                  "deployment/health.mjs",
                  "deployment/secret-json.mjs",
                  "schema/SEED.schema.json",
                  "schema/json-schema.mjs",
                  "schema/validate.mjs",
                  "schema/RELEASE.schema.json",
                  "operations/operations.mjs"
                ]
              },
              "mappings": [
                {
                  "path": "scripts/deployment/health.mjs",
                  "originPath": "deployment/health.mjs"
                },
                {
                  "path": "scripts/deployment/base-secret-json.mjs",
                  "originPath": "deployment/secret-json.mjs"
                },
                {
                  "path": "tools/seed-schema/SEED.schema.json",
                  "originPath": "schema/SEED.schema.json"
                },
                {
                  "path": "tools/seed-schema/json-schema.mjs",
                  "originPath": "schema/json-schema.mjs"
                },
                {
                  "path": "tools/seed-schema/validate.mjs",
                  "originPath": "schema/validate.mjs"
                },
                {
                  "path": "tools/seed-schema/RELEASE.schema.json",
                  "originPath": "schema/RELEASE.schema.json"
                },
                {
                  "path": "scripts/operations.mjs",
                  "originPath": "operations/operations.mjs"
                }
              ]
            }
          },
          {
            "id": "runeditrun/sales",
            "role": "family",
            "source": {
              "kind": "local",
              "paths": [
                "src/family/sales",
                "composition/families/sales/README.md",
                "composition/families/sales/LICENSE",
                "tests/composition/sales",
                "scripts/test-composition.mjs"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/sales.git",
                "revision": "b3b195939af1cdfdef3008d878c6cb945e3e0cd6",
                "path": ".",
                "paths": [
                  "core/src",
                  "core/README.md",
                  "core/LICENSE",
                  "core/tests",
                  "core/test-components.mjs"
                ]
              },
              "mappings": [
                {
                  "path": "src/family/sales",
                  "originPath": "core/src"
                },
                {
                  "path": "composition/families/sales/README.md",
                  "originPath": "core/README.md"
                },
                {
                  "path": "composition/families/sales/LICENSE",
                  "originPath": "core/LICENSE"
                },
                {
                  "path": "tests/composition/sales",
                  "originPath": "core/tests"
                },
                {
                  "path": "scripts/test-composition.mjs",
                  "originPath": "core/test-components.mjs"
                }
              ]
            }
          },
          {
            "id": "runeditrun/sendgrid-email",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/sendgrid-email",
                "composition/modules/sendgrid-email/README.md",
                "composition/modules/sendgrid-email/LICENSE",
                "tests/composition/sendgrid-email"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "bc27ecf55693a68bfa7e8a6aba9fe3ce8883bcfe",
                "path": ".",
                "paths": [
                  "modules/sendgrid-email/src",
                  "modules/sendgrid-email/README.md",
                  "modules/sendgrid-email/LICENSE",
                  "modules/sendgrid-email/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/sendgrid-email",
                  "originPath": "modules/sendgrid-email/src"
                },
                {
                  "path": "composition/modules/sendgrid-email/README.md",
                  "originPath": "modules/sendgrid-email/README.md"
                },
                {
                  "path": "composition/modules/sendgrid-email/LICENSE",
                  "originPath": "modules/sendgrid-email/LICENSE"
                },
                {
                  "path": "tests/composition/sendgrid-email",
                  "originPath": "modules/sendgrid-email/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/ferry",
            "role": "edition",
            "source": {
              "kind": "local",
              "paths": [
                "src/core",
                "src/ext",
                "src/routes",
                "migrations"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/sales.git",
                "revision": "b3b195939af1cdfdef3008d878c6cb945e3e0cd6",
                "path": ".",
                "paths": [
                  "editions/ferry/src/core",
                  "editions/ferry/src/ext",
                  "editions/ferry/src/routes",
                  "editions/ferry/migrations"
                ]
              },
              "mappings": [
                {
                  "path": "src/core",
                  "originPath": "editions/ferry/src/core"
                },
                {
                  "path": "src/ext",
                  "originPath": "editions/ferry/src/ext"
                },
                {
                  "path": "src/routes",
                  "originPath": "editions/ferry/src/routes"
                },
                {
                  "path": "migrations",
                  "originPath": "editions/ferry/migrations"
                }
              ]
            }
          },
          {
            "id": "runeditrun/base-onboarding",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/base-onboarding"
              ]
            }
          },
          {
            "id": "runeditrun/ferry-onboarding",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/installer",
                "scripts/install.mjs",
                "scripts/installer-fill.mjs",
                "public/setup/ferry.svg"
              ]
            }
          }
        ],
        "manifestSha256": "dfe4705a609d41796268a4afaf744a8643f55866d6877f1d51c1b417a29cff25",
        "lockSha256": "e6343ec2ba58030a11ffa3382c97394fb852aa04966295e3b60fc064a1545fe3"
      },
      "name": "Ferry",
      "dir": "sales-ferry",
      "category": "sales",
      "categoryLabel": "Sales",
      "spine": {
        "id": "sales",
        "record": "deal over a contact and company graph"
      },
      "composition": {
        "base": "base",
        "family": "sales",
        "edition": "ferry",
        "modules": [
          "sendgrid-email"
        ]
      },
      "replaces": [
        {
          "name": "HubSpot CRM",
          "edition": null
        },
        {
          "name": "Pipedrive",
          "edition": null
        }
      ],
      "version": "0.0.0",
      "oneLiner": "An intervention-first sales pipeline for one business that turns credible deal-risk evidence into a human-controlled next move and a durable buyer…",
      "summary": "An intervention-first sales pipeline for one business that turns credible deal-risk evidence into a human-controlled next move and a durable buyer commitment.",
      "scope": "One business, one configurable pipeline, equal operators; ownership routes work but does not restrict access.",
      "maturity": "prototype-contract",
      "clauseCount": 59,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Owner identity plate.",
          "text": "`/` presents the configured organization and owner name, one configured landing line, and a sign-in link. It may show the configured static Ferry credit. It contains no marketing claims, product metrics, testimonials, or telemetry."
        },
        {
          "id": "TEAM-001",
          "title": "Equal operators.",
          "text": "Every operator can see and act on every business record. Ownership and assignment route work; they never restrict access."
        },
        {
          "id": "TEAM-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. The operator configured at setup can be removed only by themselves. *Policy: `operator.management.v1`; default: any operator may invite or remove.*"
        },
        {
          "id": "TEAM-003",
          "title": "Business settings.",
          "text": "Settings hold one IANA business time zone and one default currency (PIPE-010). Every due time, quiet-hours window, day boundary, and Insights date range is evaluated in that time zone. Operators do not have individual time zones or individual currencies."
        },
        {
          "id": "PIPE-001",
          "title": "One configurable pipeline.",
          "text": "The business has one ordered set of open stages. Operators can add, rename, reorder, and retire stages; retiring a non-empty stage requires moving its open deals first. *Policy: `pipeline.stages.v1`; default: Discovery, Qualified, Proposal, and Negotiation.*"
        },
        {
          "id": "PIPE-002",
          "title": "Deal identity.",
          "text": "Creating a deal requires a title and at least one linked person or organization. Each open deal has exactly one stage; value, currency, expected close date, owner, and primary contact may be recorded, and any missing value is shown as `Not set` rather than invented."
        },
        {
          "id": "PIPE-003",
          "title": "Decision board.",
          "text": "The active board shows every open deal in its stage. Each stage shows a derived deal count and its total value under PIPE-010; each card shows title, account or contact, value, health, and an explicit next-action state of overdue, no next action, due today, or a future due time."
        },
        {
          "id": "PIPE-004",
          "title": "Work ordering.",
          "text": "The default board order is overdue action, no next action, due today, then future action; ties use due time, expected close date, then deal ID. Operators can filter by owner, health, stage, and activity state without changing the records included in other operators' views."
        },
        {
          "id": "PIPE-005",
          "title": "Stage movement.",
          "text": "An operator can move an open deal to any configured open stage. A stage may be configured with named exit conditions; each condition is satisfied by one recorded thing — an activity of a named type marked done, a required relationship role filled (REL-002), an open or completed customer commitment, or a manually attested fact (EVID-001). When a target stage's exit conditions are not all satisfied, Ferry names each unsatisfied condition and offers keep stage, record the missing evidence, or move anyway with a required reason. An override records the reason and the unsatisfied conditions in Deal Story and never marks a condition satisfied. *Policy: `deal.stage-transition.v1`; default: no stage has exit conditions, so every move proceeds without a prompt. A replacement may add, remove, or change conditions; it cannot remove the override reason requirement or suppress the override record.*"
        },
        {
          "id": "PIPE-006",
          "title": "Won and lost.",
          "text": "Marking a deal won or lost records the outcome, the actor, and the outcome time, and closes the deal (PIPE-011). Lost requires a reason. Open activities and open commitments on the deal keep their state and due times but stop producing reminders (ACT-004) and briefs (INT-001) while the deal is closed. *Policy: `deal.close-reason.v1`; default: any non-empty free-text reason for lost, none required for won.*"
        },
        {
          "id": "PIPE-007",
          "title": "Historical truth.",
          "text": "Changes to stage, value, currency, expected close date, owner, relationships, and outcome append an actor-attributed before/after event to Deal Story. Editing current fields never rewrites or removes those historical events."
        },
        {
          "id": "PIPE-008",
          "title": "Concurrent edits.",
          "text": "A mutation based on a stale record version returns a conflict with the current values and changes nothing. The operator can review and deliberately reapply their edit; Ferry never silently overwrites newer work."
        },
        {
          "id": "PIPE-009",
          "title": "Distinct time signals.",
          "text": "Every open deal records and displays four separate times, each with its own date and never merged into one number: the last recorded buyer interaction, the last recorded customer commitment, the due time of the next planned activity, and the time the deal entered its current stage. Editing a deal's fields, opening the deal, or sending an operator message changes none of them; only the underlying buyer, commitment, activity, or stage record does. Stage entry time changes only on a stage change and survives closing and reopening."
        },
        {
          "id": "PIPE-010",
          "title": "Currency.",
          "text": "A deal's value is recorded in the business default currency (TEAM-003) unless an operator selects another ISO 4217 currency for that deal; changing a deal's currency never converts its value. Ferry retrieves no exchange rates. An operator may record a manual rate for a currency against the default. A total or ranking that spans currencies uses only operator-recorded rates, states that it is converted, and names each rate and the date it was recorded; currencies with no recorded rate are excluded from that figure and listed with their own separate totals."
        },
        {
          "id": "PIPE-011",
          "title": "Closed deals stay reachable.",
          "text": "Won and lost are outcomes, not stages: neither appears as a board column. A won or lost deal keeps its complete Deal Story, activities, commitments, evidence, and messages. It is excluded from the board, from Focus, and from every open-deal figure in Insights; it is included in global search (SEARCH-001), CSV export (EXPORT-001), and the portable archive. Ferry assigns no health state to a closed deal."
        },
        {
          "id": "PIPE-012",
          "title": "Reopening.",
          "text": "Reopening a closed deal requires selecting an open stage and appends a reopening event; the prior outcome, its reason, and its time remain in Deal Story. Activities and commitments suspended by PIPE-006 resume producing reminders and briefs with their original due times."
        },
        {
          "id": "REL-001",
          "title": "People and organizations.",
          "text": "People and organizations are distinct records linked bidirectionally to their deals, activities, commitments, evidence, and messages. A person may belong to one organization and may hold one or more named roles on a deal."
        },
        {
          "id": "REL-002",
          "title": "Relationship coverage.",
          "text": "Deal Story shows, for each person linked to the deal, their role, the operator who owns that relationship, and the date of the most recent recorded interaction with them. Coverage for the deal is derived as complete when every role required at the deal's current stage is filled by a linked person, gap when at least one required role is unfilled, and critical gap when a role that became required at a stage before the deal's current one is still unfilled; every unfilled required role is named. *Policy: `relationship.required-roles.v1`; default: economic buyer and champion for every open deal, plus procurement and security reviewer from Proposal onward.*"
        },
        {
          "id": "REL-003",
          "title": "Duplicate safety.",
          "text": "A trimmed, case-insensitive non-empty email address identifies at most one person, and a trimmed, case-insensitive name identifies at most one organization. A create or import collision surfaces the existing record and does not merge or overwrite it; similar person names, phone numbers, and similar organization names are warnings only."
        },
        {
          "id": "REL-004",
          "title": "Manual merge.",
          "text": "Before merging two people, or two organizations, Ferry previews every field conflict and every reparented deal, activity, commitment, evidence item, person, and message. The operator chooses the surviving values; the merge applies atomically and appends an audit event, or changes nothing on failure. Merging is never performed automatically."
        },
        {
          "id": "REL-005",
          "title": "Contact and organization lists.",
          "text": "Operators can list people and organizations and filter them by organization, deal role, and relationship owner. Each person row shows their role, relationship owner, the date of the most recent recorded interaction, and their linked deals. Lists are paginated and state the total number of matching records; a displayed count never disagrees with the set it summarises."
        },
        {
          "id": "ACT-001",
          "title": "Activity lifecycle.",
          "text": "A call, meeting, task, email follow-up, or deadline has a subject, owner, due time evaluated in the business time zone (TEAM-003), and links to a deal plus optional people and organization. Its stored state is planned, done, or cancelled; overdue is derived when a planned activity's due time passes."
        },
        {
          "id": "ACT-002",
          "title": "Completion truth.",
          "text": "Completing an activity records the actual completion time once and preserves its original due time. Repeating the completion operation is a no-op. Completion prompts the operator to create a next activity or record why none exists, but never creates one or changes deal health by itself."
        },
        {
          "id": "ACT-003",
          "title": "Daily work.",
          "text": "Operators can list and filter activities by overdue, today, future, owner, type, and linked deal. Completing, rescheduling, cancelling, or reassigning an activity updates the relevant queue immediately while its prior state remains in Deal Story."
        },
        {
          "id": "ACT-004",
          "title": "Reminders and quiet hours.",
          "text": "In-app reminders appear when a planned activity becomes due and again when it becomes overdue. Email reminders are opt-in and are sent within 5 minutes of that transition, or within 5 minutes of the end of quiet hours if the transition falls inside them. A reminder is sent once per due-state transition and is cancelled when the activity is completed, cancelled, rescheduled, or reassigned. An operator-initiated send (MAIL-001) is never delayed by quiet hours. *Policy: `activity.reminder.v1`; default: in-app reminders on, email reminders off, quiet hours 19:00 to 08:00 in the business time zone.*"
        },
        {
          "id": "COMMIT-001",
          "title": "Qualified commitment.",
          "text": "A customer commitment belongs to one deal and records a named customer person, a concrete promised outcome, a due time, the operator who recorded it, and either a linked source event or an explicit manual attestation. An internal task or an operator's own promise is not a customer commitment."
        },
        {
          "id": "COMMIT-002",
          "title": "Lifecycle.",
          "text": "A commitment is open, completed, or cancelled; overdue is derived when an open commitment's due time passes. Completing, revising, or cancelling one appends the actor, time, and prior values; revisions never rewrite the original promise."
        },
        {
          "id": "COMMIT-003",
          "title": "Canonical visibility.",
          "text": "The same commitment record appears in Focus, Deal Story, the daily work queue, and Insights. Updating it in any surface produces the same resulting state everywhere."
        },
        {
          "id": "COMMIT-004",
          "title": "Idempotent outcomes.",
          "text": "Repeating the same completion, cancellation, or revision request produces no duplicate event and no second health assessment."
        },
        {
          "id": "EVID-001",
          "title": "Facts.",
          "text": "A fact is an attributable, timestamped record of a customer or operator event. Every fact shown in an Intervention Brief links to its source event; manually recorded facts identify their recorder and are labelled manual."
        },
        {
          "id": "EVID-002",
          "title": "Inferences.",
          "text": "An inference is stored and displayed separately from facts, names the facts it relies on, and can be corrected or dismissed without altering those facts. Inferred content is never presented as customer confirmation."
        },
        {
          "id": "STORY-001",
          "title": "Canonical context.",
          "text": "Deal Story presents the current commercial fields, relationships, commitments, facts, inferences, activities, outbound messages, health assessments, and attributed history for one deal in chronological order."
        },
        {
          "id": "STORY-002",
          "title": "Record correction.",
          "text": "Correcting a current field, manual fact, or inference appends the correction and actor to Deal Story. Source events and previously issued health assessments remain visible as historical context."
        },
        {
          "id": "INT-001",
          "title": "Focus is the default.",
          "text": "After sign-in, Focus opens as a projection over canonical deal records, not a separate task store. It shows one expanded Intervention Brief and no more than the next two ranked briefs; a healthy queue states when no intervention needs action and still shows today's commitments."
        },
        {
          "id": "INT-002",
          "title": "Explainable health.",
          "text": "Every open deal has exactly one current state — On track, Watching, Slipping, At risk, or Not actionable — and a rationale linked to the contributing records. The states are evaluated in this order, and every open deal matches one. Not actionable is set by an operator under INT-007. Otherwise, by default: Slipping means an open commitment or the next planned activity is overdue; At risk means Slipping with an expected close date inside 30 days and a credible intervention available (INT-011); On track means nothing is overdue, the deal has an open qualified commitment, and a planned next activity is due no later than that commitment; Watching is every remaining open deal — nothing is overdue, but a stage exit condition (PIPE-005) or required relationship role (REL-002) is unfilled, or the deal has no open qualified commitment, or it has no planned next activity. Record-update time alone never changes health. *Policy: `deal.health-assessment.v1`; default: the conditions above. A replacement may change those conditions and thresholds but may not add a state outside this list, may not leave an open deal without a state, and may not make an outbound message, a record edit, or elapsed time alone produce On track (INT-008).*"
        },
        {
          "id": "INT-003",
          "title": "Explainable ranking.",
          "text": "Focus ranks active briefs by recoverable deal value (PIPE-010), evidence of broken buyer momentum, time sensitivity, and whether a credible intervention exists; it displays those four factors instead of a percentage score. Equal briefs sort by earliest commitment or activity due time, then deal ID. *Policy: `intervention.ranking.v1`; default: the four factors above, ordered so that a brief with a credible intervention always outranks one without.*"
        },
        {
          "id": "INT-004",
          "title": "Brief anatomy.",
          "text": "An expanded brief shows impact, why now, the four time signals from PIPE-009, two to four cited facts or labelled inferences, one recommended action with its intended outcome and the source named under INT-011, controls, and the qualified buyer commitment that would demonstrate progress."
        },
        {
          "id": "INT-005",
          "title": "Unsafe recommendation.",
          "text": "Missing or conflicting evidence produces `Cannot recommend safely`, names the missing or conflicting inputs, and offers only record correction, evidence capture, or Deal Story navigation. Ferry never fabricates evidence, a recommendation, or a recipient to fill a gap."
        },
        {
          "id": "INT-006",
          "title": "Human-controlled action.",
          "text": "A prepared email, call, or task is editable. An operator may switch action, revise recipients or content, abandon it, or explicitly confirm it. Ferry never sends a message, creates an activity, or completes an activity without an explicit operator confirmation; no policy, extension, or setting enables autonomous sending or autonomous record changes."
        },
        {
          "id": "INT-007",
          "title": "Deferral and not actionable.",
          "text": "Deferring a brief requires a reason and a revisit time. Marking a deal Not actionable requires a reason and one next disposition — re-qualify, move the expected close date, nurture, reassign the deal to another operator, or close lost. Neither choice removes the record or its history."
        },
        {
          "id": "INT-008",
          "title": "Action is not progress.",
          "text": "Sending or scheduling a follow-up appends its action records but does not improve deal health. Only a commitment satisfying COMMIT-001, together with the next activity required by INT-002, may produce a new On track assessment."
        },
        {
          "id": "INT-009",
          "title": "Intervention deduplication.",
          "text": "At most one active brief exists for the same deal and causal evidence set. Re-evaluating or redelivering the same evidence updates that brief instead of creating another; materially different evidence may create a new brief and supersedes any now-stale recommendation."
        },
        {
          "id": "INT-010",
          "title": "Evaluation failure.",
          "text": "If health or intervention evaluation fails, canonical deals, activities, and commitments remain usable and no prepared action is dispatched. An affected brief shows its last successful evaluation time and a visible failure. When no evaluation has succeeded for 30 minutes, Focus shows a stale-evaluation warning with that time above the queue, whether or not any brief exists. A successful re-evaluation clears the warning without erasing the failure from operations history."
        },
        {
          "id": "INT-011",
          "title": "Recommended action source.",
          "text": "The recommended action in a brief is produced from the deal's own records, and the brief names the built-in playbook that produced it and the records it used. Playbooks are part of the recommendation policy, not an operator-editable object. *Policy: `intervention.recommendation.v1`; default: playbooks for a missed customer commitment, an unfilled required role, no recorded buyer interaction since the deal's last outbound message, and an expected close date that has passed or falls within 7 days with no planned next activity.*"
        },
        {
          "id": "INT-012",
          "title": "No generated or opaque numbers.",
          "text": "Ferry calls no AI provider and declares none in `seed.json → externals`; no content is labelled or presented as AI-generated. No number shown to an operator is a score: every health state, ranking factor, coverage state, and Insights figure names the records or the formula it derives from."
        },
        {
          "id": "INT-013",
          "title": "When health is recalculated.",
          "text": "A deal's health is recalculated within 5 seconds of a change to its stage, value, expected close date, relationships, activities, commitments, or evidence, and for every open deal at least every 15 minutes so that a due time passing changes health with no record change. Each assessment records the time it ran and the records it used."
        },
        {
          "id": "MAIL-001",
          "title": "Confirmed dispatch.",
          "text": "Confirming `Send and schedule` transactionally creates one outbound message and one linked next activity before dispatch. If that transaction cannot commit, neither is created and no provider request is made. The provider request is made within 60 seconds of confirmation through `mail.sender.v1`; only the confirmed recipients and content leave the deployment."
        },
        {
          "id": "MAIL-002",
          "title": "Delivery states.",
          "text": "An outbound message is draft, queued, accepted, or failed. The UI says sent only after the provider accepts the message; a later rejection or bounce changes it to failed and shows the provider reason without changing deal health."
        },
        {
          "id": "MAIL-003",
          "title": "No duplicate outreach.",
          "text": "Double-clicks, client retries, queue redelivery, and provider callback replay cannot create or send a second message. Every attempt reuses the message's stable ID as its idempotency key."
        },
        {
          "id": "MAIL-004",
          "title": "Failure and retry.",
          "text": "Transient provider failures retry up to five times over one hour. A terminal failure remains visible on the message and its linked activity with a deliberate retry action; retry reuses the original message ID and never hides the failed attempt."
        },
        {
          "id": "SEARCH-001",
          "title": "Global retrieval.",
          "text": "Global search matches deal titles, organization and person names, person emails, activity subjects, commitment text, and message subjects, across open and closed deals, and returns within 500ms. Each result identifies its type and whether its deal is closed, and opens the canonical record."
        },
        {
          "id": "IMPORT-001",
          "title": "Preview before mutation.",
          "text": "CSV import accepts one entity type per file — deals, people, organizations, or activities. It maps columns and validates every row before confirmation, showing creates, ID-based updates, collisions under REL-003, warnings, and rejected rows with their original row numbers and specific reasons. A file at the row limit in `seed.json` is previewed within 60 seconds."
        },
        {
          "id": "IMPORT-002",
          "title": "Safe execution.",
          "text": "A confirmed import processes each row transactionally and reports accepted and rejected counts without silent skips. Each attempt has a durable ID and records the file's checksum; the file itself is not retained. Retrying an attempt requires re-uploading a file with the same checksum and continues that attempt rather than producing a second result; a different checksum starts a new attempt."
        },
        {
          "id": "IMPORT-003",
          "title": "Deterministic updates.",
          "text": "An Ferry ID updates that record. Without an ID, a deal row creates a deal and title similarity is only a warning; person and organization collisions follow REL-003. Imported values never overwrite an existing person or organization automatically."
        },
        {
          "id": "INSIGHT-001",
          "title": "Declared metrics.",
          "text": "Insights shows open commitments due, active interventions, and recoverable revenue. Recoverable revenue is the recorded value of open Slipping or At risk deals that have an active Intervention Brief, reported under PIPE-010; the UI exposes the formula, filters, date range, and records included."
        },
        {
          "id": "INSIGHT-002",
          "title": "Drill-through.",
          "text": "Every commitment, intervention, health, and revenue row opens the corresponding Deal Story or Intervention Brief with the same filters preserved on return."
        },
        {
          "id": "INSIGHT-003",
          "title": "Figures match their records.",
          "text": "A metric shows a change against an earlier period only when Ferry holds the stored records for that period; the comparison names its window and opens the records behind both values. A metric with no stored prior period shows its current value and no trend. Every headline figure agrees with the rows the same view lists beneath it."
        },
        {
          "id": "FIELD-001",
          "title": "Custom fields.",
          "text": "Operators define custom fields on deals, people, organizations, and activities in settings, each with a name and type. Values appear on the record and in Deal Story, and travel through CSV export and import (EXPORT-001, IMPORT-003). Removing a field definition hides it from entry forms and keeps every recorded value in Deal Story and in exports until an operator deletes it."
        },
        {
          "id": "EXPORT-001",
          "title": "Export contents.",
          "text": "In addition to the complete portable archive required by BASE-DATA-001, operator CSV exports include stable Ferry IDs, linked-record IDs, custom fields, stage and outcome history, original activity due times, actual completion times, and commitment states, so that an edited export can be reimported as deterministic updates."
        }
      ],
      "hasReadme": true,
      "hasBaseline": true,
      "nonGoals": [
        "multiple workspaces or pipelines",
        "role, territory, or record-visibility hierarchies",
        "a separate lead object",
        "product catalogs, quotes, invoices, and contracts",
        "marketing campaigns and sequences",
        "autonomous outbound actions",
        "AI scoring or general AI chat",
        "retrieved exchange rates or currency conversion beyond operator-recorded rates",
        "per-operator time zones and locales",
        "inbound mailbox, Gmail, Outlook, or calendar synchronization",
        "recurring activities",
        "public forms, widgets, and booking pages",
        "file attachments",
        "SMS and calling",
        "arbitrary report builders, custom dashboards, goals, and team forecasting",
        "public APIs and webhooks",
        "native mobile apps",
        "post-sale project or customer-success management"
      ],
      "externals": [
        {
          "name": "transactional-mail",
          "required": true,
          "requiredWhen": "",
          "reason": "SendGrid delivers native sign-in links through the optional Base transport. Confirmed deal-follow-up dispatch remains an unimplemented contract target.",
          "data": [
            "recipient email address",
            "message subject and body",
            "delivery identifiers"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        }
      ],
      "operatingCost": {
        "currency": "USD",
        "status": "estimate-pending-load-test",
        "assumptions": "Cloudflare defaults to $0 on Workers Free for small use; external transactional mail and a domain are excluded. At the declared 10,000 active-deal limit, 15-minute INT-013 assessments create at least 960,000 D1 health-assessment row writes per day before indexes and other writes, above D1 Free's 100,000 rows-written-per-day limit. About 1,041 open deals is the theoretical Free write ceiling before indexes and other writes. Queues Free includes 10,000 operations per day; the declared 500 outbound messages per day use about 1,500 normal write/read/delete operations before retries.",
        "monthlyEstimate": "$0 on Workers Free for small use; D1 requires Workers Paid at the stated 15-minute assessment threshold. Estimate pending load test.",
        "includes": [
          "Workers Free baseline for small use",
          "D1 and Queues free tiers",
          "Cron and SQLite-backed Durable Objects including alarms"
        ],
        "excludes": [
          "external transactional mail",
          "domain registration",
          "custom AI providers",
          "inbound mailbox sync",
          "calendar sync"
        ]
      },
      "extensionPoints": [
        "app.route.v1",
        "app.navigation.v1",
        "app.settings-section.v1",
        "app.job.v1",
        "app.cron.v1",
        "app.queue-consumer.v1",
        "operator.management.v1",
        "pipeline.stages.v1",
        "deal.stage-transition.v1",
        "deal.close-reason.v1",
        "relationship.required-roles.v1",
        "activity.reminder.v1",
        "deal.health-assessment.v1",
        "intervention.ranking.v1",
        "intervention.recommendation.v1",
        "deal.created.v1",
        "deal.stage-changed.v1",
        "deal.closed.v1",
        "deal.reopened.v1",
        "activity.completed.v1",
        "commitment.recorded.v1",
        "commitment.overdue.v1",
        "deal.health-assessed.v1",
        "intervention.deferred.v1",
        "deal.not-actionable.v1",
        "message.accepted.v1",
        "message.failed.v1",
        "app.navigation.after.v1",
        "focus.header.after.v1",
        "focus.brief.actions.after.v1",
        "pipeline.toolbar.after.v1",
        "pipeline.deal-card.actions.after.v1",
        "deal-story.sidebar.after.v1",
        "deal-story.timeline.after.v1",
        "activity.detail.after.v1",
        "contact.detail.after.v1",
        "insights.intervention.after.v1",
        "settings.sections.after.v1",
        "auth.session.v1",
        "mail.sender.v1"
      ],
      "limits": {
        "status": "estimate-untested",
        "operators": 25,
        "pipelineStages": 20,
        "activeDeals": 10000,
        "organizations": 10000,
        "people": 50000,
        "activities": 250000,
        "commitments": 100000,
        "csvRowsPerImport": 50000,
        "outboundMessagesPerDay": 500,
        "publicRateLimits": {
          "authInitiationsPerIpPerHour": 5,
          "authVerificationsPerIpPerHour": 10,
          "healthRequestsPerIpPerMinute": 60,
          "assetRequestsPerIpPerMinute": 600
        }
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target-unverified"
      },
      "landing": "assets/seeds/ferry/verification-2026-09-12-justevery-pilot-deal-desktop-followup.png",
      "shots": [
        "assets/seeds/ferry/verification-2026-09-12-justevery-pilot-deal-desktop-followup.png",
        "assets/seeds/ferry/verification-2026-09-12-justevery-pilot-deal-desktop.png",
        "assets/seeds/ferry/00-landing-desktop.jpg",
        "assets/seeds/ferry/01-focus-at-risk-desktop.jpg",
        "assets/seeds/ferry/02-pipeline-desktop.jpg",
        "assets/seeds/ferry/03-deal-story-desktop.jpg"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/ferry",
        "release": "0.0.0",
        "manifestSha256": "b31735c9b30fc03d56c28136dc0caf0ef6393c8266c0cfc64ab362bfdf85207c"
      }
    },
    {
      "id": "lamp",
      "authoring": {
        "mode": "edition-owned",
        "agentSource": "AGENTS.md",
        "id": "runeditrun/lamp",
        "parent": {
          "repository": "https://github.com/runeditrun/status-lamp.git",
          "revision": "3481401773321069d97704e0f2adaf6f25ce848b"
        },
        "toolchain": {
          "id": "runeditrun/edition-authoring",
          "version": "1.1.0",
          "sha256": "a13d7d0bcf174618923ca5d1134575c149ea6b57c89d1640c03c1ebf091ab372"
        },
        "components": [
          {
            "id": "runeditrun/lamp",
            "role": "edition",
            "source": {
              "kind": "local",
              "paths": [
                "src/core/features",
                "src/core/platform",
                "src/core/app",
                "src/ext",
                "src/routes",
                "migrations"
              ]
            },
            "requires": [
              "runeditrun/widget-origins"
            ]
          },
          {
            "id": "runeditrun/widget-origins",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/core/shared/widget-origins.ts"
              ]
            }
          },
          {
            "id": "runeditrun/owner-migrations",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/deployment/owner-migrations.mjs",
                "scripts/deployment/owner-migrations.test.mjs",
                "composition/modules/owner-migrations/README.md",
                "composition/modules/owner-migrations/LICENSE"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "d1343b638afd254679bc2b35819fc1d268bb6cc7",
                "path": ".",
                "paths": [
                  "deployment/owner-migrations.mjs",
                  "deployment/owner-migrations.test.mjs",
                  "deployment/OWNER-MIGRATIONS.md",
                  "LICENSE"
                ]
              },
              "mappings": [
                {
                  "path": "scripts/deployment/owner-migrations.mjs",
                  "originPath": "deployment/owner-migrations.mjs"
                },
                {
                  "path": "scripts/deployment/owner-migrations.test.mjs",
                  "originPath": "deployment/owner-migrations.test.mjs"
                },
                {
                  "path": "composition/modules/owner-migrations/README.md",
                  "originPath": "deployment/OWNER-MIGRATIONS.md"
                },
                {
                  "path": "composition/modules/owner-migrations/LICENSE",
                  "originPath": "LICENSE"
                }
              ]
            }
          },
          {
            "id": "runeditrun/sendgrid-email",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/sendgrid-email",
                "tests/composition/sendgrid-email",
                "composition/modules/sendgrid-email/README.md",
                "composition/modules/sendgrid-email/LICENSE"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "d1343b638afd254679bc2b35819fc1d268bb6cc7",
                "path": ".",
                "paths": [
                  "modules/sendgrid-email/src",
                  "modules/sendgrid-email/tests",
                  "modules/sendgrid-email/README.md",
                  "modules/sendgrid-email/LICENSE"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/sendgrid-email",
                  "originPath": "modules/sendgrid-email/src"
                },
                {
                  "path": "tests/composition/sendgrid-email",
                  "originPath": "modules/sendgrid-email/tests"
                },
                {
                  "path": "composition/modules/sendgrid-email/README.md",
                  "originPath": "modules/sendgrid-email/README.md"
                },
                {
                  "path": "composition/modules/sendgrid-email/LICENSE",
                  "originPath": "modules/sendgrid-email/LICENSE"
                }
              ]
            }
          }
        ],
        "manifestSha256": "91cd33a52b5f838e13b2aa707fc55ae52a18cb80586e9020da20a2ca328f03f7",
        "lockSha256": "cb998b59baf8d8c2eea7214c82594753091aca4f063fe344e69164c813177e68"
      },
      "name": "Lamp",
      "dir": "statuspage",
      "category": "status",
      "categoryLabel": "Status",
      "spine": {
        "id": "status",
        "record": "incident on a component"
      },
      "replaces": [
        {
          "name": "Statuspage",
          "edition": null
        }
      ],
      "version": "1.1.0",
      "oneLiner": "A status page and incident communication desk for one business.",
      "summary": "A status page and incident communication desk for one business.",
      "scope": "",
      "maturity": "ui-port-local-runtime",
      "clauseCount": 88,
      "clauses": [
        {
          "id": "PAGE-001",
          "title": "Contents.",
          "text": "`GET /` on the status page host returns, in one response: the overall status; every visible component with its current state and note; every unresolved incident with its 5 most recent published updates, newest first, and a link to that incident's page carrying all of them; every maintenance that is in progress and every scheduled maintenance starting within 14 days; and each visible component's uptime figure and daily record for the reporting window."
        },
        {
          "id": "PAGE-002",
          "title": "Readable without script.",
          "text": "The complete content of PAGE-001, and the subscribe form of SUB-001, are present and usable in the initial HTML response. With JavaScript disabled, every status, update, and timestamp is rendered and readable and the subscribe form submits. Script only replaces timestamps with the visitor's local-timezone equivalent."
        },
        {
          "id": "PAGE-003",
          "title": "Weight.",
          "text": "At the component limit in `seed.json → limits`, the public page's initial HTML response is at most 50KB gzipped, and the page issues at most 4 further requests — stylesheet, font, logo, script — all to its own origin."
        },
        {
          "id": "PAGE-004",
          "title": "Overall status.",
          "text": "*Policy: `page.status-rollup.v1`; default: the overall status is the most severe current state among visible components, under the order in COMP-001.* It is shown as one labelled state at the top of the page. With every visible component Operational and no unresolved incident, that label reads as operational."
        },
        {
          "id": "PAGE-005",
          "title": "Snapshot read path.",
          "text": "Every change to the content in PAGE-001 writes a complete rendered snapshot of the public page, the Atom feed, and the widget payload to object storage. Serving those three surfaces reads the snapshot and issues no database query. A snapshot write that fails is retried until it succeeds and is reported as a failed job under BASE-OPS-004; the previous snapshot keeps serving until it does."
        },
        {
          "id": "PAGE-006",
          "title": "Serving through a failure.",
          "text": "When the database, the queue, or object storage is unreachable, the three surfaces in PAGE-005 return 200 — from the current snapshot, or from the cached response under the `stale-if-error` directive of PAGE-007 when the origin itself cannot answer. They return 503 only when no snapshot has ever been written. A response that could not be built from current data states the instant its content was generated and is labelled as the last known state; no failed read ever produces the all-clear of PAGE-004. `/health` still reports 503 per BASE-OPS-001; the public page's availability does not depend on it."
        },
        {
          "id": "PAGE-007",
          "title": "Publish latency and caching.",
          "text": "The three surfaces in PAGE-005 are served with `Cache-Control: public, max-age=10, s-maxage=10, stale-while-revalidate=30, stale-if-error=3600`. An update, component state change, or maintenance transition purges that cache and is readable on all three surfaces within 10 seconds of the request that caused it returning 200."
        },
        {
          "id": "PAGE-008",
          "title": "Public read paths are not rate limited.",
          "text": "The public read paths marked `read` in `seed.json → deploy.publicPaths` never return 429. Requests from one address up to the `publicPageRequestsPerSecond` limit in `seed.json → limits` all return 200. **Supersedes BASE-ACCESS-003** for those paths only. A status page is read hardest at the moment it matters most, and a per-address limit would reject real readers behind one egress address during an incident — but the decisive reason is mechanical: RFC 6585 requires that a 429 is never stored by a cache, so rate limiting the read path converts a cacheable traffic spike into an uncacheable one and sends it all to the origin. What replaces the limit is the cache itself: those paths return identical bytes to every visitor between snapshots, so origin load is a function of the PAGE-007 window and not of how many people are reading. The public write paths (`/subscribe`, `/unsubscribe`, `/manage`, `/api/intake/*`) stay rate limited under BASE-ACCESS-003."
        },
        {
          "id": "PAGE-009",
          "title": "Time display.",
          "text": "Every timestamp on a public surface or in a notification is rendered in the page's configured timezone with its UTC offset, and every timestamp on the page carries the same instant as an ISO 8601 value in a machine-readable attribute."
        },
        {
          "id": "PAGE-010",
          "title": "Atom feed.",
          "text": "`/feed.atom` validates as Atom 1.0 and contains one entry for every published incident update and for every maintenance notice — scheduled, rescheduled, started, completed, cancelled — from the last 12 months, ordered newest first. An entry's identifier is stable from first publication and is never reused."
        },
        {
          "id": "PAGE-011",
          "title": "Embeddable widget.",
          "text": "One script tag renders a banner showing the overall status and any unresolved incident, read from the PAGE-005 snapshot. The script is at most 10KB gzipped, loads only from the deployment's origin, and renders only on the host origins listed in settings per BASE-PUBLIC-002."
        },
        {
          "id": "PAGE-012",
          "title": "Branding.",
          "text": "Page name, logo, accent colour, timezone, hostname, and a footer-credit toggle are set in settings without a code change. The footer credit is the plain static text `Powered by Lamp · runeditrun.com`, defaults on, and disabling it makes no network action. The logo is served from the deployment's own origin. The favicon composites a mark for the current overall status over the configured logo rather than replacing it, and the page carries Open Graph metadata whose title and image state the current overall status."
        },
        {
          "id": "PAGE-013",
          "title": "Enforced isolation.",
          "text": "Every public page response carries a `Content-Security-Policy` whose `default-src`, `script-src`, `style-src`, `img-src`, `connect-src`, `form-action`, and `base-uri` name only the deployment's own origin, and `frame-ancestors 'none'`, together with `X-Content-Type-Options: nosniff` and `Referrer-Policy: strict-origin-when-cross-origin`. The page contains no CAPTCHA and no consent banner, because it stores nothing on the visitor's device and loads nothing that would need either."
        },
        {
          "id": "PAGE-014",
          "title": "Live refresh.",
          "text": "When script is available the page re-reads `/api/public/status` at most every 30 seconds and updates the overall status and the newest update in place. That response is a fixed set of fields whose size does not grow with the number of components. The region it updates is an ARIA live region, so a change is announced without a reload. With script disabled the page does not refresh itself and states the instant its content was generated."
        },
        {
          "id": "COMP-001",
          "title": "States and their order.",
          "text": "A component's public state is exactly one of Operational, Degraded performance, Partially unavailable, Major outage, or Under maintenance. Those states are ordered by severity, most severe first: Major outage, Partially unavailable, Degraded performance, Under maintenance, Operational. Every clause that says \"most severe\" means this order."
        },
        {
          "id": "COMP-002",
          "title": "Grouping and order.",
          "text": "A component belongs to at most one group. Operators set the order of groups and of components within a group, and the public page renders them in that order."
        },
        {
          "id": "COMP-003",
          "title": "Append-only state history.",
          "text": "Every change to a component's public state appends an entry recording the previous state, the new state, the instant of the change, and the operator or incident that caused it. An entry is never modified or removed; a correction under UPTIME-004 appends a superseding entry rather than altering the one it corrects."
        },
        {
          "id": "COMP-004",
          "title": "State note.",
          "text": "A component has a note of at most 250 characters, empty by default. When it is not empty it is shown beside that component's state on the public page exactly as entered."
        },
        {
          "id": "COMP-005",
          "title": "Internal components.",
          "text": "A component marked internal is absent from the public page, the Atom feed, the widget, and every uptime figure, and never contributes to the overall status."
        },
        {
          "id": "COMP-006",
          "title": "Component state is claimed, not set.",
          "text": "Three things claim a state for a component: an unresolved incident, an in-progress maintenance, and an operator's standing claim set outside either. An incident or maintenance claims a state per attached component, chosen when the component is attached. A component's public state is the most severe of the states claimed for it, and Operational when nothing claims it. Resolving an incident, completing a maintenance, attaching or detaching a component, and setting or clearing an operator claim each recompute that component's state by this rule within 10 seconds."
        },
        {
          "id": "COMP-007",
          "title": "Deletion.",
          "text": "Deleting a component removes it and its state history from the public page and from every uptime figure within 10 seconds. Incidents and maintenance that referenced it keep the component's name in their published updates."
        },
        {
          "id": "UPTIME-001",
          "title": "Definition.",
          "text": "For each visible component the page shows the percentage of the reporting window during which the component was not in a downtime state, computed from COMP-003 state history at one-minute resolution and rounded down to two decimal places, so a window containing any downtime never shows 100.00%. The reporting window is 90 days."
        },
        {
          "id": "UPTIME-002",
          "title": "Downtime weights.",
          "text": "*Policy: `uptime.downtime-states.v1`; default: a minute in Major outage or Partially unavailable counts as one downtime minute, a minute in Degraded performance counts as half a downtime minute, and a minute in Under maintenance counts as none.* Degraded performance is not weighted at zero: most published incidents never get worse than it, and discarding them is what makes an uptime figure disagree with the incident history printed beneath it."
        },
        {
          "id": "UPTIME-003",
          "title": "Short history.",
          "text": "A component whose earliest state history entry is later than the start of the reporting window shows uptime computed from that entry, and the page labels the figure with the number of whole days it covers."
        },
        {
          "id": "UPTIME-004",
          "title": "Corrections.",
          "text": "An operator can correct a closed interval of a component's state history, stating the correct state for that interval and a reason. The correction appends to the history per COMP-003, recomputes every uptime figure it affects within 60 seconds, and records the operator, the interval, and the reason in the audit log. A request without a reason is rejected with 400."
        },
        {
          "id": "UPTIME-005",
          "title": "Stated method.",
          "text": "Beside the uptime figures the page names each state's weight under UPTIME-002 and the length of the window under UPTIME-001, in text present in the initial HTML. Because UPDATE-001 refuses to publish an update that claims no component, every published incident moves these figures; none of the incident history printed on the same page is invisible to them."
        },
        {
          "id": "UPTIME-006",
          "title": "Daily record.",
          "text": "The page shows one mark per day of the reporting window for each visible component. A day containing any downtime minute is labelled with the number of downtime minutes in that day. A day before the component's earliest state history entry is rendered as having no data and is visually distinct from a day with no downtime."
        },
        {
          "id": "INCIDENT-001",
          "title": "Lifecycle.",
          "text": "An incident is in exactly one of Investigating, Identified, Monitoring, or Resolved. It is created in Investigating, and its state changes only through a published update that sets it."
        },
        {
          "id": "INCIDENT-002",
          "title": "Impact.",
          "text": "An incident's impact is the most severe state it has claimed on any attached component at any time since it was opened, so a resolved incident keeps the impact it reached. An incident with no attached components has no impact and cannot be published; UPDATE-001 requires at least one."
        },
        {
          "id": "INCIDENT-003",
          "title": "Reference.",
          "text": "Every incident has a reference of the form `INC-` followed by digits and a permanent public URL containing it. A reference is never reused, including after INCIDENT-007."
        },
        {
          "id": "INCIDENT-004",
          "title": "Published updates are immutable.",
          "text": "A published update is never edited or deleted. A correction is a new published update that names the update it corrects, and both stay on the public timeline in the order they were published."
        },
        {
          "id": "INCIDENT-005",
          "title": "Reopening.",
          "text": "An incident resolved in error is reopened by publishing an update that sets it to Investigating. The resolution update and the reopening update both stay on the public timeline."
        },
        {
          "id": "INCIDENT-006",
          "title": "Overdue update.",
          "text": "When the next-update time committed by an incident's latest published update passes with no newer update published, the incident is marked overdue in the operator app. The public page shows the committed time and never labels the incident overdue."
        },
        {
          "id": "INCIDENT-007",
          "title": "Deletion.",
          "text": "Deleting an incident requires a reason and removes it from the public page, the history page, and the Atom feed within 10 seconds. A request without a reason is rejected with 400. The operator, the reason, and the time are recorded in the audit log."
        },
        {
          "id": "INCIDENT-009",
          "title": "Retroactive incidents.",
          "text": "An operator can open an incident whose updates carry publication times in the past. Those times are what the public timeline and the history page show, the component states it claims are written into the state history at those times so UPTIME-001 counts them, and no subscriber is notified for any of its updates."
        },
        {
          "id": "INCIDENT-008",
          "title": "Internal notes.",
          "text": "An incident carries internal notes visible only to a signed-in operator. A note never appears on the public page, in the Atom feed, in the widget, in any notification, or in any AI request."
        },
        {
          "id": "UPDATE-001",
          "title": "Template.",
          "text": "Publishing an update requires at least one attached component, an affected-audience statement of at most 250 characters, a customer-impact statement of at most 250 characters, and a next-update time. A what-still-works statement of at most 250 characters and a details statement of at most 500 characters are optional. Every field is plain text; no HTML or markdown is interpreted. A publish request that omits a required field, exceeds a length, or attaches no component returns 400 and changes nothing. The next-update time is a commitment to publish again by then; it is never presented as an estimate of when the incident will be resolved."
        },
        {
          "id": "UPDATE-002",
          "title": "One rendering.",
          "text": "The public page, the Atom feed, the widget payload, and the notification email render an update from the same renderer. No surface omits a populated field of the update or shows a field the update does not carry."
        },
        {
          "id": "UPDATE-003",
          "title": "Sets the lifecycle.",
          "text": "Every published update sets the incident's lifecycle state. An update that sets Resolved records the incident's end time and recomputes every attached component under COMP-006."
        },
        {
          "id": "UPDATE-004",
          "title": "Preview.",
          "text": "Before publishing, the composer shows the update as the public page and as the notification email will render it, produced by the renderer named in UPDATE-002."
        },
        {
          "id": "UPDATE-005",
          "title": "Drafts.",
          "text": "An unpublished update appears on no public surface. A draft is saved within 2 seconds of the last edit and is restored unchanged after a browser reload."
        },
        {
          "id": "UPDATE-006",
          "title": "Scheduled publish.",
          "text": "An update given a future publish time publishes within 60 seconds of that time and appears on no public surface and in no notification before it. If UPDATE-007 review is in force and the update has not been reviewed when that time arrives, it is not published, stays a draft, and is reported under NOTIFY-006."
        },
        {
          "id": "UPDATE-007",
          "title": "Review.",
          "text": "*Policy: `update.review.v1`; default: no review is required. When review is required, a publish request for an update its author wrote is rejected with 409 until an operator other than the author marks it reviewed; the reviewer and the time are recorded on the update.*"
        },
        {
          "id": "UPDATE-008",
          "title": "Publish is atomic.",
          "text": "One transaction commits the update together with the recipient set it will notify on each channel selected under NOTIFY-007. That set is fixed at publish time: a subscriber who narrows their selection afterwards still receives this update, and one who unsubscribes afterwards does not, per SUB-008. When the publish request returns 200 that transaction is committed, and PAGE-007 and NOTIFY-002 follow from it. When it returns an error the update is on no surface and no recipient is recorded for it. Dispatch of the committed set is retried until it succeeds and never blocks the publish."
        },
        {
          "id": "UPDATE-009",
          "title": "Templates.",
          "text": "An operator can save a template holding the update fields of UPDATE-001, the components it attaches with the state each claims, and the lifecycle state it sets. Applying a template fills the composer and attaches those components with those states, and publishes nothing."
        },
        {
          "id": "MAINT-001",
          "title": "Distinct from incidents.",
          "text": "A scheduled maintenance has a start time, an end time, and attached components. It has a reference of the form `SCH-` followed by digits, and is listed separately from incidents wherever both appear."
        },
        {
          "id": "MAINT-002",
          "title": "Automatic transitions.",
          "text": "A scheduled maintenance moves from Scheduled to In progress within 60 seconds of its start time, and from In progress to Completed within 60 seconds of its end time, with no operator action."
        },
        {
          "id": "MAINT-003",
          "title": "Component states.",
          "text": "A maintenance claims Under maintenance for every attached component while it is In progress, and claims nothing before it starts or after it completes. A component's public state follows from COMP-006 throughout, so a state an operator or an incident claimed before the window is still claimed after it."
        },
        {
          "id": "MAINT-004",
          "title": "Overrun.",
          "text": "A maintenance still In progress 15 minutes after its end time is shown on the public page as running longer than planned, beside its original end time, until an operator completes it or extends the window."
        },
        {
          "id": "MAINT-005",
          "title": "Reminders.",
          "text": "*Policy: `maintenance.reminders.v1`; default: subscribers are notified when the maintenance is scheduled, 24 hours before the start time, when it moves to In progress, and when it moves to Completed.*"
        },
        {
          "id": "MAINT-006",
          "title": "Rescheduling and cancellation.",
          "text": "Changing the window of a scheduled maintenance notifies every subscriber already notified about it. Cancelling it publishes a cancellation notice to those same subscribers, removes it from the upcoming list within 10 seconds, and keeps it in the maintenance history marked cancelled."
        },
        {
          "id": "SUB-001",
          "title": "Subscribing.",
          "text": "Submitting an email address on the public page creates a pending subscription and sends one confirmation email to that address within 60 seconds. That address receives at most one confirmation email in any 24 hours however many times it is submitted, which is what bounds a subscription-bombing attack. A pending subscription receives no other notification. The form is a standard POST that works with script disabled, is rejected with 403 when the request carries `Sec-Fetch-Site: cross-site`, and carries no CSRF token, because with no cookie and no session there is no ambient authority to forge."
        },
        {
          "id": "SUB-002",
          "title": "Confirmation.",
          "text": "*Policy: `subscriber.confirmation.v1`; default: a subscription becomes active only when the link in the confirmation email is followed.*"
        },
        {
          "id": "SUB-003",
          "title": "Pending expiry.",
          "text": "A pending subscription not confirmed within 72 hours is deleted, including the address it holds. **Supersedes BASE-DATA-002** for pending subscriptions only: the address was supplied by an unverified visitor who may not own it, so it is held only as long as confirmation is possible."
        },
        {
          "id": "SUB-004",
          "title": "Component selection.",
          "text": "A subscriber selects which components they want updates about, and is notified about an incident or maintenance only when it claims a state for at least one component in that selection. A subscription whose selection has never been narrowed follows every visible component, including components added after it was created. A subscription with a narrowed selection is unchanged when a component is added. When a component is attached to an unresolved incident, every subscriber who follows that component and has received no notification for that incident receives exactly one notification carrying its most recent published update, and then receives subsequent updates normally."
        },
        {
          "id": "SUB-005",
          "title": "Cookieless subscriber tokens.",
          "text": "A subscriber is identified on public surfaces only by an opaque token of at least 128 bits of entropy that addresses one subscription, is carried in the URL of a link the deployment emailed to that subscriber, grants no other capability, and is revoked when the subscription is deactivated or deleted. No cookie is set on any public surface. Responses on token-addressed paths carry `Referrer-Policy: no-referrer` and `X-Robots-Tag: noindex`. **Supersedes BASE-PUBLIC-001** for token-addressed paths only, which transmit the token on load rather than after a first interaction, because SUB-006 requires an unsubscribe that completes without any interaction on our page."
        },
        {
          "id": "SUB-006",
          "title": "One-click unsubscribe.",
          "text": "Every notification email carries a `List-Unsubscribe` header with a `mailto:` and an `https:` value, a `List-Unsubscribe-Post: List-Unsubscribe=One-Click` header, and a visible unsubscribe link in the body. A POST to the `https:` value, with a body of `List-Unsubscribe=One-Click` in either `multipart/form-data` or `application/x-www-form-urlencoded`, deactivates that subscription and returns 200 with no redirect, no cookie, and no further interaction. Both headers are named in the message's DKIM `h=` tag, without which receivers decline to offer one-click at all. That endpoint's BASE-ACCESS-003 rate limit is counted per subscription token, never per source address, so one mailbox provider's shared egress cannot exhaust the limit for other subscribers."
        },
        {
          "id": "SUB-007",
          "title": "Managing a subscription.",
          "text": "The token URL in every notification opens a page that shows the subscription's channel and component selection and lets the subscriber change the selection or unsubscribe, without a session."
        },
        {
          "id": "SUB-008",
          "title": "Unsubscribe integrity.",
          "text": "After a subscription is deactivated, no notification is delivered to it. A notification committed before the deactivation and dispatched after it sends nothing and records the outcome as suppressed."
        },
        {
          "id": "SUB-009",
          "title": "Webhook subscribers.",
          "text": "An endpoint URL and a contact email address together subscribe as a webhook subscriber; the address exists so SUB-010 can say why the endpoint was deactivated. Each notification is a POST carrying `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers, where the signature is a space-separated list of versioned HMAC-SHA256 values over `{webhook-id}.{webhook-timestamp}.{raw body}`, so a secret can be rotated without a gap. The body carries the incident, every update it has published, and each attached component's previous and new state, so a receiver that missed a delivery needs no prior state. It carries no subscriber address. A delivery succeeds only on a 2xx within 30 seconds; a redirect is a failure. A Slack incoming-webhook URL receives the same events with a Slack-formatted body."
        },
        {
          "id": "SUB-010",
          "title": "Failing destinations.",
          "text": "An address that hard-bounces once, or whose provider reports a spam complaint, is deactivated with that reason recorded. An address that soft-bounces on 5 consecutive notifications, and a webhook endpoint whose last 10 consecutive deliveries all failed under SUB-009, are deactivated with that reason recorded. Deactivating a webhook endpoint emails its contact address. A deactivated destination receives no further notification."
        },
        {
          "id": "SUB-011",
          "title": "Operator view.",
          "text": "Operators see every subscriber with channel, state of pending, active, or deactivated, component selection, and the reason and time of any deactivation. Deleting a subscriber deletes the address or endpoint it holds."
        },
        {
          "id": "SUB-012",
          "title": "No enumeration.",
          "text": "Submitting an address that already has a subscription returns the same status and body as submitting a new address and creates no second subscription. That address receives one email saying it is already subscribed, carrying its management link and nothing else."
        },
        {
          "id": "NOTIFY-001",
          "title": "What notifies subscribers.",
          "text": "*Policy: `notification.events.v1`; default: an incident notifies on its first published update, on every subsequent published update, and on the update that resolves it; a scheduled maintenance notifies per MAINT-005; a component state change made outside an incident or maintenance notifies no one.*"
        },
        {
          "id": "NOTIFY-002",
          "title": "Delivery time.",
          "text": "At the subscriber limit in `seed.json → limits`, every eligible email notification for a published update is accepted by the mail provider within 5 minutes of the publish request returning 200."
        },
        {
          "id": "NOTIFY-003",
          "title": "Retry.",
          "text": "A notification that fails with a retryable error is retried 5 times with exponential backoff over 1 hour. After the last attempt it is recorded as failed against that subscriber and that update."
        },
        {
          "id": "NOTIFY-004",
          "title": "Delivery log, without tracking.",
          "text": "Every notification records the subscriber, channel, update, queue time, outcome, and outcome time. An outcome is one of queued, delivered, bounced, complained, suppressed, or failed. No notification carries a tracking pixel or a link that records who followed it, so no outcome describes reading. The log is filterable by channel, outcome, and update, and exports as CSV."
        },
        {
          "id": "NOTIFY-005",
          "title": "At most once.",
          "text": "A subscriber receives at most one notification per published update per channel, whatever the number of times the publish is retried or the queue delivers the job."
        },
        {
          "id": "NOTIFY-006",
          "title": "Operator alerts.",
          "text": "An operator email is sent, at most once per event, when an incident becomes overdue under INCIDENT-006, when a maintenance overruns under MAINT-004, when a scheduled update is held back for review under UPDATE-006, when an alert creates a draft incident under INTAKE-002, and when a notification is recorded as failed under NOTIFY-003."
        },
        {
          "id": "NOTIFY-007",
          "title": "Channels are per update.",
          "text": "The operator selects, for each update, which subscriber channels receive it, and selecting none is allowed. The public page, the Atom feed, and the widget always receive the update and cannot be deselected. The delivery log records the selection."
        },
        {
          "id": "NOTIFY-008",
          "title": "Who the deployment emails.",
          "text": "The deployment sends email only to an operator, to an address with a pending subscription for its single confirmation email, and to an address with an active subscription. Every notification is sent through `mail.sender.v1` from the address configured in settings."
        },
        {
          "id": "NOTIFY-010",
          "title": "Mail headers.",
          "text": "Every notification email carries `List-Id`, `List-Archive`, and `List-Help` identifying the page, `Auto-Submitted: auto-generated` so it triggers no vacation reply, and, for every update after an incident's first, `In-Reply-To` and `References` naming the message that carried the first, so a mail client groups an incident's updates into one thread. `pnpm setup` fails naming the record when the From address's domain publishes no SPF, no DKIM key for the configured selector, or no DMARC policy."
        },
        {
          "id": "NOTIFY-009",
          "title": "Delivery never blocks publication.",
          "text": "A publish under UPDATE-008 returns 200 while the mail provider, the queue, or a subscriber endpoint is failing. The public page, the feed, and the widget update under PAGE-007 regardless, and the committed notification set is delivered under NOTIFY-003 when the channel recovers."
        },
        {
          "id": "INTAKE-001",
          "title": "Authenticated requests.",
          "text": "Each source is configured with one of two credentials. With a signing secret, a request is accepted only when the HMAC-SHA256 over its timestamp header and raw body matches and the timestamp is within 5 minutes of the deployment's clock; the signature is verified over the received bytes before they are parsed. With a bearer token, a request is accepted only when it presents that token, which exists because most monitoring tools — among them Datadog, Pingdom, and Uptime Kuma — cannot sign a webhook at all. A request with an absent, malformed, stale, or non-matching credential returns 401 and creates nothing."
        },
        {
          "id": "INTAKE-002",
          "title": "Alerts never publish.",
          "text": "*Policy: `alert.intake.v1`; default: an accepted alert creates or updates a draft incident within 30 seconds and changes no public surface. An operator publishes it under UPDATE-008.* A buyer who wants a monitored component to change state without waiting for a person replaces this policy; the narrative still waits for an operator, because no alert payload contains the UPDATE-001 fields."
        },
        {
          "id": "INTAKE-003",
          "title": "Deduplication.",
          "text": "Alerts carrying the same deduplication key produce one incident for as long as that incident is unresolved: an alert whose key matches an unresolved incident is recorded against that incident and creates no second one. A redelivery of a request already accepted creates no additional incident, draft, or alert record and returns the same status as the first delivery."
        },
        {
          "id": "INTAKE-004",
          "title": "Recovery alerts.",
          "text": "An alert marked as recovered for a deduplication key records the recovery time on the matching alert record and changes no public surface and no incident lifecycle state."
        },
        {
          "id": "INTAKE-005",
          "title": "Payload mapping.",
          "text": "Each source configures, as JSON paths into its own payload, which field supplies the deduplication key, which supplies the title, and which distinguishes a firing alert from a recovered one. A request whose payload lacks the configured deduplication-key path returns 400 and is recorded against that source where an operator can see it."
        },
        {
          "id": "DRAFT-001",
          "title": "Suggested update.",
          "text": "When an AI provider is configured, an operator can request a suggested update for an incident. The suggestion fills the composer's fields, is editable in every field, and is not published."
        },
        {
          "id": "DRAFT-002",
          "title": "Inputs.",
          "text": "A suggestion is built from the incident's accepted alerts, its published updates, its attached components and their claimed states, and the tone instructions configured in settings. Internal notes under INCIDENT-008 are not sent."
        },
        {
          "id": "DRAFT-003",
          "title": "Absent or failing provider.",
          "text": "AI calls go through `ai.provider.v1`. With no provider configured the composer shows no suggestion control and the deployment makes no AI request. When a configured provider fails or does not answer within 20 seconds, the composer shows the error and every other publish path behaves unchanged."
        },
        {
          "id": "TEAM-001",
          "title": "Equal operators.",
          "text": "Every operator can create, edit, publish, and resolve every incident and maintenance, change every component, and manage subscribers. There are no roles. The only gate on publishing is UPDATE-007."
        },
        {
          "id": "TEAM-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. The operator configured at setup can be removed only by themselves."
        },
        {
          "id": "TEAM-003",
          "title": "Attribution.",
          "text": "Every published update, component state change, maintenance change, history correction, and subscriber deletion records the operator who made it. That operator's name is shown in the operator app and on no public surface."
        },
        {
          "id": "HIST-001",
          "title": "Public history.",
          "text": "The history page lists resolved incidents and completed maintenance grouped by month for the last 12 months, each with start time, end time, duration, the components it affected, and every update it published."
        },
        {
          "id": "HIST-002",
          "title": "Post-incident report.",
          "text": "An operator can attach one post-incident report to a resolved incident, holding a summary, a timeline, a cause, and remediation. It appears on that incident's public page within 10 seconds of publishing, and notifies subscribers on the channels the operator selects for it in the same way NOTIFY-007 selects them for an update."
        },
        {
          "id": "HIST-003",
          "title": "Report due.",
          "text": "*Policy: `postmortem.due.v1`; default: an incident that claimed Partially unavailable or Major outage on any component and has no post-incident report 7 days after resolution is marked as owing one in the operator app.*"
        },
        {
          "id": "HIST-004",
          "title": "Export contents.",
          "text": "The export contains every incident, update, component with its full state history, maintenance, subscriber with channel and state, notification delivery record, post-incident report, setting, and audit entry as JSON, and every uploaded image as its original file."
        }
      ],
      "hasReadme": true,
      "hasBaseline": true,
      "nonGoals": [
        "Uptime monitoring, probing, or synthetic checks: Lamp reports state that operators or accepted alerts produce, and running probes would make it a monitoring product with a second, contradictory source of truth behind the public page.",
        "On-call scheduling, paging, and escalation: the buyer's existing alerting tool already does this, and it is the system that feeds INTAKE-001.",
        "More than one status page per deployment: one business gets one public record, so a second page is a second deployment.",
        "Private or audience-scoped status pages: visitor authentication would be a whole second access surface for a product whose value is being readable by anyone, at any time, with nothing between them and it.",
        "Roles and permissions: these are one business's equal operators, and UPDATE-007 covers the one gate buyers actually ask for.",
        "SMS and voice notifications: per-message cost, carrier registration, and per-country consent rules that a self-hosted deployment cannot satisfy generically.",
        "Multi-language status pages: every incident update would need a translation before publish, which is the opposite of publishing in two minutes.",
        "A general-purpose operator API authenticated by API tokens: BASE-ACCESS-002 requires a session on every operator endpoint, and machine writes have one narrow door, INTAKE-001, with its own secret.",
        "SSO and SCIM: magic-link sign-in per BASE-ACCESS-001 is enough for a handful of operators, and an identity provider is an adapter, not a feature.",
        "Visitor analytics and email open tracking: BASE-PUBLIC-001 keeps the page free of cookies and visitor data, and NOTIFY-004 keeps tracking pixels out of notifications, because knowing who read an outage notice is worth less than being the one page a customer can open without being counted.",
        "Native mobile apps: the public page is the mobile experience, and it has to work with script disabled."
      ],
      "externals": [
        {
          "name": "mail-sender",
          "required": true,
          "requiredWhen": "",
          "reason": "Cloudflare Workers cannot deliver email to arbitrary recipients. Subscriber notification is the product's second surface, and SUB-006 and NOTIFY-008 require authenticated bulk sending with List-Unsubscribe support.",
          "data": [
            "Subscriber email address",
            "The published update's fields",
            "The subscription's unsubscribe and management tokens"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        },
        {
          "name": "ai-provider",
          "required": false,
          "requiredWhen": "",
          "reason": "DRAFT-001 only. Absent, the composer shows no suggestion control and no request is made (DRAFT-003). Internal notes are never sent (DRAFT-002).",
          "data": [
            "Incident title",
            "Accepted alert payloads",
            "Published updates",
            "Component states"
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        },
        {
          "name": "subscriber-webhooks",
          "required": false,
          "requiredWhen": "",
          "reason": "SUB-009. The destinations are chosen by the buyer's subscribers, so they cannot be enumerated as named hosts at publish time; this entry is declared as a class of destinations, not one named service, which satisfies BASE-DATA-004 without a supersession because the payload carries no personal data, every request is signed, and every delivery is in the log.",
          "data": [
            "The published update's fields",
            "The delivery identifier",
            "The update identifier"
          ],
          "adapters": []
        },
        {
          "name": "run-edit-run-editor",
          "required": false,
          "requiredWhen": "",
          "reason": "Optional owner-enabled editor frame; separate Run Edit Run authentication and installation authority are required. Disabled by default.",
          "data": [
            "Installation ID and edition origin",
            "User-selected element structure",
            "Explicitly reviewed text limited to 240 characters"
          ],
          "adapters": [
            "rer.editor.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "computed from published Cloudflare Workers Free/Paid pricing tiers and the stated usage assumptions, not measured against a running deployment",
        "currency": "USD",
        "perMonthEstimate": 0,
        "basis": "The low-volume default is USD 0/month on Workers Free: 100,000 Worker requests/day, 10,000 Queues operations/day, D1's 5 million reads/day and 100,000 writes/day, the R2 free tier, Cron, and SQLite-backed Durable Objects including alarms. The declared 5,000 public requests/second capacity (or any other Free-plan cap) requires Workers Paid; USD 5/month is the Paid minimum, not a sustained-load estimate. Transactional mail and optional AI remain excluded."
      },
      "extensionPoints": [
        "page.status-rollup.v1",
        "uptime.downtime-states.v1",
        "update.review.v1",
        "maintenance.reminders.v1",
        "subscriber.confirmation.v1",
        "notification.events.v1",
        "alert.intake.v1",
        "postmortem.due.v1",
        "incident.opened.v1",
        "incident.update.published.v1",
        "incident.resolved.v1",
        "component.status.changed.v1",
        "maintenance.scheduled.v1",
        "maintenance.started.v1",
        "maintenance.completed.v1",
        "subscriber.confirmed.v1",
        "subscriber.deactivated.v1",
        "notification.failed.v1",
        "alert.accepted.v1",
        "snapshot.published.v1",
        "status-page.header.after.v1",
        "status-page.components.after.v1",
        "status-page.incident.after.v1",
        "status-page.footer.before.v1",
        "overview.dashboard.after.v1",
        "incident.sidebar.after.v1",
        "incident.composer.fields.after.v1",
        "component.row.actions.v1",
        "subscriber.detail.after.v1",
        "settings.sections.v1",
        "mail.sender.v1",
        "ai.provider.v1"
      ],
      "limits": {
        "status": "Mixed evidence: limits marked estimate:true are modeled or locally measured only and still require deployment- or account-specific validation; estimate:false values are enforced configuration limits. Each entry records its evidence.",
        "subscribers": {
          "value": 1000,
          "estimate": true,
          "why": "Local D1 engine benchmark: 1,000 active email subscribers were captured into frozen envelopes and dispatched with persisted provider attempts in 224,093ms through a test-only 100ms acknowledgement and Resend's documented default 10 request/second rate model (observed peak 5/s). This is not an account-specific provider acceptance guarantee: configured team quota, external latency, DKIM header coverage, tracking settings, domain authentication, and recipient delivery require deployment verification."
        },
        "components": {
          "value": 200,
          "estimate": true
        },
        "componentGroups": {
          "value": 25,
          "estimate": true
        },
        "openIncidents": {
          "value": 25,
          "estimate": true
        },
        "incidentUpdatesPerIncident": {
          "value": 200,
          "estimate": true
        },
        "publicPageRequestsPerSecond": {
          "value": 5000,
          "estimate": true,
          "why": "Snapshot served from cache; PAGE-008 requires 200 for all of them."
        },
        "subscribeRequestsPerMinutePerAddress": {
          "value": 5,
          "estimate": false,
          "why": "Rate limit on a BASE-ACCESS-003 write path; not a capacity number."
        },
        "intakeRequestsPerMinutePerSource": {
          "value": 120,
          "estimate": true
        },
        "uploadMaxBytes": {
          "value": 2097152,
          "estimate": false,
          "why": "Page logo only (BASE-INPUT-003)."
        },
        "uploadTypes": {
          "value": [
            "image/png",
            "image/jpeg",
            "image/webp"
          ],
          "estimate": false,
          "why": "No SVG: BASE-INPUT-002 would have to sanitise a scripting format for one logo, on the one surface that must never run anything unexpected."
        },
        "snapshotMaxBytesGzipped": {
          "value": 51200,
          "estimate": false,
          "why": "PAGE-003."
        }
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "Automated Axe WCAG 2.2 AA checks run in pnpm verify against test-owned local public status, permanent incident/history and valid subscription-token pages, sign-in, every current authenticated operator route, and an allow-listed widget surface. They do not replace manual keyboard or assistive-technology review, which remains outstanding.",
        "includes": [
          "operator app",
          "public status page",
          "embeddable widget"
        ]
      },
      "landing": "assets/seeds/lamp/01-public-status-active-desktop.png",
      "shots": [
        "assets/seeds/lamp/01-public-status-active-desktop.png",
        "assets/seeds/lamp/04-overview-active.png",
        "assets/seeds/lamp/05-incident-composer-draft.png",
        "assets/seeds/lamp/06-components-standing-claim.png",
        "assets/seeds/lamp/07-maintenance.png",
        "assets/seeds/lamp/08-subscribers.png"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/lamp",
        "release": "1.1.0",
        "manifestSha256": "fbc84e39fd6b9aebe9e165446e7cc1f9cdf34ac41a993458317f20835f7a37e5"
      }
    },
    {
      "id": "pencil",
      "authoring": {
        "mode": "legacy",
        "agentSource": "AGENTS.consumer.md"
      },
      "name": "Pencil",
      "dir": "typeform",
      "category": "forms",
      "categoryLabel": "Forms",
      "spine": {
        "id": "forms",
        "record": "form response"
      },
      "replaces": [
        {
          "name": "Typeform",
          "edition": null
        }
      ],
      "version": "0.0.0",
      "oneLiner": "A self-hosted feedback-to-action loop for one business: ask a short adaptive flow, surface evidence, and own the next action.",
      "summary": "A self-hosted feedback-to-action loop for one business: ask a short adaptive flow, surface evidence, and own the next action.",
      "scope": "One business, one shared workspace, equal operators with no roles.",
      "maturity": "contract-drafted-native-ui-thin-spine",
      "clauseCount": 104,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Configured public landing.",
          "text": "`/` renders the owner-configured public surface using the same respondent experience and admission state as the configured published flow's canonical direct link. The sample configuration selects the sample published flow. If that flow is closed, archived, or missing, `/` uses the same closed or 404 outcome as its direct link. The owner may configure the displayed business name, one supporting line, theme, and the optional default-on static `Powered by Pencil · runeditrun.com` credit. The landing contains no vendor marketing, pricing, invented metrics, testimonials, or third-party requests."
        },
        {
          "id": "SHELL-001",
          "title": "Operator shell.",
          "text": "`/app` and every route below it require a valid operator session before the operator document or API data is served. A valid session receives labelled, keyboard-operable navigation to the implemented operator surfaces; the public respondent experience is never an operator navigation destination. `/signin` remains sessionless and is the only entry point to the operator session flow, so an unauthenticated request never receives the operator shell as a fallback."
        },
        {
          "id": "OPER-001",
          "title": "Equal operators.",
          "text": "Every operator can see and act on every flow, audience, response, analysis, and action. There are no roles."
        },
        {
          "id": "OPER-002",
          "title": "Management.",
          "text": "Operators are invited and removed in settings. Any operator may invite another and may remove any other operator, except that the setup operator can be removed only by themselves and the last operator cannot be removed. There are no roles (OPER-001), so there is no seam that narrows who may do this."
        },
        {
          "id": "OPER-003",
          "title": "Sign-in depends on mail.",
          "text": "`BASE-ACCESS-001` magic links are delivered through `mail.sender.v1`, so mail is a required external, not an optional one: startup fails under WS-003 naming the missing variable when `MAIL_PROVIDER`, `MAIL_API_KEY`, or `MAIL_FROM` is absent. No deployment starts in a state where no operator can sign in."
        },
        {
          "id": "WS-001",
          "title": "Workspace time zone.",
          "text": "Settings hold one IANA time zone for the workspace, defaulting to `UTC`. Every deadline, sending window, digest send time, and operator-facing date and time is evaluated and displayed in it. A revision records the zone in force when its deadline was set, so changing the workspace zone later cannot move a deadline on an open revision."
        },
        {
          "id": "WS-002",
          "title": "Sending hours.",
          "text": "Settings hold a daily sending window in the WS-001 zone, defaulting to 09:00–17:00 every day. Scheduled mail — reminders and digests — that falls due outside the window is held and sent when the window next opens; nothing is dropped for falling outside it. Mail an operator triggers directly, and every magic link, is sent immediately regardless of the window."
        },
        {
          "id": "WS-003",
          "title": "Optional externals are configuration, not degradation.",
          "text": "*Supersedes `BASE-SECRET-002`.* Pencil starts only when every variable marked required in `seed.json → env` is present, and fails at startup naming the first missing one. `ai.provider.v1` and `task.destination.v1` are declared optional: with either absent the deployment starts and serves, every clause that does not name that adapter holds in full, and each feature that does name it is shown as unavailable alongside the variables that would enable it. No feature silently produces a reduced result instead."
        },
        {
          "id": "DASH-001",
          "title": "First flow.",
          "text": "A workspace with no flows shows an empty state that explains Ask → Signal → Act and creates a draft flow."
        },
        {
          "id": "DASH-002",
          "title": "Flow list.",
          "text": "Operators can list flows by draft, open, closed, or archived state, sorted by last activity, with completed-response count, audience, and analysis status visible. An invited flow also shows completions divided by launched participants not bounced or opted out; zero eligible participants shows `0 / 0` with no percentage. An anonymous public flow shows starts and completions without inventing a target denominator."
        },
        {
          "id": "FLOW-001",
          "title": "Lifecycle.",
          "text": "A new flow is draft; publishing makes it open; an open flow can close; a closed flow can reopen the same published revision only after no deadline or target condition remains satisfied, or it can archive; a draft can archive; and an archived flow cannot reopen. Draft public URLs return 404, closed revisions show the standard closed page, and archived public URLs return 404. Archiving removes the flow from default operator views without deleting its revisions, responses, analyses, or actions."
        },
        {
          "id": "FLOW-002",
          "title": "Ordered steps.",
          "text": "A draft contains an ordered list of steps: at most one welcome screen, which is optional and can only be first; one or more questions; and one or more named completion screens. Operators can add, edit, duplicate, reorder, and delete them."
        },
        {
          "id": "FLOW-003",
          "title": "Question types.",
          "text": "A question is short text, long text, email, single choice, multiple choice, rating, or priority; it has a prompt, optional why-we-ask text, required flag, and type-specific options within the limits in `seed.json`."
        },
        {
          "id": "FLOW-004",
          "title": "Saved edits.",
          "text": "A field-valid builder write includes the draft version it read and may leave the graph incomplete until publication. A current-version edit is persisted before the builder shows it as saved; a stale edit is rejected with the latest version and cannot overwrite another operator's edit. Leaving or reloading after the saved indicator cannot lose the accepted edit."
        },
        {
          "id": "FLOW-005",
          "title": "Branching.",
          "text": "A single-choice answer can match equality, a rating or priority answer can match equality or ordered comparison, and a multiple-choice answer can match any or all selected stable choice IDs. Preview shows the ordered rule evaluations and chosen next step used by the published runtime. *Policy: `flow.branch-selection.v1`; default: first matching rule in displayed order, then the required default path.*"
        },
        {
          "id": "FLOW-006",
          "title": "Publish validation.",
          "text": "Publishing runs every FLOW-012 through FLOW-014 validation and is rejected with all offending stable step IDs and reasons; rejection creates no revision and leaves the draft editable."
        },
        {
          "id": "FLOW-007",
          "title": "Preview isolation.",
          "text": "Preview runs the current draft with test answers, identifies itself as preview, sends no invitations or external effects, and never creates a response or changes result counts."
        },
        {
          "id": "FLOW-008",
          "title": "Published revisions.",
          "text": "Publishing creates an immutable numbered revision and makes it the current revision resolved by the flow's canonical direct and embed URL. Every invitation, first-interaction session permitted by RESP-013, completed response, analysis, and export records the revision it used; merely loading the page creates no record."
        },
        {
          "id": "FLOW-009",
          "title": "Live edits.",
          "text": "Editing an open flow changes a new draft only. Existing invitations and first-interaction sessions stay pinned to their recorded revision; publishing the draft affects only later direct or embedded sessions and later invitation launches."
        },
        {
          "id": "FLOW-010",
          "title": "Closure.",
          "text": "An operator may close a flow immediately or configure a revision-specific deadline or completed-response target. A deadline is an instant in the WS-001 zone recorded with that revision. A manual flow close closes every revision's admission; a deadline or target closes only that revision's admission atomically."
        },
        {
          "id": "FLOW-011",
          "title": "Duplication.",
          "text": "Duplicating a flow creates an unlinked draft containing its latest draft or published structure and settings, but no audience members, invitations, responses, analyses, actions, delivery history, or external IDs."
        },
        {
          "id": "FLOW-012",
          "title": "Valid references.",
          "text": "Every branch predicate references an existing structured question and stable choice when applicable, and every branch destination references an existing question or completion screen."
        },
        {
          "id": "FLOW-013",
          "title": "Reachable termination.",
          "text": "The flow's first step is its welcome screen or its first question, every step is reachable from it on at least one path, and every possible path reaches one completion screen without a cycle."
        },
        {
          "id": "FLOW-014",
          "title": "Valid questions.",
          "text": "Every question has a non-empty prompt within the text limit, every structured question has valid distinct options within its type limits, and the flow contains at least one question."
        },
        {
          "id": "FLOW-015",
          "title": "Stable structure IDs.",
          "text": "A step and each of its choices receive stable IDs when created; editing, reordering, publishing, exporting, and importing preserve them, while duplication creates new IDs."
        },
        {
          "id": "FLOW-016",
          "title": "Published policy snapshot.",
          "text": "A published revision stores the evaluated configuration for `flow.branch-selection.v1`, `response.identity.v1`, and `flow.response-admission.v1`; later policy changes affect only a newly published revision."
        },
        {
          "id": "FLOW-017",
          "title": "Revision admission.",
          "text": "A new direct or embedded visit starts the current revision; an invitation starts its recorded revision; and a returning first-interaction session resumes its recorded revision. A superseded revision cannot start a new anonymous session and remains reachable only through an existing invitation or session until its admission closes."
        },
        {
          "id": "FLOW-018",
          "title": "Revision counts.",
          "text": "Deadline and target admission checks use completed responses for that revision only. Reaching either condition cannot close another revision, while manual close and archive close all revisions."
        },
        {
          "id": "FLOW-019",
          "title": "Answer values by type.",
          "text": "A stored answer is exactly one of: for short text and long text, a string within that type's character limit in `seed.json → limits`; for email, a syntactically valid address stored as the respondent typed it and compared case-insensitively; for single choice, one stable choice ID belonging to that question; for multiple choice, a set of distinct stable choice IDs belonging to that question, within its configured minimum and maximum selections; for rating, an integer within the question's configured scale; for priority, a complete ordering of that question's stable choice IDs with no repetition and no omission. Nothing else is a valid answer."
        },
        {
          "id": "FLOW-020",
          "title": "Required applies to the path taken.",
          "text": "A required question blocks progress only on a path that reaches it. A question that the respondent's answers route around is never asked, never recorded as unanswered or missing, and never blocks final submission; publish validation does not require a required question to appear on every path."
        },
        {
          "id": "FLOW-021",
          "title": "Deleting a question keeps its answers.",
          "text": "Deleting a step in a draft changes only that draft. Answers already collected keep their stable step ID and the revision that asked them, stay in the ledger, results, exports, and existing analyses, and are never rewritten or removed by a later publication. A question first added in a later revision is never reported as unanswered by responses to an earlier revision that did not contain it."
        },
        {
          "id": "AUD-001",
          "title": "Contacts.",
          "text": "Operators can create, edit, import, and delete contacts with name, email, and custom fields. Email addresses compare case-insensitively and an import reports, rather than silently duplicating, matching contacts."
        },
        {
          "id": "AUD-002",
          "title": "Segments.",
          "text": "A segment filters contacts by stored fields. Launching invitations records the participant set plus evaluated `flow.audience-eligibility.v1` and `flow.reminder-schedule.v1` configuration, so later contact, segment, or policy changes do not rewrite the launch; AUD-006 still enforces current hard consent and existence checks at send time."
        },
        {
          "id": "AUD-003",
          "title": "Participants.",
          "text": "Each invited contact becomes one participant with an opaque invitation token, independent queued/sent/bounced/failed delivery status, eligible/opted-out consent status, and not-started/started/completed response status. Delivery attempts are append-only, and names, emails, and other contact values never appear in the public URL."
        },
        {
          "id": "AUD-004",
          "title": "Eligibility.",
          "text": "Invitations are created only for contacts allowed by the audience policy; that policy may narrow but never admit a missing-email or opted-out contact. *Policy: `flow.audience-eligibility.v1`; default: also exclude contacts already invited to that published revision.*"
        },
        {
          "id": "AUD-005",
          "title": "Launch idempotency.",
          "text": "Repeating the same audience-launch command for one revision returns the original launch and cannot create duplicate participants, invitation jobs, or delivery attempts."
        },
        {
          "id": "AUD-006",
          "title": "Send-time eligibility.",
          "text": "Immediately before each invitation or reminder, Pencil verifies that the contact still exists and has not opted out. Mail uses the participant email snapshot displayed and recorded at launch; editing the contact later never silently reroutes an active participant."
        },
        {
          "id": "DIST-001",
          "title": "Public link.",
          "text": "Every open revision has a canonical direct link that renders the respondent experience without an operator session."
        },
        {
          "id": "DIST-002",
          "title": "Embed.",
          "text": "One asynchronous script or HTTPS iframe embeds the same respondent experience. It does not block the host page, and direct and embedded submissions produce the same response data and validation."
        },
        {
          "id": "DIST-003",
          "title": "Invitations.",
          "text": "An operator can send an invitation to eligible participants. Each message names the business and flow, contains that participant's opaque link, and records a durable delivery attempt before calling `mail.sender.v1`."
        },
        {
          "id": "DIST-004",
          "title": "Reminders.",
          "text": "Reminders are sent only to sent participants who have not completed, opted out, bounced, or reached a closed or archived flow. Completion cancels pending reminders. *Policy: `flow.reminder-schedule.v1`; default: one reminder 2 days after the invitation, released within the WS-002 sending hours.*"
        },
        {
          "id": "DIST-005",
          "title": "Opt-out.",
          "text": "Every invitation and reminder includes an opt-out link. Opting out prevents future flow mail to that contact regardless of policy. Renewed consent records its actor, time, and source before mail can resume; opt-out does not delete an existing response."
        },
        {
          "id": "DIST-006",
          "title": "Mail retry.",
          "text": "A transient `mail.sender.v1` failure is retried at most 5 times over 1 hour using the original delivery idempotency key."
        },
        {
          "id": "DIST-007",
          "title": "Adapter isolation.",
          "text": "Mail cannot be unconfigured (OPER-003). A failing `mail.sender.v1` disables invitations, reminders, notification mail, and new magic-link sign-ins with a named error visible to operators, while existing operator sessions, direct links, embeds, response collection, results, the ledger, and exports keep working. Recovery is restoring the sender; Pencil offers no password or bypass sign-in."
        },
        {
          "id": "DIST-008",
          "title": "Terminal delivery.",
          "text": "A permanent mail failure or exhausted retry is marked failed with the provider reason and retry action visible to operators; no invitation is recorded as sent merely because it was queued."
        },
        {
          "id": "DIST-009",
          "title": "Embed isolation.",
          "text": "The embed rejects a host origin not allowed by BASE-PUBLIC-002 before rendering the flow. It exposes no answer, response ID, participant token, or contact value to the host URL, DOM, or `postMessage`; host communication is limited to non-identifying loaded, height, and completed events."
        },
        {
          "id": "DIST-010",
          "title": "Ambiguous mail outcome.",
          "text": "`mail.sender.v1` implements send-or-get by delivery idempotency key when its provider supports lookup. An ambiguous timeout without lookup is marked unknown and is not automatically retried; an operator may reconcile or explicitly resend it with the duplicate-delivery risk shown."
        },
        {
          "id": "RESP-001",
          "title": "One question at a time.",
          "text": "A respondent sees one welcome, question, or completion screen at a time, with no builder or operator controls."
        },
        {
          "id": "RESP-002",
          "title": "Truthful progress.",
          "text": "The page shows the respondent's current position and remaining reachable questions for the answers already given. Branching can change that count, and skipped or unreachable questions are never counted."
        },
        {
          "id": "RESP-003",
          "title": "Answers.",
          "text": "The respondent can submit only the values FLOW-019 defines for the published question's type and configured limits. Rating, priority, and choice questions begin unselected and require affirmative respondent selection; a required unanswered or invalid question stays on screen with a field-specific error and creates no response."
        },
        {
          "id": "RESP-004",
          "title": "Back navigation.",
          "text": "A respondent can move back through screens already visited in the current session and edit an answer. Pencil recomputes the forward path and progress and discards answers belonging to steps that become unreachable before final validation, export, or analysis."
        },
        {
          "id": "RESP-005",
          "title": "Privacy disclosure.",
          "text": "Before the first answer, the flow states why feedback is requested, whether the response is anonymous or identified, who can see individual answers, and each configured external and data category declared in `seed.json → externals` that may receive the answers. The policy decides which identity mode applies; it can never suppress or weaken the disclosure of the mode it chose. *Policy: `response.identity.v1`; default: public-link responses are anonymous and invited responses are identified.*"
        },
        {
          "id": "RESP-006",
          "title": "Anonymous means anonymous.",
          "text": "An anonymous response records no contact link, no participant, no name or email taken from an invitation, contact record, or session, no raw IP address, and no host-page identifier. It receives a random response ID that cannot be reversed to identify the respondent; expiring rate-limit and session state is not linked to that ID. An email or name the respondent knowingly types into a question is stored as that answer and nothing else: it creates no contact and no participant, and RESP-005's disclosure states before the flow starts that answering such a question identifies the respondent to the operator."
        },
        {
          "id": "RESP-007",
          "title": "Final submission.",
          "text": "A completed response, all its answers, count change, and durable outbox commands for required downstream effects are committed in one transaction before any effect is dispatched."
        },
        {
          "id": "RESP-008",
          "title": "Admission per invitation.",
          "text": "An invitation admits only the completions its recorded admission rule allows. A visit beyond that shows the existing completion state and creates no further response, and no rule can admit a completion after the flow's admission has closed (RESP-009). *Policy: `flow.response-admission.v1`; default: one completion per invitation; anonymous direct links allow multiple independent completions.*"
        },
        {
          "id": "RESP-009",
          "title": "Concurrent closure.",
          "text": "Final submission rechecks deadline, target, and manual closure in the same admission transaction. A submission that loses the final available place sees the standard closed page naming the business and flow and creates no response."
        },
        {
          "id": "RESP-010",
          "title": "No silent partials.",
          "text": "Answers from an abandoned or reloaded session are not visible to operators, included in results, exported as responses, or counted as completions. Only final submission creates response data in the deployment."
        },
        {
          "id": "RESP-011",
          "title": "Completion.",
          "text": "After a successful submission the respondent sees the published completion screen and cannot edit the completed response through the public link."
        },
        {
          "id": "RESP-012",
          "title": "Submission retry.",
          "text": "A submission idempotency key is scoped to one respondent session and published revision and retained with the completed response. Retrying that key returns the same response ID and completion screen and cannot increment counts or dispatch downstream effects twice."
        },
        {
          "id": "RESP-013",
          "title": "Session metrics.",
          "text": "The first respondent interaction creates an expiring session record containing only an opaque session ID, revision ID, source, start time, the stable ID of the furthest step reached, and a device class of mobile, tablet, or desktop—never answers, contact identity, raw IP, user-agent string, or host identity. It pins the revision and supplies starts, drop-off, device, and completion-time metrics; an abandoned session expires under BASE-DATA-002 after the retention period in `seed.json → limits`."
        },
        {
          "id": "RESP-014",
          "title": "Submit failure.",
          "text": "A final-submit request that is not durably committed leaves the respondent off the completion screen, preserves current in-browser answers, shows a retryable error, and retries with the original submission idempotency key."
        },
        {
          "id": "RESP-015",
          "title": "Abuse defences.",
          "text": "Every public submission is checked by defences the deployment owns entirely: a hidden honeypot field, a minimum elapsed time from session start to final submission taken from `seed.json → limits`, a submission token bound to the RESP-013 session, and the public rate limits enforced under BASE-ACCESS-003. Pencil loads no third-party challenge, script, or reputation service on a public surface, so BASE-PUBLIC-001 continues to hold. A submission whose session token is missing or unrecognised is refused under RESP-014 with a visible, retryable error naming the expired session, never silently discarded. Scoring a respondent's trustworthiness beyond these checks is out of scope."
        },
        {
          "id": "RESP-016",
          "title": "Bot submissions are dropped.",
          "text": "A submission that fills the honeypot is shown the ordinary completion screen and creates no response, answer, count change, session-metric change, or downstream effect. The drop records an audit entry under BASE-OPS-005 naming the defence that fired, the revision, and the time, and containing no answer text or respondent identity. The honeypot is the only check whose failure discards a submission."
        },
        {
          "id": "RESP-017",
          "title": "Suspected submissions are quarantined, not lost.",
          "text": "A submission that fails only the minimum-elapsed-time check is committed under RESP-007 as a quarantined response. It is stored complete with its answers, is excluded from completed-response counts, admission checks, results, analysis evidence sets, notifications, and response exports, and is never destroyed automatically."
        },
        {
          "id": "RESP-018",
          "title": "Quarantine review.",
          "text": "Operators can list quarantined responses for a revision with their answers and the defence that quarantined them, and can release or delete each one. Releasing makes the response an ordinary completed response from that moment, and every affected count, rate, and results figure is recomputed; deleting follows DATA-002. A release is allowed after the revision's admission has closed, is recorded with its operator and time, and never reopens admission or re-triggers a deadline or target check. Nothing leaves quarantine without an operator acting."
        },
        {
          "id": "RESP-019",
          "title": "Retained flow metrics.",
          "text": "When a session completes or expires, its start, furthest-reached step ID, device class, and completion outcome are folded into per-revision counters that hold counts only, cannot be traced to a session, response, or respondent, and are retained with the revision after the session record has expired. These counters are the source of every historical starts, drop-off, and device figure; no partial answer is retained with them."
        },
        {
          "id": "LEDG-001",
          "title": "Ledger.",
          "text": "Operators can list and open every released completed response with stable response ID, flow revision, source, participant when identified, started and submitted times, and each answer keyed by stable step ID. A quarantined response appears only in the RESP-018 review list until it is released."
        },
        {
          "id": "LEDG-002",
          "title": "Counts.",
          "text": "Completed-response and participant counts are derived from stored participants and completed responses. Starts come only from RESP-013 session records and the RESP-019 counters they fold into; preview sessions, duplicate submissions, failed deliveries, abandoned sessions, dropped submissions, and unreleased quarantined responses never increment completed responses."
        },
        {
          "id": "LEDG-003",
          "title": "Filters.",
          "text": "Operators can filter the ledger by revision, submission time, source, participant status, and structured answer, while preserving the same response IDs shown in exports and evidence."
        },
        {
          "id": "LEDG-004",
          "title": "Response export.",
          "text": "Starting an export fixes its filter and evidence-cutoff time. CSV and JSON contain exactly the matching released responses committed by that cutoff, including response and revision IDs, source, timestamps, stable step IDs and labels, and lossless values for every answer including open text. Quarantined responses are excluded until released, and the export names how many were excluded."
        },
        {
          "id": "RSLT-001",
          "title": "Response summary.",
          "text": "Every published revision has a results view that needs no `ai.provider.v1`: it shows completed responses, starts, completion rate, and average time to complete for the current filter, each beside the counts it was computed from. It is available from publication, shows zeros rather than an error before the first session, and never blocks on an analysis run."
        },
        {
          "id": "RSLT-002",
          "title": "Drop-off by question.",
          "text": "Results show, for each step on the published revision, how many sessions reached it and how many of those reached no later step, computed from RESP-019 counters and therefore still available after the underlying sessions have expired. A step no session reached is shown as reached by zero, not omitted."
        },
        {
          "id": "RSLT-003",
          "title": "Stated denominators.",
          "text": "Every percentage Pencil displays in results or analysis names the numerator and denominator it was computed from. A single-answer distribution divides by the responses that answered that question and its parts sum to that denominator. A multiple-choice distribution divides by the same denominator, may therefore total more than 100%, and is labelled as multiple-select. Completion rate divides completed responses by starts for the same revision and filter, and is never mixed with a per-question answer rate. Changing a filter changes both numerator and denominator together."
        },
        {
          "id": "RSLT-004",
          "title": "Device split.",
          "text": "Results show the share of sessions in each RESP-013 device class, divided by the sessions counted for that revision and filter and named as RSLT-003 requires. Device class is derived at session start, is never stored on a response, and never appears in an export of responses."
        },
        {
          "id": "RSLT-005",
          "title": "Results across revisions.",
          "text": "A results view spanning more than one revision reports each question only for the revisions that contained it, using the responses that could have answered it as the denominator. It never treats a question added later as unanswered by earlier responses, and it never merges two questions that have different stable step IDs."
        },
        {
          "id": "ANLY-001",
          "title": "Analysis run.",
          "text": "An operator can request analysis for any revision with completed responses. A run is queued durably and shows queued, running, completed, insufficient-evidence, or failed status. *Policy: `analysis.autorun.v1`; default: run when a flow closes and has at least 5 completed responses.*"
        },
        {
          "id": "ANLY-002",
          "title": "Fixed evidence set.",
          "text": "An analysis records the exact response IDs and revision it read, and reads only released responses. Responses submitted, or released from quarantine, after it started are excluded and make the completed analysis stale rather than changing it in place."
        },
        {
          "id": "ANLY-003",
          "title": "Findings.",
          "text": "A run that reaches completed status ranks themes and selects one primary finding from its fixed evidence set; an insufficient-evidence run follows ANLY-013 instead."
        },
        {
          "id": "ANLY-004",
          "title": "Exact evidence.",
          "text": "Text presented as a respondent quote is an exact excerpt of a stored answer. Generated paraphrases and summaries are labelled as analysis and never presented in quotation marks or attributed to a respondent."
        },
        {
          "id": "ANLY-005",
          "title": "Disclosure threshold.",
          "text": "In every analysis view, a theme or filtered segment with fewer included responses than the threshold shows no raw excerpt, participant identity, unique field combination, or breakdown; exactly the threshold is permitted. Suppression changes presentation, not the raw ledger or operator export. *Policy: `analysis.disclosure.v1`; default threshold: 5 completed responses in that theme or current filter cohort.*"
        },
        {
          "id": "ANLY-006",
          "title": "Recommendations.",
          "text": "A completed analysis with sufficient evidence may propose one primary action with rationale, linked findings, confidence, expected impact, and effort; an insufficient-evidence run proposes none. *Policy: `recommendation.selection.v1`; default: the action supported by the highest-impact finding with sufficient evidence.*"
        },
        {
          "id": "ANLY-007",
          "title": "Staleness.",
          "text": "A stale analysis remains readable and labelled with its evidence cutoff. Operators can run a new analysis; prior runs remain in history except for the erasure redaction required by DATA-002."
        },
        {
          "id": "ANLY-008",
          "title": "Provider failure.",
          "text": "If `ai.provider.v1` is unconfigured or fails an analysis or builder-assistance request, the request is marked failed with the error and retry action, no suggestion, finding, or recommendation is fabricated, and flow publishing, response collection, the ledger, and exports continue to work."
        },
        {
          "id": "ANLY-009",
          "title": "Builder assistance.",
          "text": "An operator may request a sharper, broader, or contextual rewrite of a draft question. Suggestions show their source text and are never applied or published without an operator accepting them."
        },
        {
          "id": "ANLY-010",
          "title": "Cost.",
          "text": "Every AI call records provider, model, input and output usage, feature, analysis or draft ID, and reported cost. Operators can view totals by day and feature."
        },
        {
          "id": "ANLY-011",
          "title": "Evidence provenance.",
          "text": "Each displayed theme count, percentage, supporting excerpt, and recommendation records the included response IDs from which it was derived; an operator can navigate from displayed evidence to those retained responses."
        },
        {
          "id": "ANLY-012",
          "title": "Output validation.",
          "text": "Before a run completes, Pencil validates its schema, verifies that cited response IDs belong to the run's evidence set, verifies quote ranges exactly against stored answers, and recomputes counts and percentages over that evidence set; any failure marks the run failed."
        },
        {
          "id": "ANLY-013",
          "title": "Insufficient evidence.",
          "text": "A run whose evidence or disclosure-eligible cohorts cannot support a finding completes as `insufficient-evidence`, shows the evidence cutoff and reason, and emits no theme, quote, or recommendation."
        },
        {
          "id": "ANLY-014",
          "title": "Untrusted evidence.",
          "text": "Respondent text is treated only as data: it cannot change system instructions or configuration, select tools, call adapters, create jobs, or authorize effects; schema-invalid provider output fails under ANLY-012."
        },
        {
          "id": "ANLY-015",
          "title": "Analysis policy snapshot.",
          "text": "Each run stores the evaluated `analysis.disclosure.v1` and `recommendation.selection.v1` configuration; later policy changes affect only a new run and never retroactively reveal or reinterpret historical output."
        },
        {
          "id": "ANLY-016",
          "title": "Provider retry.",
          "text": "A transient `ai.provider.v1` transport failure is retried at most 3 times over 10 minutes with the same request, evidence set, and policy snapshot; schema or evidence-validation failures are terminal and never retried as transport failures."
        },
        {
          "id": "ACT-001",
          "title": "Action record.",
          "text": "An operator can accept a recommendation as an action with title, rationale, owner, target metric, review date, and links to its analysis and evidence. Accepting does not itself contact an external service."
        },
        {
          "id": "ACT-002",
          "title": "Action lifecycle.",
          "text": "A new action is planned. Legal transitions are planned → in progress or closed, in progress → ready for review or closed, ready for review → effective, ineffective, in progress, or closed, and effective or ineffective → closed; every transition records the operator, time, and optional outcome note."
        },
        {
          "id": "ACT-003",
          "title": "External task.",
          "text": "`task.destination.v1` must implement create-or-get by Pencil's idempotency key and return the existing remote task ID for a repeated key; repeated activation or delivery of one action therefore cannot create more than one external task."
        },
        {
          "id": "ACT-004",
          "title": "Task failure.",
          "text": "An unconfigured adapter disables external task creation with setup guidance. A failed call keeps the local action, records the attempt and error, and offers a retry that reuses the same idempotency key."
        },
        {
          "id": "ACT-005",
          "title": "Follow-up measurement.",
          "text": "An action records a metric name, unit, desired direction, baseline value, source, and observation time. An operator can link a later flow or analysis, record the comparable observed value and time, and mark the outcome effective or ineffective. Pencil never sets or changes that verdict itself, and no extension point can; a review date that has passed is surfaced on the action, never resolved on the operator's behalf."
        },
        {
          "id": "ACT-006",
          "title": "Confirmed task payload.",
          "text": "Before calling `task.destination.v1`, Pencil shows the action fields and operator-authenticated report link that will leave the deployment and sends only those confirmed values; raw answers and excerpts are never included."
        },
        {
          "id": "ACT-007",
          "title": "Task delivery state.",
          "text": "External task delivery records pending, confirmed, unknown, or failed status, the idempotency key, every attempt, and the remote task ID when confirmed. An unknown network outcome is retried with the same key rather than assumed failed or sent again with a new key."
        },
        {
          "id": "NOTIFY-001",
          "title": "Analysis attention.",
          "text": "The in-app notification centre records one notification for the requesting operator when an analysis completes or fails; an automatic close-time analysis records one for every unmuted operator. Duplicate queue delivery cannot create the same notification twice."
        },
        {
          "id": "NOTIFY-002",
          "title": "New-response mail.",
          "text": "Each operator sets, per flow, whether completed responses are mailed to them immediately, summarised in one daily digest, or not mailed. The operator who published the flow starts on the digest; every other operator starts on nothing. A message names the business and flow and links to the ledger; it never carries answer text, respondent identity, or a link that grants access without signing in."
        },
        {
          "id": "NOTIFY-003",
          "title": "Digest.",
          "text": "A digest is one message per operator per flow per day, sent at the WS-002 window's opening hour in the WS-001 zone, covering the completed responses committed since the previous digest. A day with no new completed responses sends no message, and a digest that fails to send is retried under DIST-006 rather than merging into the next day's."
        },
        {
          "id": "NOTIFY-004",
          "title": "No mail loops.",
          "text": "Every notification, digest, invitation, and reminder is sent from the configured no-reply address and marked auto-generated. Pencil ingests no inbound mail, so a reply or vacation auto-responder can never create a response, participant, notification, or further outbound mail. A bounce of operator notification or digest mail is recorded against that operator's mail setting only and never marks a participant bounced; participant bounce status comes only from invitation and reminder delivery (DIST-008)."
        },
        {
          "id": "DATA-001",
          "title": "Export contents.",
          "text": "The deployment export contains every operator, contact, segment, flow and revision, step and branch, participant and delivery attempt, response and answer including quarantined ones with their state, RESP-019 revision counters, analysis and evidence link, recommendation, action, notification, setting, audit entry, and referenced file as original data. Import preserves Pencil's stable graph, revision, response, evidence, recommendation, and action linkages."
        },
        {
          "id": "DATA-002",
          "title": "Response erasure.",
          "text": "Deleting a response, or a contact that owns it, revokes and deletes active invitation tokens, removes contact links or anonymises retained delivery facts, and removes its answers, raw excerpts, and evidence links from every analysis within 5 minutes. Affected aggregates are recomputed or marked unavailable, affected analyses are labelled redacted and stale, and only the anonymised audit tombstone required by BASE-DATA-003 remains."
        }
      ],
      "hasReadme": false,
      "hasBaseline": true,
      "nonGoals": [
        "Multiple workspaces",
        "Roles and permissions",
        "Agency management",
        "Payments and order forms",
        "Appointment scheduling",
        "Public template marketplace",
        "Survey-panel recruitment",
        "Native mobile apps",
        "Video, audio, signature, and file-upload questions",
        "Single sign-on",
        "Advanced fraud scoring",
        "Broad marketing analytics",
        "Live collaborative editing",
        "Arbitrary automation builder",
        "Storing or reporting the answers of an abandoned session"
      ],
      "externals": [
        {
          "name": "mail",
          "required": true,
          "requiredWhen": "",
          "reason": "Cloudflare has no owned primitive that delivers transactional email to external recipients, and BASE-ACCESS-001 magic-link sign-in is delivered by mail, so no deployment can run without it.",
          "data": [
            "operator email and magic-link sign-in token",
            "participant email and display name",
            "business and flow name",
            "opaque invitation or opt-out link",
            "invitation, reminder, notification, or digest copy"
          ],
          "adapters": [
            "mail.sender.v1"
          ]
        },
        {
          "name": "ai",
          "required": false,
          "requiredWhen": "",
          "reason": "Evidence synthesis and draft-question assistance require a model provider; no model is bundled with the deployment.",
          "data": [
            "draft question text for requested assistance",
            "published revision prompts and settings",
            "completed answer text and structured values selected for an analysis",
            "stable response IDs without contact identity"
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        },
        {
          "name": "task-destination",
          "required": false,
          "requiredWhen": "",
          "reason": "Creating a task in a buyer-selected task system necessarily sends the confirmed action to that system.",
          "data": [
            "operator-confirmed action fields",
            "operator-confirmed report link"
          ],
          "adapters": [
            "task.destination.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "estimate",
        "freePlanDefault": true,
        "platformMonthlyUsdAtTypicalLocalOrEarlyProductionUse": {
          "min": 0,
          "max": 0
        },
        "platformMonthlyUsdAtDeclaredEstimatedLimits": {
          "min": 5,
          "max": null
        },
        "workersPaidRequiredWhen": "The first measured boundary is any of: the 100,001st dynamic Worker request in a UTC day; an invocation that cannot stay within the Free plan's 10 ms CPU limit; the 5,000,001st D1 row read or 100,001st D1 row written in a UTC day; one D1 database exceeding 500 MB or the account exceeding 5 GB; or the 10,001st Queue operation in a UTC day. A normally delivered Queue message consumes write, read, and delete operations, so 3,334 such messages cross the Queue boundary. The declared capacity targets are not Free-capable or load-tested and are not a monthly usage forecast.",
        "workersPaidMinimumMonthlyUsd": 5,
        "excludes": [
          "R2 usage above its Standard free tier",
          "AI provider usage",
          "email delivery",
          "optional task destination charges",
          "domain registration"
        ],
        "assumptions": "The current thin spine uses one Worker and D1. R2 and Queues are declared but not implemented; R2 Standard includes 10 GB-month storage, 1 million Class A operations, 10 million Class B operations, and free egress each month. No second worker, Durable Object, Workflow, KV, or hosted database is used until a measured need exists."
      },
      "extensionPoints": [
        "app.routes.v1",
        "app.navigation.v1",
        "app.settings.v1",
        "jobs.consumers.v1",
        "schedules.cron.v1",
        "flow.branch-selection.v1",
        "flow.audience-eligibility.v1",
        "flow.reminder-schedule.v1",
        "response.identity.v1",
        "flow.response-admission.v1",
        "analysis.autorun.v1",
        "analysis.disclosure.v1",
        "recommendation.selection.v1",
        "flow.created.v1",
        "flow.published.v1",
        "flow.closed.v1",
        "participant.invited.v1",
        "response.completed.v1",
        "analysis.completed.v1",
        "analysis.failed.v1",
        "action.created.v1",
        "action.reviewed.v1",
        "navigation.after.v1",
        "dashboard.after.v1",
        "flow.row.actions.after.v1",
        "flow.header.actions.after.v1",
        "flow.editor.sidebar.after.v1",
        "flow.step.settings.after.v1",
        "flow.results.header.after.v1",
        "flow.results.finding.after.v1",
        "response.question.after.v1",
        "settings.after.v1",
        "mail.sender.v1",
        "ai.provider.v1",
        "task.destination.v1"
      ],
      "limits": {
        "status": "estimated",
        "mustBeReplacedByLoadTest": true,
        "flows": {
          "total": 1000,
          "simultaneouslyOpen": 50
        },
        "flowRevisions": {
          "perFlow": 100
        },
        "steps": {
          "perRevision": 50,
          "choiceOptionsPerStep": 100,
          "priorityOptionsPerStep": 10,
          "branchRulesPerStep": 100
        },
        "contacts": {
          "total": 100000,
          "participantsPerRevision": 25000
        },
        "responses": {
          "perRevision": 100000,
          "shortTextCharactersPerAnswer": 200,
          "openTextCharactersPerAnswer": 5000
        },
        "respondentSessions": {
          "retentionDays": 30
        },
        "abuse": {
          "minimumSecondsFromSessionStartToSubmit": 3
        },
        "publicRateLimits": {
          "requestsPerIpPerMinute": 120,
          "finalSubmissionsPerIpPerMinute": 30
        },
        "operatorRateLimits": {
          "requestsPerOperatorPerMinute": 600
        },
        "p95Milliseconds": {
          "publicScreen": 500,
          "answerTransition": 500,
          "finalSubmission": 1000,
          "responseLedger": 500
        },
        "notes": "These are design targets, not proven capacities. The backend pass must load-test them, revise the values, and mark status as tested before release."
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target-not-yet-tested",
        "includes": [
          "operator app",
          "direct public flow",
          "embedded public flow",
          "email"
        ]
      },
      "landing": "assets/seeds/pencil/01-landing-desktop.png",
      "shots": [
        "assets/seeds/pencil/01-landing-desktop.png",
        "assets/seeds/pencil/03-dashboard-desktop.png",
        "assets/seeds/pencil/04-builder-desktop.png",
        "assets/seeds/pencil/05-results-desktop.png",
        "assets/seeds/pencil/qa-builder-comparison.png",
        "assets/seeds/pencil/qa-respondent-comparison.png"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/pencil",
        "release": "0.0.0",
        "manifestSha256": "832bb9b052a7d5ed85d116407f0a07792156d0f2c8d35918f8cb409510305e4f"
      }
    },
    {
      "id": "lily",
      "authoring": {
        "mode": "edition-owned",
        "agentSource": "AGENTS.md",
        "id": "runeditrun/lily",
        "parent": {
          "repository": "https://github.com/runeditrun/support-lily.git",
          "revision": "ae8426e691b35ad5fbb3ac81832a2e3e1e94660b"
        },
        "toolchain": {
          "id": "runeditrun/edition-authoring",
          "version": "1.1.0",
          "sha256": "a13d7d0bcf174618923ca5d1134575c149ea6b57c89d1640c03c1ebf091ab372"
        },
        "components": [
          {
            "id": "runeditrun/base",
            "role": "foundation",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/deployment/health.mjs",
                "scripts/deployment/base-secret-json.mjs",
                "tools/seed-schema/SEED.schema.json",
                "tools/seed-schema/json-schema.mjs",
                "tools/seed-schema/validate.mjs",
                "tools/seed-schema/RELEASE.schema.json",
                "scripts/operations.mjs",
                "src/core/platform/ticket-mail/relay-verification.ts"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "603ee025aa5991fb44e0c3b0731f2113be85ceab",
                "path": ".",
                "paths": [
                  "deployment/health.mjs",
                  "deployment/secret-json.mjs",
                  "schema/SEED.schema.json",
                  "schema/json-schema.mjs",
                  "schema/validate.mjs",
                  "schema/RELEASE.schema.json",
                  "operations/operations.mjs",
                  "infrastructure/sendgrid-event-relay/src/crypto.ts"
                ]
              },
              "mappings": [
                {
                  "path": "scripts/deployment/health.mjs",
                  "originPath": "deployment/health.mjs"
                },
                {
                  "path": "scripts/deployment/base-secret-json.mjs",
                  "originPath": "deployment/secret-json.mjs"
                },
                {
                  "path": "tools/seed-schema/SEED.schema.json",
                  "originPath": "schema/SEED.schema.json"
                },
                {
                  "path": "tools/seed-schema/json-schema.mjs",
                  "originPath": "schema/json-schema.mjs"
                },
                {
                  "path": "tools/seed-schema/validate.mjs",
                  "originPath": "schema/validate.mjs"
                },
                {
                  "path": "tools/seed-schema/RELEASE.schema.json",
                  "originPath": "schema/RELEASE.schema.json"
                },
                {
                  "path": "scripts/operations.mjs",
                  "originPath": "operations/operations.mjs"
                },
                {
                  "path": "src/core/platform/ticket-mail/relay-verification.ts",
                  "originPath": "infrastructure/sendgrid-event-relay/src/crypto.ts"
                }
              ]
            }
          },
          {
            "id": "runeditrun/support",
            "role": "family",
            "source": {
              "kind": "local",
              "paths": [
                "src/family/support",
                "composition/families/support/README.md",
                "composition/families/support/LICENSE",
                "tests/composition/support",
                "scripts/test-composition.mjs"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/support.git",
                "revision": "605495b6e0ab69d41181519bfe13141b0dd7c102",
                "path": ".",
                "paths": [
                  "core/src",
                  "core/README.md",
                  "core/LICENSE",
                  "core/tests",
                  "core/test-components.mjs"
                ]
              },
              "mappings": [
                {
                  "path": "src/family/support",
                  "originPath": "core/src"
                },
                {
                  "path": "composition/families/support/README.md",
                  "originPath": "core/README.md"
                },
                {
                  "path": "composition/families/support/LICENSE",
                  "originPath": "core/LICENSE"
                },
                {
                  "path": "tests/composition/support",
                  "originPath": "core/tests"
                },
                {
                  "path": "scripts/test-composition.mjs",
                  "originPath": "core/test-components.mjs"
                }
              ]
            }
          },
          {
            "id": "runeditrun/support-intake",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/support-intake",
                "composition/modules/support-intake/README.md",
                "composition/modules/support-intake/LICENSE",
                "tests/composition/support-intake"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/support.git",
                "revision": "605495b6e0ab69d41181519bfe13141b0dd7c102",
                "path": ".",
                "paths": [
                  "modules/support-intake/src",
                  "modules/support-intake/README.md",
                  "modules/support-intake/LICENSE",
                  "modules/support-intake/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/support-intake",
                  "originPath": "modules/support-intake/src"
                },
                {
                  "path": "composition/modules/support-intake/README.md",
                  "originPath": "modules/support-intake/README.md"
                },
                {
                  "path": "composition/modules/support-intake/LICENSE",
                  "originPath": "modules/support-intake/LICENSE"
                },
                {
                  "path": "tests/composition/support-intake",
                  "originPath": "modules/support-intake/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/support-email",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/support-email",
                "composition/modules/support-email/README.md",
                "composition/modules/support-email/LICENSE",
                "tests/composition/support-email"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/support.git",
                "revision": "605495b6e0ab69d41181519bfe13141b0dd7c102",
                "path": ".",
                "paths": [
                  "modules/support-email/src",
                  "modules/support-email/README.md",
                  "modules/support-email/LICENSE",
                  "modules/support-email/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/support-email",
                  "originPath": "modules/support-email/src"
                },
                {
                  "path": "composition/modules/support-email/README.md",
                  "originPath": "modules/support-email/README.md"
                },
                {
                  "path": "composition/modules/support-email/LICENSE",
                  "originPath": "modules/support-email/LICENSE"
                },
                {
                  "path": "tests/composition/support-email",
                  "originPath": "modules/support-email/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/sendgrid-email",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "src/modules/sendgrid-email",
                "composition/modules/sendgrid-email/README.md",
                "composition/modules/sendgrid-email/LICENSE",
                "tests/composition/sendgrid-email"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "d1343b638afd254679bc2b35819fc1d268bb6cc7",
                "path": ".",
                "paths": [
                  "modules/sendgrid-email/src",
                  "modules/sendgrid-email/README.md",
                  "modules/sendgrid-email/LICENSE",
                  "modules/sendgrid-email/tests"
                ]
              },
              "mappings": [
                {
                  "path": "src/modules/sendgrid-email",
                  "originPath": "modules/sendgrid-email/src"
                },
                {
                  "path": "composition/modules/sendgrid-email/README.md",
                  "originPath": "modules/sendgrid-email/README.md"
                },
                {
                  "path": "composition/modules/sendgrid-email/LICENSE",
                  "originPath": "modules/sendgrid-email/LICENSE"
                },
                {
                  "path": "tests/composition/sendgrid-email",
                  "originPath": "modules/sendgrid-email/tests"
                }
              ]
            }
          },
          {
            "id": "runeditrun/lily",
            "role": "edition",
            "source": {
              "kind": "local",
              "paths": [
                "src/routes",
                "src/ext",
                "migrations",
                "package.json"
              ]
            }
          },
          {
            "id": "runeditrun/owner-migrations",
            "role": "module",
            "source": {
              "kind": "local",
              "paths": [
                "scripts/deployment/owner-migrations.mjs",
                "scripts/deployment/owner-migrations.test.mjs",
                "composition/modules/owner-migrations/README.md",
                "composition/modules/owner-migrations/LICENSE"
              ],
              "origin": {
                "repository": "https://github.com/runeditrun/base.git",
                "revision": "d1343b638afd254679bc2b35819fc1d268bb6cc7",
                "path": ".",
                "paths": [
                  "deployment/owner-migrations.mjs",
                  "deployment/owner-migrations.test.mjs",
                  "deployment/OWNER-MIGRATIONS.md",
                  "LICENSE"
                ]
              },
              "mappings": [
                {
                  "path": "scripts/deployment/owner-migrations.mjs",
                  "originPath": "deployment/owner-migrations.mjs"
                },
                {
                  "path": "scripts/deployment/owner-migrations.test.mjs",
                  "originPath": "deployment/owner-migrations.test.mjs"
                },
                {
                  "path": "composition/modules/owner-migrations/README.md",
                  "originPath": "deployment/OWNER-MIGRATIONS.md"
                },
                {
                  "path": "composition/modules/owner-migrations/LICENSE",
                  "originPath": "LICENSE"
                }
              ]
            }
          }
        ],
        "manifestSha256": "3d6c2cda3a4ce793f4fc58ab053e4df186de03cceab40a84b0af2e598502987c",
        "lockSha256": "181d66c7882e6df142b37bb48aa827ea10076a4ffeb14d846ecfbccd0822de24"
      },
      "name": "Lily",
      "dir": "zendesk",
      "category": "support",
      "categoryLabel": "Support",
      "spine": {
        "id": "ticket-desk",
        "record": "ticket: a thread with classification fields, a number and an assignee"
      },
      "composition": {
        "base": "base",
        "family": "support",
        "edition": "lily",
        "modules": [
          "support-intake",
          "support-email",
          "sendgrid-email"
        ]
      },
      "replaces": [
        {
          "name": "Zendesk",
          "edition": null
        }
      ],
      "version": "0.1.0",
      "oneLiner": "A self-hostable ticket desk with numbered cases, shared views, Help Centre intake, configurable channels, service targets, customer records, and…",
      "summary": "A self-hostable ticket desk with numbered cases, shared views, Help Centre intake, configurable channels, service targets, customer records, and operational analytics.",
      "scope": "One business, one shared support workspace, with role and group access controls.",
      "maturity": "thin-real-worker-d1-ui-port; ticket-desk-contract-draft",
      "clauseCount": 109,
      "clauses": [
        {
          "id": "HOME-001",
          "title": "Deployment front door.",
          "text": "`GET /` presents the configured business name, a short configured description of its support service, links to the public Help Centre and configured public request surfaces, and a sign-in link to `/signin`. It contains no vendor pricing, trial, billing, product-switcher, or marketing claims."
        },
        {
          "id": "APP-001",
          "title": "Authenticated workspace.",
          "text": "An authorised user entering `/app` can reach Home, Views, Tickets, Customers, Help Centre, Analytics, Channels, and Administration according to their role. Navigation has a compact responsive form, global search, keyboard shortcuts for search/navigation/common ticket actions, contextual coach marks, and a dismissible setup checklist whose completed state is per Administrator. An unavailable optional channel or adapter is labelled unavailable with the configuration action rather than simulated data."
        },
        {
          "id": "ACCESS-001",
          "title": "Roles.",
          "text": "Lily has Administrator, Agent, Contributor, and Light Agent roles. Administrators manage deployment-wide settings, people, groups, forms, roles, channels, and policies. Agents work tickets and public replies. Contributors can make internal contributions only. Light Agents have the explicitly configured limited ticket access. An authenticated person may perform only actions permitted by their role and any ticket/group restriction."
        },
        {
          "id": "ACCESS-002",
          "title": "Groups and membership.",
          "text": "Administrators create, rename, retire, and populate groups. A ticket can be assigned to a group and then an agent; group assignment is visible in views, history, filters, reports, and exports. Retiring a group requires an explicit reassignment or unassignment of its open tickets and retains historical attribution."
        },
        {
          "id": "ACCESS-003",
          "title": "Authorisation and lifecycle.",
          "text": "Administrators invite, suspend, restore, and remove agents. Removing an agent unassigns their active tickets and preserves the author identity on earlier messages, notes, and audit records. The last active Administrator cannot be removed. Agents configure their own signature and notification preferences; the configured signature is appended only to their public email reply."
        },
        {
          "id": "ACCESS-004",
          "title": "Concurrent edits.",
          "text": "Each case mutation supplies the revision it was based on. A stale mutation returns `409` and makes no partial field, status, note, assignment, automation, or outbound-message change. An accepted inbound message is durable even if it races with an agent edit."
        },
        {
          "id": "TKT-001",
          "title": "Creation.",
          "text": "A human customer email to the support address or an agent's manual submission creates one ticket with an immutable ID, creation time, customer, channel, subject, initial message, declared ticket custom fields, and revision. A new ticket is Open with Normal priority unless the creating agent chooses another priority."
        },
        {
          "id": "TKT-002",
          "title": "States and pending reasons.",
          "text": "A ticket is Open, Pending, Solved, Closed, or Merged. Pending carries exactly one configured reason, and every reason declares whether it waits on the customer; a ticket is never Pending without a reason. Solved records an agent-confirmed outcome, Closed is immutable archival history, and Merged is an immutable redirect under TKT-016."
        },
        {
          "id": "TKT-003",
          "title": "Customer reply.",
          "text": "A qualifying human customer message on an Open, Pending, or Solved ticket appends to that ticket, makes it Open, and clears its pending reason. A message matching Closed creates a linked new ticket; a message matching Merged appends to the destination under TKT-016. No qualifying human customer message is discarded."
        },
        {
          "id": "TKT-004",
          "title": "Auto-close.",
          "text": "A Solved ticket closes seven days after it was last solved unless a qualifying human customer message arrives first. Closing never deletes messages, evidence, executions, or audit history. *Policy: `ticket.auto-close.v1`; default: seven days.*"
        },
        {
          "id": "TKT-005",
          "title": "Queues and views.",
          "text": "Agents can filter tickets by status, pending reason, priority, assignee, group, tag, channel, service-target state, and ticket custom fields; sort by target due time, last activity, or priority; and save a named view that authorised agents can open. My queue lists Open and Pending tickets assigned to the current agent, earliest target due time first."
        },
        {
          "id": "TKT-006",
          "title": "Search.",
          "text": "Full-text search over ticket IDs, subjects, message bodies, customer names and email addresses, tags, and configured custom fields returns matching authorised tickets within 500ms at the stated limits."
        },
        {
          "id": "TKT-007",
          "title": "Assignment.",
          "text": "New tickets are assigned by the assignment policy. Agents may assign or reassign a ticket to one agent, one group, or both without changing state or service clocks. *Policy: `ticket.assignment.v1`; default: unassigned.*"
        },
        {
          "id": "TKT-008",
          "title": "Priority.",
          "text": "Every ticket has Urgent, High, Normal, or Low priority. Authorised agents and allowed automation may change it; each change records actor, prior value, new value, and time."
        },
        {
          "id": "TKT-009",
          "title": "Timeline.",
          "text": "The ticket timeline presents customer messages, public replies, internal notes, system events, adapter evidence, automation runs, and execution records in durable event-time order, with insertion order as a stable tie-breaker across reload and pagination."
        },
        {
          "id": "TKT-010",
          "title": "Public and internal messages.",
          "text": "Public replies are eligible for customer delivery. Internal notes are a distinct stored kind that cannot be passed to a delivery adapter, included in customer-facing export, or used to claim an action completed."
        },
        {
          "id": "TKT-011",
          "title": "Solved guard.",
          "text": "Solving is an explicit agent action and sends no draft or external action. A Solved ticket rejects new public replies and external executions until it is reopened under TKT-003 or TKT-015; Closed and Merged tickets reject them permanently."
        },
        {
          "id": "TKT-012",
          "title": "Concurrent changes.",
          "text": "Each ticket mutation supplies its revision. A stale mutation is rejected with the current revision and makes no partial change; inbound message append never fails because of a concurrent agent edit."
        },
        {
          "id": "TKT-013",
          "title": "Ticket merge.",
          "text": "An agent may merge only tickets for the same customer, neither Closed nor Merged, after confirming source and destination IDs and a reason. The source becomes Merged and stores the destination ID. Cross-customer merge is refused; it needs a separately versioned, elevated extension with explicit participant and recipient reconciliation before customer-visible data could cross a customer boundary."
        },
        {
          "id": "TKT-014",
          "title": "Manual tickets.",
          "text": "An agent-created ticket records whether its initial message came from the requester or agent, and that attribution is immutable. Agent-originated tickets start no first-response clock and are excluded from first-response reporting."
        },
        {
          "id": "TKT-015",
          "title": "Operator reopen.",
          "text": "An agent may reopen Solved to Open with a required reason; it starts a new service cycle, sends nothing, and records the action. Closed and Merged tickets cannot be reopened."
        },
        {
          "id": "TKT-016",
          "title": "Merged history and replies.",
          "text": "A Merged ticket is read-only, its timeline is presented on the destination, replies addressed to stored threading identifiers reach the destination, and both IDs remain resolvable in search and audit history. Its source conversation stays linked to the immutable source and is transcluded on the destination with original conversation and author identity. Source and destination retain independent lifecycle and completion histories; routing or transclusion changes neither lifecycle."
        },
        {
          "id": "TKT-017",
          "title": "Tags.",
          "text": "Agents apply and remove case-insensitive canonical tags on tickets and customers; each change records actor and time. Tags filter queues, match search, serve as automation conditions and report breakdowns, and appear in export. Removing a tag retains the audit event."
        },
        {
          "id": "TKT-018",
          "title": "Reply outcome.",
          "text": "Sending a public reply leaves the ticket Pending with a configured reason that waits on the customer, unless the agent selects a different permitted status before sending. Sending never solves a ticket alone. *Policy: `ticket.reply-status.v1`; default: Pending, waiting on the customer.*"
        },
        {
          "id": "CASE-001",
          "title": "Numbered case and default conversation.",
          "text": "A Lily case is the visible numbered ticket. Its public number is unique for the deployment life, never reused after deletion or merge, and remains resolvable after import. Each new ticket creates one hidden conversation by default. Customer-visible messages live on that conversation; notes, evidence, assignment, actions, and lifecycle live on the case. Completion never closes or deletes the conversation."
        },
        {
          "id": "CASE-002",
          "title": "View predicates and columns.",
          "text": "A saved view has an All block and an optional Any block. It matches when every All condition matches and, when Any is non-empty, at least one Any condition matches; an empty All block is true and an empty Any block imposes no additional condition. Rows, counts, exports, dashboard drill-through, sorting, pagination, and selected columns use the same stored predicate."
        },
        {
          "id": "CASE-003",
          "title": "Forms, followers, context, and work tabs.",
          "text": "Administrators define ticket forms, required fields, and custom fields; creation preserves the selected form and submitted values. An authorised agent can add/remove followers, who receive `ticket.followers.v1` updates without needing to be owner or assignee. Detail exposes requester context, selected columns, filtered activity types, and configured tabs. *Policy: `ticket.followers.v1`; default: assignment, requester message, note, public reply, at-risk, breach, and solved events notify followers subject to preferences.*"
        },
        {
          "id": "CASE-004",
          "title": "Bulk and destructive work.",
          "text": "An authorised agent can apply reviewed bulk assignment, tag, status, macro, or spam actions to a filtered selection with independent per-ticket success, skip, or failure outcomes. Spam marking, requester suspension, deletion, and restoration require the separately authorised destructive action and preserve audit history under baseline retention."
        },
        {
          "id": "CASE-005",
          "title": "Agent and mobile work.",
          "text": "Home presents assigned/followed work, capacity, activity, and queue links; availability influences only later routing. The responsive mobile workspace supports ticket work and each agent can configure mobile notification categories and quiet hours with the same routing and suppression semantics as NOTIFY-001 and NOTIFY-002."
        },
        {
          "id": "CASE-006",
          "title": "Live conversation.",
          "text": "A configured widget may keep a live visitor/agent conversation. Each live conversation has a visible participant and queue state, is linked to its ticket when a request is submitted or an agent creates one, and retains its message chronology when it becomes the ticket's hidden conversation. An agent presence indicator never claims a live responder when none is assigned."
        },
        {
          "id": "CASE-007",
          "title": "Global and recent search.",
          "text": "Global search returns authorised tickets, customers, organisations, Help Centre articles, agents, groups, and saved views, identifies the matched type and field, and keeps a per-agent recent-record list that can be cleared. Ticket full-text latency remains TKT-006; global results preserve access checks and do not reveal a recent record after access changes."
        },
        {
          "id": "CASE-008",
          "title": "Work affordances.",
          "text": "Agents can open concurrent ticket tabs without losing unsaved drafts, return to a chosen tab, and see its current revision or conflict state. A queue can offer an explicit guided next-ticket action based on its visible ordering; its action, selection rationale, and any skipped ticket are recorded. The exact guided-play interaction is a Lily decision because the available Zendesk capture establishes the surface but not its execution."
        },
        {
          "id": "CASE-009",
          "title": "Tag entry.",
          "text": "Ticket tags display as removable chips, offer existing canonical tags while typing, and permit an authorised agent to create a new tag inline. Each entry/removal is audited. Autocomplete and inline creation are Lily product requirements; the cited Zendesk evidence establishes display and removal only."
        },
        {
          "id": "CASE-010",
          "title": "Rich composition, counters, and capacity.",
          "text": "The composer persists rich formatting, emoji, links, and accepted attachments before submit. Workspace chrome shows authorised agents live conversation and assigned-work counters. Administrators set an enforced per-agent active-ticket cap; routing skips capped agents and records the reason, while direct assignment above the cap requires an explicit authorised override."
        },
        {
          "id": "MAIL-001",
          "title": "Inbound email.",
          "text": "A signature-verified provider delivery to a configured support address creates or appends a ticket message within 60 seconds, preserving sender, recipients, subject, sanitised text and HTML, attachments, provider ID, and threading headers."
        },
        {
          "id": "MAIL-002",
          "title": "Threading.",
          "text": "An inbound email appends only when `In-Reply-To` or `References` matches a stored message ID, or a valid Lily-issued opaque reply token identifies its ticket, **and** its sender is the current requester or current participant. The timeline audits which identifier matched. A removed or unknown sender starts a new ticket even with a valid identifier; sender and subject alone never join mail."
        },
        {
          "id": "MAIL-003",
          "title": "Threading conflict.",
          "text": "An unmatched message starts a new ticket. Conflicting headers, or a valid token that disagrees with them, start a new ticket with a visible conflict event naming both candidate tickets; the message is never attached speculatively."
        },
        {
          "id": "MAIL-004",
          "title": "Inbound idempotency.",
          "text": "Replaying one provider event with the same provider message ID produces one stored message, one service-cycle effect, and at most one routing, acknowledgement, and automation effect. Provider-event ID and RFC `Message-ID` are durably one-to-one. If either known identity arrives paired with a new, conflicting identity or immutable thread metadata, Lily quarantines the conflict without routing, service-cycle, acknowledgement, or automation effect until reviewed. Body equality never resolves it."
        },
        {
          "id": "MAIL-005",
          "title": "Automatic mail.",
          "text": "Mail with automatic, bulk, delivery-status, receipt, or no-reply signals is non-actionable. A matching item is stored as a visible system event and does not reopen, notify, start a service cycle, run automation, or request an acknowledgement. An unmatched item creates no queue entry and is retained as a discarded-mail audit record."
        },
        {
          "id": "MAIL-006",
          "title": "Outbound email.",
          "text": "A public reply atomically creates one durable outgoing message and one dispatch job with a stable idempotency key, reply headers, and a Lily-issued opaque reply token, then dispatches that job within 60 seconds. It uses the ticket's stored reply mailbox: the first configured support-address match retained from RFC intake under MAIL-011, or, for a manual, web-form, widget, or other non-RFC ticket, exactly one enabled configured support mailbox selected and stored by an authorised agent before its first public reply. A repeated key returns the original message and dispatch job."
        },
        {
          "id": "MAIL-007",
          "title": "Delivery state.",
          "text": "Each outgoing message monotonically progresses through queued, accepted, delayed, delivered, bounced, rejected, suppressed, failed, or needs_review. Provider acceptance is never customer delivery. There are at most five total dispatch attempts in one hour with the original key. A failure before provider acceptance never stops a first-response clock. Lily preserves provider-acceptance time and delivery evidence. If the sole qualifying public reply later reaches final bounced, rejected, or failed state, it restores that service cycle's original first-response clock and target due calculation without resetting elapsed time; if another qualifying public reply remains provider-accepted, the clock stays stopped. The pre-send ticket status and its mutation revision are recorded; on that same sole-reply final failure, Lily restores the pre-send status only when no later independent status mutation occurred, never clobbering later agent changes. Final failure or needs_review notifies under NOTIFY-004. An ambiguous needs_review outcome is never automatically retried and requires provider reconciliation."
        },
        {
          "id": "MAIL-008",
          "title": "Authoritative delivery reports.",
          "text": "Verified provider reports move an accepted message only forward to delivered, delayed, bounced, or rejected and record report time. Duplicate or out-of-order reports never regress a terminal state; Lily never labels delivery without an authoritative report."
        },
        {
          "id": "MAIL-009",
          "title": "Attachments.",
          "text": "Inbound and outbound attachments remain bound to their immutable message. A file outside the seed limits is rejected with filename and reason while the readable message and accepted files are retained."
        },
        {
          "id": "MAIL-010",
          "title": "Provider events.",
          "text": "Intake and delivery webhooks are rejected without a valid `mail.receiver.v1` signature. Duplicate and out-of-order delivery events are tolerated and never create data or regress a terminal state."
        },
        {
          "id": "MAIL-011",
          "title": "Recipients and support-address intake.",
          "text": "One RFC message addressed to multiple configured support addresses creates one intake record, retains every matched support address, and records the first configured match as its reply mailbox. A new ticket records the sender as requester and only non-support `To` and `Cc` recipients as participants; Bcc is never disclosed. A public reply addresses the requester and current participants the agent keeps in the composer; removed recipients are not restored later. Support and notification addresses are never participants."
        },
        {
          "id": "MAIL-012",
          "title": "Intake durability and destination.",
          "text": "Lily acknowledges a verified intake only after a durable intake record exists; failure before it returns `503` with no partial ticket. Post-record parse, storage, or enqueue failure is visible with a retryable or terminal reason. A verified message with no configured support destination is quarantined with destination and reason."
        },
        {
          "id": "MAIL-013",
          "title": "Mailbox identity.",
          "text": "An agent-authored public reply uses the ticket's stored reply mailbox: the first configured RFC support-address match under MAIL-011, or the authorised enabled configured-mailbox selection stored under MAIL-006 for a manual, web-form, widget, or other non-RFC ticket. It carries ticket threading data and appends that agent's configured signature. Operator notifications use a no-reply address that is not a support destination and carry no agent signature. Automatic acknowledgement is not a notification and follows MAIL-014."
        },
        {
          "id": "MAIL-014",
          "title": "Automatic acknowledgement.",
          "text": "When enabled, the actionable message that creates a ticket produces exactly one stored automatic acknowledgement under MAIL-006 through MAIL-008. It uses the ticket's stored reply mailbox and threading data but no agent signature; it is not sent from the no-reply notification address. No acknowledgement is sent for manual, non-actionable, blocked, or later ticket messages, and it never counts as a first response. *Policy: `channel.autoresponder.v1`; default: disabled until an Administrator enables it for a mailbox.*"
        },
        {
          "id": "CHAN-001",
          "title": "Channel catalogue.",
          "text": "Administrators see every configured intake and delivery channel, its enabled state, form or mailbox binding, group routing, public address, and configuration health. Disabling a channel preserves its history and stops only later intake or dispatch through that channel."
        },
        {
          "id": "CHAN-002",
          "title": "Web widget and simulator.",
          "text": "A configured public widget collects a request with the configured form and creates a ticket under TKT-001. It identifies the current requester where consented, exposes the required privacy text, and confirms a successful request without pretending an agent is present. An Administrator can use a simulator to preview configured form, bot, appearance, proactive-message, and live-conversation states without creating a real ticket or sending a visitor message."
        },
        {
          "id": "CHAN-003",
          "title": "Grounded bot assistance.",
          "text": "A configured bot greets the visitor, offers up to ten article-grounded generated answers from approved Help Centre material, and shows cited article links. It stores every bot and visitor turn before agent handoff in the linked conversation, collects the ticket form, and hands off for no valid answer or visitor request. Core `ai.provider.v1` receives only approved article material and disclosed visitor input, never private notes; cited identifiers are validated against supplied articles and invalid output fails for review rather than falling back to invented copy."
        },
        {
          "id": "CHAN-004",
          "title": "Proactive messages.",
          "text": "An administrator can configure an eligible widget audience and a reviewed proactive message. The configuration states its trigger and frequency limit, respects consent and suppression, and records each presentation; it does not create a ticket unless the visitor submits a request."
        },
        {
          "id": "CHAN-005",
          "title": "Voice queue and ticket link.",
          "text": "A configured voice adapter exposes queues or lines with waiting-call count, agent or group routing context, call state, and stable Ticket ID when one exists. An authorised agent can attach a call record to an existing ticket; the attachment is audited and adds no invented call-to-ticket relation when no ticket was selected. This is a Lily lifecycle decision; the Zendesk evidence only establishes a Live Calls Ticket ID column."
        },
        {
          "id": "CHAN-006",
          "title": "Side conversations.",
          "text": "An authorised agent can open a labelled side conversation with an approved external participant, see its delivery state and replies on the ticket, and keep it separate from the customer-visible conversation. It cannot disclose private notes without an explicit reviewed message."
        },
        {
          "id": "CUST-001",
          "title": "Identity.",
          "text": "The first qualifying customer message from an unseen normalised email address creates one customer; comparison is case-insensitive and a shared display name never merges identities."
        },
        {
          "id": "CUST-002",
          "title": "Profile.",
          "text": "A profile has display name, primary and observed email addresses, locale, tags, declared custom fields, and private account notes. Locally owned fields are editable and adapter-sourced fields are read-only with source and refresh time."
        },
        {
          "id": "CUST-003",
          "title": "History.",
          "text": "A profile shows every Open, Pending, Solved, Closed, and Merged ticket for that customer with its satisfaction and available adapter records."
        },
        {
          "id": "CUST-004",
          "title": "Notes.",
          "text": "Agents may add private, attributed customer notes. Notes are never sent to the customer or written back through an adapter."
        },
        {
          "id": "CUST-005",
          "title": "Merge.",
          "text": "An agent merges customers only after confirming survivor and source IDs and seeing addresses, ticket counts, and record links. Tickets, addresses, notes, tags, fields, and audit history move to the survivor; incompatible provider customer IDs block the operation and name the conflict."
        },
        {
          "id": "CUST-006",
          "title": "Erasure boundary.",
          "text": "Erasing a customer removes locally owned messages, attachments, notes, evidence snapshots, and tickets under `BASE-DATA-003`; it never claims to erase a provider account or provider data."
        },
        {
          "id": "CUST-007",
          "title": "Resync suppression.",
          "text": "This predecessor guarantee applies only to an enabled `external.sync.v1` extension described in `research/optional-commerce-adapter.md`: erasure stores a one-way keyed digest of normalised provider namespace and resource ID, no customer field, to prevent automatic resync recreation; it travels with export/import and only explicit restore removes it. Core Lily has no automatic external sync or sync credential."
        },
        {
          "id": "CUSTOMER-001",
          "title": "Organisations and suspension.",
          "text": "Administrators create organisations and membership. An authorised agent may suspend a requester; *Policy: `requester.suspension.v1`; default: new intake is quarantined for review while earlier history stays readable.*"
        },
        {
          "id": "CUSTOMER-002",
          "title": "Imports and record links.",
          "text": "An Administrator can run a reviewed CSV customer import with mapping, validation, duplicate handling, and per-row outcomes. An optional record-link adapter attaches named external records with adapter, identifier, link state, and retrieval time; unavailable data is labelled unavailable and never fabricated."
        },
        {
          "id": "HELP-001",
          "title": "Public Help Centre and audiences.",
          "text": "An Administrator activates or deactivates the public Help Centre; deactivation makes public article and request paths unavailable while preserving authoring history. Published content is visible only to its named audience. An audience selects signed-in users, staff, groups, tags, and users with All/Any blocks: all All conditions and, when non-empty, one Any condition must match; empty All is true and empty Any adds no restriction. Administrators preview matching users and anonymous/staff roles. A preview is not evidence of signed-out deployment reachability."
        },
        {
          "id": "HELP-002",
          "title": "Articles, taxonomy, history, and bulk work.",
          "text": "Agents with article authority can create, edit, publish, unpublish, archive, and restore articles in categories and sections. Articles have a title, body, author, timestamps, lifecycle state, audience visibility, content tags, and version history. Article history is filterable by actor, event, and date. A selected article set supports reviewed bulk publish, unpublish, archive, restore, category, and audience changes with per-item success, skip, or failure results."
        },
        {
          "id": "HELP-003",
          "title": "Findability and media.",
          "text": "The Help Centre offers text search, category navigation, pagination, article metadata, a shared media library with upload, search, insert, and removal permissions, and article comments when comments are enabled. Search results respect audience visibility and label unavailable translations or attachments rather than substituting a different article."
        },
        {
          "id": "HELP-004",
          "title": "Languages and appearance.",
          "text": "Administrators configure Help Centre languages, translated article variants, branding, theme appearance, and a reviewed public preview. A language fallback is explicit and never represents untranslated copy as a translation."
        },
        {
          "id": "HELP-005",
          "title": "Help Centre requests.",
          "text": "A public request form uses an administrator-selected ticket form, validates required fields, creates a ticket under TKT-001, and gives the requester a result that does not expose another ticket. Its submission is distinguishable from article comments and widget intake."
        },
        {
          "id": "ADMIN-001",
          "title": "Operational settings.",
          "text": "Administrators configure business hours, IANA timezone, holiday calendar, ticket types and priorities, statuses, forms, custom fields, canned replies, mailbox identities, routing, authentication, retention, and API/webhook credentials. Retention configuration can label, report, or schedule review of records but cannot automatically delete user data; deletion follows the applicable baseline clause and explicit authorised action. The settings screen distinguishes active configuration from unavailable optional adapters."
        },
        {
          "id": "ADMIN-002",
          "title": "Business rules, capacity, and usage.",
          "text": "Administrators can inspect routing rules, capacity limits, API/webhook integrations, installed approved applications, their last execution or error, and current storage/API/channel usage meters with source, period, and refresh time. No setting silently expands authorisation or gives an external application access beyond its configured scope."
        },
        {
          "id": "MACRO-001",
          "title": "Macro library.",
          "text": "Authorised agents create, edit, archive, and search shared macros for a public reply or internal note. A macro may propose tags, priority, assignment, form fields, or status, but does not itself send a message. The library reports applied count, recent use, and sortable rolling seven-day usage without exposing ticket content to an unauthorised viewer."
        },
        {
          "id": "MACRO-002",
          "title": "Placeholders.",
          "text": "Applying a macro resolves its declared placeholders from the current ticket, requester, organisation, and agent. An unresolved required placeholder blocks use and identifies the missing value."
        },
        {
          "id": "MACRO-003",
          "title": "Review boundary.",
          "text": "A macro fills an editable draft and previews every field change. Submitting remains an explicit TKT-018 and MAIL-006 action and cannot bypass revision, authorisation, or outbound delivery rules."
        },
        {
          "id": "SLA-001",
          "title": "Calendar and selection.",
          "text": "Administrators configure first-response and resolution targets against one workspace timezone, working-hours schedule, and holiday calendar. *Policy: `service-target.selection.v1`; default: targets are selected by ticket priority and measured in business time.* A ticket with no matching target displays that fact and is excluded from met/breached denominators."
        },
        {
          "id": "SLA-002",
          "title": "Snapshot and calculation.",
          "text": "Starting a service cycle stores the target version, duration, due time, and business-calendar inputs. Later setting edits do not rewrite its result. Recalculation records its reason and prior due time."
        },
        {
          "id": "SLA-003",
          "title": "First response.",
          "text": "The first-response clock stops only when the first public agent reply is provider-accepted, subject to MAIL-007's sole-qualifying-reply final-failure restoration. Assignment, notes, macros, automation, an acknowledgement, and queued or failed delivery do not stop it."
        },
        {
          "id": "SLA-004",
          "title": "Resolution cycle.",
          "text": "The resolution clock runs for Open, and for Pending unless the selected pending reason explicitly waits on the requester. It stops when the case is solved. A requester message or agent reopen starts the next cycle while retaining prior cycles."
        },
        {
          "id": "SLA-005",
          "title": "Visibility.",
          "text": "Ticket rows, ticket detail, Home, and saved views show the exact due time, calendar basis, remaining or overdue time, and state `active`, `paused`, `at_risk`, or `breached`. At risk begins at 25% of target duration remaining in business time."
        },
        {
          "id": "SLA-006",
          "title": "Breach effects.",
          "text": "Crossing a target marks the cycle breached, emits a notification event, and never resolves, closes, reassigns, or otherwise mutates the ticket on its own."
        },
        {
          "id": "QUEUE-001",
          "title": "Filter-exact dashboard.",
          "text": "Home shows the current selected view's open backlog, urgent count, target-state counts, resolved-today count, mean first-response time over the last 24 hours, and satisfaction score over the last 7 days. For every ticket state it shows the count and longest current wait. Every count opens the equivalent saved-view conditions and shows the data timestamp; it is no more than 30 seconds old at the stated limits."
        },
        {
          "id": "QUEUE-002",
          "title": "Target tiers.",
          "text": "An active ticket with a target is exactly one of `breached` once past due, `at_risk` with 25% or less of its target duration remaining, `due_soon` with more than 25% remaining and two business hours or less to due, or `later` otherwise. A paused cycle is `paused`; a ticket with no matching target is `no_target`. The four active-target tiers are mutually exclusive and exhaustive."
        },
        {
          "id": "QUEUE-003",
          "title": "Operational ordering.",
          "text": "The default work ordering is breached, at_risk, due_soon, later, paused, no_target, then nearest active due time, priority, latest requester activity, and ticket number. An agent may choose documented alternatives without changing the selected view conditions."
        },
        {
          "id": "QUEUE-004",
          "title": "Due-state detail.",
          "text": "A queue and ticket disclose which target is due, absolute due time with timezone, and the previous/current due time and reason for a recalculation."
        },
        {
          "id": "QUEUE-005",
          "title": "Measured times.",
          "text": "First-response and resolution measures are stored as business and calendar elapsed time and shown with their cycle. Reopening never overwrites a recorded first response."
        },
        {
          "id": "AUTO-001",
          "title": "Rule lifecycle.",
          "text": "A rule is Draft, Active, or Paused and has a name, ordered conditions and actions, creator, latest editor, version, and run count. Draft and Paused rules process no new events."
        },
        {
          "id": "AUTO-002",
          "title": "Allowed actions.",
          "text": "A rule can react to ticket creation, requester message, ticket field or status change, or one scheduled elapsed-time condition. It may apply a tag, set priority, assign an eligible group or agent, set a pending reason, send an internal notification, or apply a macro draft. It cannot send a public reply, complete a ticket, change a requester record, or invoke an external mutation. *Policy: `automation.action-allowlist.v1`; default: the listed actions only.*"
        },
        {
          "id": "AUTO-003",
          "title": "Order and audit.",
          "text": "Active rules evaluate in visible order against one immutable triggering revision. Each run records matching conditions and each applied, skipped, or failed action."
        },
        {
          "id": "AUTO-004",
          "title": "Idempotency and loops.",
          "text": "A rule run is unique by rule version, ticket, triggering event or scheduled window, and action. An evaluation chain evaluates each active rule version at most once; duplicate events and scheduler runs repeat no effects."
        },
        {
          "id": "AUTO-005",
          "title": "Concurrent changes and failure.",
          "text": "A rule rechecks its conditions and ticket revision before mutation. A conflict is a visible skipped run. A failed action is visible with rule, ticket, attempted action, and error; a retry reuses its idempotency key."
        },
        {
          "id": "AUTO-006",
          "title": "Rule authorisation.",
          "text": "Only an Administrator may activate, pause, or change a rule's owner-visible scope. A rule cannot act on a ticket, group, form, or field its owner is not authorised to configure."
        },
        {
          "id": "AUTO-007",
          "title": "Reviewed recovery.",
          "text": "A retried failed rule run uses the original triggering revision and idempotency keys, reports any now-stale condition, and never replays an action that already succeeded."
        },
        {
          "id": "NOTIFY-001",
          "title": "Accountable ticket updates.",
          "text": "New unassigned tickets notify every eligible Agent; direct assignment notifies the new assignee or every eligible member of the assigned group; self-assignment sends nothing. Requester follow-up, at-risk, breach, and solved notifications go to followers, or to the assignee then eligible Administrators if there are no followers. An at-risk and a breach notification occur at most once each per service cycle."
        },
        {
          "id": "NOTIFY-002",
          "title": "Coalescing and quiet hours.",
          "text": "Repeated requester-follow-up events coalesce to one notification until an assigned agent opens the ticket; repeated event categories use their named service-cycle or open-cycle boundary. *Policy: `ticket.quiet-hours.v1`; default: quiet hours defer non-urgent notifications while urgent and breached work bypasses them.* Intake, service clocks, and other recipients are unaffected."
        },
        {
          "id": "NOTIFY-003",
          "title": "No mail loops.",
          "text": "Notifications use a configured no-reply sender that is not a support destination. Mail to that address creates or reopens no ticket and triggers no automation. Automatic acknowledgements are ticket mail under MAIL-014, not notifications."
        },
        {
          "id": "NOTIFY-004",
          "title": "Failure reaches a person.",
          "text": "A public-message delivery failure, automation failure, notification failure, or configuration/security failure is visible on the ticket or settings record and is delivered to the initiating agent, or every active Administrator when there is no initiating agent. This responsible-failure notification cannot be muted and never blocks or rolls back its causing event."
        },
        {
          "id": "CSAT-001",
          "title": "Survey request.",
          "text": "A solved ticket with a customer-visible accepted reply can queue one signed satisfaction request for that solve cycle. *Policy: `csat.request.v1`; default: one email 24 hours after the solve.* No survey is sent to a blocked requester, and reopening cancels an unsent request."
        },
        {
          "id": "CSAT-002",
          "title": "Survey response.",
          "text": "The single-use public link accepts one whole-number rating from one to five with an optional comment, expires after 30 days, exposes no unrelated ticket data, and never creates or reopens a ticket."
        },
        {
          "id": "CSAT-003",
          "title": "Survey history.",
          "text": "A result shows on the ticket and customer record with its solve cycle and time. A later cycle makes a new request and never overwrites a prior result."
        },
        {
          "id": "INSIGHT-001",
          "title": "Operational dashboards.",
          "text": "Analytics provides prebuilt dashboards for ticket volume and status, first-response and resolution duration, service-target outcomes, satisfaction score and response rate, assignment and group workload, and channel mix."
        },
        {
          "id": "INSIGHT-002",
          "title": "Dataset and filters.",
          "text": "Every report states its period, timezone, selected view or filters, ticket states, denominator, freshness time, and comparison period. It can filter by assignee, group, type, priority, tag, form, channel, custom field, and target state."
        },
        {
          "id": "INSIGHT-003",
          "title": "Report builder.",
          "text": "An authorised user can create, edit, save, share, and retire a report using documented ticket, customer, satisfaction, and service-target fields. A report exposes the tickets behind an aggregate only to viewers authorised for those tickets."
        },
        {
          "id": "INSIGHT-004",
          "title": "Formatting and export.",
          "text": "Dashboard and report users can choose supported chart and table formatting. An export contains the complete durable result for current filters and period with metric definitions, rather than only the rendered page."
        },
        {
          "id": "INSIGHT-005",
          "title": "Freshness and correctness.",
          "text": "Ticket, service-target, and satisfaction analytics reflect committed mutations within five minutes at the stated limits. A panel shows its last successful refresh and a stale or failed state; no cached result is represented as current."
        },
        {
          "id": "INSIGHT-006",
          "title": "Operational export.",
          "text": "An agent can export the complete durable current result of an authorised view, queue, dashboard, or report, including selected filters, period, and metric definitions. This convenience export does not replace DATA-001."
        },
        {
          "id": "INSIGHT-007",
          "title": "Merge accounting.",
          "text": "A merged source remains auditable and searchable but is excluded from live backlog, created, solved, and service-target totals. Its source conversation entries remain visible through canonical transclusion without double-counting the canonical case."
        },
        {
          "id": "INSIGHT-008",
          "title": "Explainable drivers.",
          "text": "Each classified volume, satisfaction, or service-target aggregate exposes its contributing tag or configured classification, stated denominator, and authorised ticket drill-down. Unclassified eligible tickets appear as `Other`; the displayed drivers, Other, and excluded denominator reconcile to the reported total."
        },
        {
          "id": "DATA-001",
          "title": "Portable export.",
          "text": "Lily's complete export includes tickets, cases, conversations, messages, attachments, customers, organisations, forms, tags, views, macros, service cycles, automations and runs, satisfaction records, settings, article versions, record links, notifications, and audit entries, with original attachments. It contains no secret and follows baseline data guarantees."
        },
        {
          "id": "DATA-002",
          "title": "Data boundaries.",
          "text": "Export, retention, deletion, and erasure identify locally owned data and optional-adapter references. Lily does not claim to update, delete, export, or erase a provider record unless the configured adapter returned a confirmed result for that operation."
        },
        {
          "id": "DATA-003",
          "title": "Zendesk importer.",
          "text": "An Administrator can use the documented importer with an administrator-provided Zendesk export into an empty Lily installation. It validates mappings before mutation and offers dry run. It imports numbered tickets, requesters, messages, original attachments, tags, forms, groups, users, status history, merge history, and available metadata, with a per-record outcome and one import audit record. Each source object has a preserved source ID and a Lily ID; source ticket numbers are retained only when unused, otherwise Lily allocates the next permanent number and records the source-number mapping. Existing source IDs make reruns idempotent. Imported merge and status history are retained as source history; unsupported data receives a per-record outcome without fabrication. Stage 2 must prove this with a real export; no Freshdesk importer is committed."
        }
      ],
      "hasReadme": true,
      "hasBaseline": true,
      "nonGoals": [
        "Multiple businesses or tenants",
        "A separately browsable conversation inventory",
        "An owned CRM, commerce, order, payment, fulfilment, or marketing system",
        "Zendesk community, badges, moderation, theme marketplace, dashboard sharing or restrictions, CRM suite/tasks/prospecting, native mobile SDK, or social connectors",
        "Application marketplace, classic report builder, vendor benchmark survey, product switcher, signup/trial/billing chrome, or workforce management",
        "Outbound campaigns, arbitrary external webhooks, native mobile applications, or voice recording",
        "Autonomous customer messages, ticket lifecycle changes, external mutations, or fabricated adapter data",
        "Freshdesk replacement or import until compliant mapped public-source evidence exists"
      ],
      "externals": [
        {
          "name": "native-auth-mail",
          "required": true,
          "requiredWhen": "",
          "reason": "Cloudflare primitives do not originate transactional email; Lily uses the explicitly selected Resend or SendGrid transport to deliver native magic-link sign-in messages.",
          "data": [
            "allow-listed operator email address",
            "signed magic-link URL",
            "delivery metadata required to send the sign-in message"
          ],
          "adapters": []
        },
        {
          "name": "mail-provider",
          "required": false,
          "requiredWhen": "A deployment enables inbound or outbound email.",
          "reason": "A configured provider supplies inbound delivery callbacks, outbound email transport, and authoritative delivery reports.",
          "data": [
            "customer email addresses",
            "message content",
            "email attachments",
            "threading metadata",
            "delivery event metadata"
          ],
          "adapters": [
            "mail.receiver.v1",
            "mail.sender.v1"
          ]
        },
        {
          "name": "record-link-provider",
          "required": false,
          "requiredWhen": "A deployment enables optional external record links.",
          "reason": "A buyer-controlled adapter may expose named external records without making Lily an owned CRM or commerce system.",
          "data": [
            "buyer-selected external record identifiers",
            "adapter retrieval timestamps",
            "approved display fields"
          ],
          "adapters": [
            "record.link.v1"
          ]
        },
        {
          "name": "commerce-and-ai-provider",
          "required": false,
          "requiredWhen": "A buyer elects to implement the separately specified optional commerce adapter.",
          "reason": "The retained optional specification requires replaceable commerce and AI providers while keeping manual core ticket work independent.",
          "data": [
            "buyer-approved ticket evidence",
            "buyer-approved external resource identifiers",
            "configured resolution instructions"
          ],
          "adapters": [
            "commerce.platform.v1",
            "ai.provider.v1"
          ]
        },
        {
          "name": "help-centre-ai-provider",
          "required": false,
          "requiredWhen": "A deployment enables the generative Help Centre bot.",
          "reason": "Article-grounded generated visitor answers require a buyer-selected model provider.",
          "data": [
            "approved Help Centre article material",
            "disclosed visitor input",
            "validated article citation identifiers"
          ],
          "adapters": [
            "ai.provider.v1"
          ]
        },
        {
          "name": "voice-provider",
          "required": false,
          "requiredWhen": "A deployment enables the optional voice queue and call-ticket channel.",
          "reason": "A buyer-selected telephony service supplies phone lines, queue state, and call records that Cloudflare primitives do not originate.",
          "data": [
            "caller and destination numbers",
            "call state and timing",
            "buyer-selected queue or line identifiers",
            "selected ticket identifier"
          ],
          "adapters": [
            "voice.provider.v1"
          ]
        },
        {
          "name": "external-sync-provider",
          "required": false,
          "requiredWhen": "A buyer installs the optional external.sync.v1 extension.",
          "reason": "A buyer-controlled provider transport is required only when that extension synchronises records outside Lily.",
          "data": [
            "normalised provider namespace",
            "external resource identifier",
            "keyed erasure-suppression digest"
          ],
          "adapters": [
            "external.sync.v1"
          ]
        },
        {
          "name": "sendgrid",
          "required": false,
          "requiredWhen": "MAIL_PROVIDER or TICKET_MAIL_PROVIDER is sendgrid.",
          "reason": "Optional explicitly selected provider for native sign-in, signed inbound mail, human-reviewed public replies, and signed delivery events. Provider acceptance is not delivery confirmation.",
          "data": [
            "Reviewed message body",
            "Explicit recipients",
            "Attachments",
            "Provider acceptance reference"
          ],
          "adapters": [
            "mail.sender.v1",
            "mail.receiver.v1"
          ]
        }
      ],
      "operatingCost": {
        "status": "free-plan default; production usage not measured",
        "estimate": "The locally deployable core uses Cloudflare Workers, D1, R2, Queues, and Cron. Mail, record-link, commerce, and AI provider costs exist only when a buyer enables those adapters and are paid directly by that buyer. Published cost and capacity claims await Stage 2 measurement.",
        "included": [
          "Cloudflare Worker",
          "D1",
          "R2",
          "Queues",
          "Cron"
        ],
        "excluded": [
          "mail provider",
          "record-link provider",
          "voice provider",
          "optional commerce provider",
          "optional AI provider"
        ]
      },
      "extensionPoints": [
        "app.route.v1",
        "app.navigation.v1",
        "settings.section.v1",
        "job.consumer.v1",
        "cron.task.v1",
        "ticket.assignment.v1",
        "ticket.auto-close.v1",
        "ticket.reply-status.v1",
        "ticket.followers.v1",
        "requester.suspension.v1",
        "channel.autoresponder.v1",
        "service-target.selection.v1",
        "automation.action-allowlist.v1",
        "ticket.quiet-hours.v1",
        "csat.request.v1",
        "ticket.created.v1",
        "ticket.message.received.v1",
        "ticket.message.sent.v1",
        "ticket.assigned.v1",
        "ticket.status.changed.v1",
        "ticket.merged.v1",
        "customer.updated.v1",
        "automation.run.completed.v1",
        "service-target.changed.v1",
        "satisfaction.received.v1",
        "app.navigation.after.v1",
        "home.dashboard.after.v1",
        "ticket.queue.row.actions.v1",
        "ticket.header.actions.after.v1",
        "ticket.timeline.after.v1",
        "ticket.composer.toolbar.after.v1",
        "ticket.requester-context.after.v1",
        "customer.profile.after.v1",
        "help.article.after.v1",
        "settings.after.v1",
        "mail.receiver.v1",
        "mail.sender.v1",
        "record.link.v1",
        "voice.provider.v1",
        "external.sync.v1",
        "commerce.platform.v1",
        "ai.provider.v1"
      ],
      "limits": {
        "status": "estimated",
        "basis": "Capacity and rate values are not load tested and must be replaced by measured limits before the first release. Public request values are per-IP one-minute enforcement thresholds, not capacity proof. Attachment content types are a product decision, not a measurement.",
        "concurrentOperators": 25,
        "tickets": 50000,
        "messages": 250000,
        "inboundMessagesPerMinute": 20,
        "outboundMessagesPerMinute": 20,
        "activeAutomationRules": 20,
        "attachmentBytesPerFile": 10485760,
        "attachmentBytesTotal": 10737418240,
        "attachmentContentTypes": [
          "image/png",
          "image/jpeg",
          "image/gif",
          "image/webp",
          "application/pdf",
          "text/plain",
          "text/csv",
          "message/rfc822",
          "application/zip",
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
          "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        ],
        "publicRequestsPerMinutePerIp": {
          "assets": 240,
          "health": 60,
          "landing": 60,
          "signIn": 10,
          "survey": 30,
          "providerWebhooks": 120,
          "integrations": 120,
          "helpCentre": 60,
          "helpRequest": 20,
          "widget": 30
        }
      },
      "accessibility": {
        "level": "WCAG 2.2 AA",
        "status": "target; automated and manual checks pending the Stage 2 backend port"
      },
      "landing": "assets/seeds/lily/01-landing.jpg",
      "shots": [
        "assets/seeds/lily/01-landing.jpg",
        "assets/seeds/lily/03-home.jpg",
        "assets/seeds/lily/04-tickets.jpg",
        "assets/seeds/lily/05-ticket-workspace.jpg",
        "assets/seeds/lily/06-customers.jpg",
        "assets/seeds/lily/07-customer.jpg"
      ],
      "install": null,
      "licence": "MIT",
      "availability": "in-development",
      "releaseProvenance": {
        "kind": "published-site-snapshot",
        "repository": "https://github.com/runeditrun/site",
        "revision": "cf01013ebb6bbfaf5189a5107794f60702cce3f2",
        "note": "Frozen previously published catalogue artifacts; not an application source or runtime acceptance receipt.",
        "project": "runeditrun/lily",
        "release": "0.1.0",
        "manifestSha256": "0506ee45f95c060c698f5358580ad922fe7cce3b06cd9d67f69036b48c8ad0fc"
      }
    }
  ]
}