@happyvertical/smrt-ingestion 0.57.37 → 0.57.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,15 +3,15 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-ingestion",
6
- "packageVersion": "0.57.37",
6
+ "packageVersion": "0.57.38",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "fb94c868962cca4df40793c6a89f65853d52486ea8792ede0f7cb2e6dd86f083",
11
- "packageJson": "2aa3c620e9d1b6781a887adc3cbd59780bfcaa5505e88f101aec8471a5335714",
10
+ "manifest": "847b952d967c39cb744555e8e8e4f91c9406e5a98f15b4d4f2918bf03949b681",
11
+ "packageJson": "bfcf7c935c33d167604376a50d8040b2d9b1c936dbe703d437fa27b4cf0470c4",
12
12
  "agents": "dea14442054ec1de798cba9be1fbdd955f44b0e4672639cbf3db8101d290c1fb",
13
13
  "moduleDoc:agents/execution.md": "3b223cb3c85312a29bb6157f22b153b4d643dca790b149f47c35396f981f2dc7",
14
- "moduleDoc:README.md": "49384c00608705488c26fb8c515c70cd7ba17a62d0b4c41ace2a821206011edd",
14
+ "moduleDoc:README.md": "d4dfdb4a4c1af3e1def6cf038e315e5bf66ab8d5942dee12a014527737a69748",
15
15
  "moduleDoc:EXTRACTION.md": "7aebb8ab51149e406938502935a46fe96174aa83975bbe5b913940f9b11053e9",
16
16
  "moduleDoc:PROPOSALS.md": "ac7ccecae2df7fb2e46c901a8c7b65bd0197f7583171fbb5e6a67824bb349839",
17
17
  "moduleDoc:FEEDBACK.md": "bf244222a1bc24843bdb7e94320dc3c0dfb7cb892001639d274786f4b667dacb",
@@ -1710,7 +1710,7 @@
1710
1710
  {
1711
1711
  "path": "README.md",
1712
1712
  "module": "README",
1713
- "content": "# @happyvertical/smrt-ingestion\n\nDurable receipt, evidence, analysis attempts and action-journal foundations for\n[ADR 0004](../../docs/adr/0004-application-aware-ingestion.md).\n\nThe root, `/models` and `/dto` entry points are browser-safe. Import\n`IngestionService` from `/server` in trusted host code. Generated REST, CLI and\nMCP access to the persistence models is disabled; hosts expose authorized DTO\nmethods through their authenticated application transport.\n\nBefore starting a service, deploy the package manifest through the public s-m-r-t\nmigration APIs or `smrt db:migrate`. `assertReady()` only checks existing tables.\nSQLite and PostgreSQL are supported; other engines fail construction. Index\nadditions ship the same way: `intake_items_received_idx` (newest-first inbox\npaging on `tenant_id, confidential_scope_id, created_at, id`) is declared on the model and reaches existing deployments through the\nnext manifest migration; there are no hand-written migration files in this\npackage, and `smrt doctor --db` / `db:status --parity` report the index until\nit is applied.\n\n`listItemsPage({ limit?, cursor?, state?, itemIds?, scanLimit? })` returns\n`{ items: [{ item, state, cursor }], scanned, nextCursor? }`: one authorized\ninbox page newest receipt first with each row's durable inbox state and\ncontinuation cursor, and the number of rows the call examined. `listItems()`\nremains as a deprecated array-returning method that walks the private page\nscan to the end. Without the optional `cursorKey` service option every cursor\nnames the last row the actor can read, and a page that finds no readable row\nwithin its scan limit fails with `IntakeScanLimitError`; with a server secret\nof at least 32 bytes, cursors are sealed and such a page resumes past what it\nskipped. `scanned` is for host\naccounting only. See [REVIEW.md](REVIEW.md) for the paging and authorization\ncontract.\n\nA host supplies a database, AssetRuntime, authenticated tenant/actor/confidential\nscope, a live authorization callback, a registered jobs target, and an idempotent\n`purgeDerived` callback for host-owned previews, retrieval indexes and caches.\nThe authorization callback receives the captured ceiling and the current\ntransaction executor; it must intersect current grants with that ceiling. Never\nconstruct the scope, retention policy or budgets from incoming document labels.\n\n`receive` accepts bounded original byte buffers, with stable source part IDs,\nparent relationships, optional upstream identity/version and an optional\noriginal `filename` (kept for labels on `IntakeEvidenceDTO.filename`, never\npart of receipt identity). The multipart upload source, email attachments and\nthe watched folder all supply it, as may a host `snapshotReference` part or a\nvendor webhook's verified parts. Every source delivers through one place that\nremoves control, format (bidi, zero-width, tag), line-separator and\ndefault-ignorable characters (Unicode properties, see\n`src/presented-filename.ts`) and drops a name that is then empty or over 512\ncharacters, so an odd name falls back to the default label instead of failing\nthe receipt (`receive` itself rejects such a name from a direct caller as\n`invalid`, and names are cleaned again when read back). Removing every format\nand default-ignorable character has display-only effects, since nothing uses\nthe name as identity: emoji ZWJ sequences and presentation selectors flatten,\nZWNJ/ZWJ are removed from Persian and Indic text (letters may join\ndifferently), and ideographic variation selectors are removed (a CJK variant\nshows its base glyph). Unicode normalization\n(NFC/NFKC) and confusable look-alike characters are out of scope: the name is\nstill untrusted text, so render it as text, never as markup or a path. The\nupstream `sourceReference` is stored but not exposed on the DTO: its ids are\nupstream locators, not display text, and they are never cleaned. For email\nthey are the provider's message locator (plus the attachment part id); for the\nwatched folder the id is the raw folder entry name, whose cleaned form is what\n`filename` exposes. Source adapters own\nstreaming and upstream snapshot retrieval. Receipt identity is tenant + source +\ndelivery key; changing bytes or authenticated scope conflicts. Content equality\nin a different conversation does not deduplicate an arrival. The returned item\nID and version are safe acknowledgment data, never a storage URL.\n\nStorage intents contain a stable asset ID and a public AssetStore-planned URI\nbefore byte I/O. Preservation verifies existing bytes instead of replacing them.\nA receipt becomes ready only after every part is durable and its dispatch intent\ncommits. A source retries the same receive call after failure or lost\nacknowledgment. Hosts schedule `repairDispatches` after restart and periodically;\nit repairs missing/failed queue submissions through SmrtJobCollection. The host\njob restores the captured scope and calls the analysis lifecycle methods. Run\nTaskRunner with the `ingestion` queue enabled.\n\n`analyze` appends immutable input/configuration revisions. `claimAnalysis` uses\nleases, monotonic fences and attempt budgets; `completeAnalysis` rejects expired\nor superseded workers. Terminal attempt output/provenance is preserved. Queue\nsubmission budgets also stop repeated handler failures in visible\n`needs_attention`. Reprocessing does not mint actions. `createAction`,\n`publishProposal` and `appendPlan` persist explicit intentions and exact attempt\nbindings for the later review/execution layer. They do not approve or apply\nbusiness operations. Decision/execution/feedback models are persistence\nfoundations; policy, handler expansion, review and business apply belong to the\nlater epic children.\n\n`expire` revokes access and pending work before removing bytes and calling\n`purgeDerived`. Interrupted deletion is retried by `repairDeletions` and\n`sweepRetention`. Minimal receipt/action identities remain, including successful\naction result digests; all content-bearing journal payloads are redacted. Private\ncleanup locators remain in deletion tombstones so repeated sweeps can remove a\nlate stale writer's orphan bytes even after the asset owner row is gone. Source\nadapters must enforce their configured delivery-age cutoff; expired replay IDs\nnever become fresh work. Continue running the cleanup sweep for the retained\nstorage namespace. Upstream source mutation/deletion cannot change copied\noriginals; explicit privacy deletion revokes every ingestion-derived payload and\nrequires host cache/index purge.\n\nSee [EXTRACTION.md](EXTRACTION.md) for bounded extraction, lease-bound input\nsnapshots, provider isolation requirements and the extraction corpus.\n\nValidation commands are `pnpm test`, `pnpm test:postgres`, `pnpm test:providers`, `pnpm typecheck` and\n`pnpm build` (including built browser import inspection). The PostgreSQL lane\nrequires the repository's configured database service and never silently skips.\nThe provider lane exercises local SDK adapters and process isolation; it does not\nclaim live remote recognition quality. `pnpm test:components` and `pnpm test:e2e`\ncover the maintained review surface. `pnpm test:evaluation` checks repository\nevaluation tooling contracts; measured held-out quality requires a separately\nfrozen protocol and an approved aggregate provider budget. See the [behavior matrix](../../docs/test-matrix/3670-ingestion-foundation.md).\n\n## Authoritative review and execution\n\nConfigure `IngestionOptions.execution` for policy, immutable previews, authenticated\ndecisions and idempotent domain execution. See [execution integration](agents/execution.md)\nfor the public handler catalog, required host authorization/target locks, deployment\nmigrations and jobs continuation integration. The maintained application reference\nin `reference/handlers.ts` creates draft ContentDocuments and attaches retained\nevidence through public domain APIs; it is not a published runtime adapter.\n\n## Configured source adapters\n\nThe server entry exports `createSourceDeliveryHandler`, `EmailSourceAdapter`,\n`WatchFolderSourceAdapter`, and `createVendorWebhookHandler`. Construct a\n`SourceBinding` from authenticated host configuration: enabled flag, scoped\n`IngestionService`, stable source ID/version, captured authority ceiling,\nretention policy, allowed media types and receipt limits. Re-resolve this binding\nwhen grants/configuration change. Source payloads never supply authority.\n\nThe reference POST handler authenticates before reading bytes and requires\n`Idempotency-Key`. JSON accepts exactly `capturedAt` and one of `text`,\n`structured`, or `reference: { owner: 'asset'|'message', id, version }`.\nThe host's `snapshotReference` must authorize current ownership and return copied\nbytes; an absent snapshot fails rather than acknowledging a mutable locator.\nMultipart accepts one `file` plus `captureId` (equal to the idempotency key),\n`capturedAt`, `captureSource` (`camera`, `native_picker`, `audio`, `document`), and optional\nlowercase SHA-256 `sha256`. This reuses mobile's durable UUID Idempotency-Key,\nMIME-bearing file part and explicit capture metadata convention. Captures retain\noriginal bytes and a parent metadata evidence record. Configure both the wire\nlimit `maxRequestBytes` and receipt `maxBytes`/`maxParts`; include\n`application/json` in the media allowlist for capture/email metadata. Signature\nchecks cover PDF, TIFF, PNG/JPEG/WebP and WAV/Ogg/WebM/MP3/MP4 audio; signature\nacceptance promises preservation, not extraction support or codec validity.\n\nEmail is explicitly opt-in per configured account. `EmailSourceAdapter` takes a\n`readMessage` callback compatible with the messages owner's\n`EmailAccount.readIntakeMessage(providerLookup, maxBytes, imapIdentity?)` snapshot.\nThe provider lookup is Gmail message ID, POP3 UIDL, or IMAP RFC Message-ID; it\nis separate from the delivery dedupe key. IMAP requires `{ folder, uidValidity,\nuid }`: the method selects the mailbox and verifies UIDVALIDITY and returned UID.\nAmbiguous sender IDs resolving to another UID fail explicitly. That method connects,\nfetches the full message including actual attachment bytes, copies the bytes,\nand disconnects. Metadata-only attachments fail explicitly. It never reads an\nattachment path supplied by a provider. A host lists provider deliveries using a\nstable account-specific locator (for IMAP include folder, UIDVALIDITY and UID),\nan explicit revision, and an opaque resumable checkpoint. Same locator/revision\nwith changed bytes conflicts; an explicit new revision creates a new receipt.\nDo not use sender-controlled RFC Message-ID as the sole transport identity.\n`receiveBatch` processes in order and stops at the first failure. The host owns\ndurable checkpoint storage and commits its `acknowledge` callback only after the\nadapter has received accepted/duplicate. If checkpoint persistence fails, retry\nreturns the original receipt. Message/thread/attachment relationships are copied;\ncredentials and arbitrary headers are excluded. The ordinary `syncFrom` metadata\nsync remains separate; `hasAttachments` is never proof of byte preservation.\n\nWatch-folder intake polls only an explicitly configured existing private local\ndirectory. Producers should close their files, preferably publishing with atomic\nrename. Two unchanged size/inode/mtime/ctime observations separated by\n`stabilityMs` are required, and the interval runs again after claim/restart.\nA private `.ingestion-claims/<UUID>/claim.json` intent is synced before atomically\nrenaming the original into that directory. Keep the directory on one filesystem,\nunder the host's ownership; symlink inputs are refused. A file descriptor still\nheld by an uncooperative producer can write after any stability window, so the\nproducer close/publish protocol is required for immutable input. Accepted or\nduplicate receipts move the entire claim to `.ingestion-processed`; do not delete\nclaims to retry. Polling recovers persisted UUIDs after process death, so receipt\nreplay deduplicates ready-without-ack. `failure.json` and poll results expose safe\ncategories while originals remain available. Intent directories without an\noriginal mean death before claim or a competing poll won; the producer original\nhas not been removed. Operators may remove these empty intents after confirming\nno active importer. Keep processed originals according to the host's retention\npolicy. This adapter does not watch arbitrary paths or start background loops.\n\nVendor webhooks use a host `verify(request, originalBytes)` callback after bounded\nraw-body reading. The callback must verify signature, delivery time/replay window\nand account ownership, then return trusted binding and durable delivery identity.\nNo vendor implementation or hardware driver is selected. All adapters invoke\nonly receipt preservation, never domain writes, AI tools or external actions.\n\nSee [PROPOSALS.md](PROPOSALS.md) for bounded application catalog discovery, durable\ninterpretation revisions, optional typed decisions and explicit generated preview.\n\n## Human review\n\nMount the `/svelte` inbox and review components through an authenticated host using\nthe browser-safe `IntakeReviewHost` DTO contract. See [REVIEW.md](REVIEW.md) for\nexact revision-bound callbacks, live evidence access, logical split corrections,\nassignment ownership, and the maintained upload-to-domain-result browser fixture.\n`IntakeWorkspace` puts the inbox, the selected item with its original inline,\nthe message box and approve-and-advance on one page with keyboard next/previous;\nit uses the same callbacks and adds no authority (REVIEW.md, \"Single-page triage\nworkspace\").\n\nOptional explicit feedback and scoped example retrieval are documented in\n[FEEDBACK.md](FEEDBACK.md). Approval and execution outcomes remain separate from\ninterpretation correctness; routing policy adoption is explicit and versioned.\n\nReviewers can start any offered handler for an item (`startReviewerAction`) or\nclose it as handled without action (`resolveItem`); the published host adapter\nwires both as `startAction`/`resolve` and exposes the offered `catalog`. Hosts\nwith their own transport add the two POST operations behind the same session,\nCSRF and reviewer checks as `decide`, and map a thrown\n`HandlerArgumentsInvalidError` (name and value-free `issues`) from `startAction`\nexactly as they do for `preview` so the form stays inline (see\n[REVIEW.md](REVIEW.md)). The `intake_dispositions` table requires\na manifest migration before upgrading.\n\n## Recipe preview\n\n`ingestion.inbox` and `ingestion.review` declare the user-facing inbox and\nreview workflows for app discovery. The reference workflow and measured\nevaluation are now published, but the evaluation did not meet supported-reference\nquality gates and did not establish safety. Automation remains disabled. The\nrecipes do not configure a source or provider, expose generic CRUD routes, or\nenable automatic execution. Mount `IntakeInbox` and `IntakeReview` with an\nauthenticated `IntakeReviewHost`; [REVIEW.md](REVIEW.md) remains the source of\ntruth for its scoped callbacks and freshness checks. See the [adoption\nchecklist](../../docs/evaluation/3677-adoption.md) and [measured\nreport](../../docs/evaluation/3677-measured-evaluation.md) for the release\nlimits.\n\nThe recipe manifest registers those existing browser components as\n`/ingestion/inbox` and `/ingestion/review` routes for a host to mount. The route\nmetadata does not supply a host, source, provider, or demo fixture, and it does\nnot change the preview-only status. Neither recipe declares generic model\nnavigation: the ingestion records have no generated screens, so hosts use the\nauthenticated route surfaces instead. Both recipes declare `both` runtime: their\ncomponents render in a browser, while the authenticated review host and package\ndependency closure require a server.\n\n## First analysis and host constraints\n\n`receive` retains evidence and queues the initial dispatch, but does not choose\nan extraction configuration. Before a worker can analyze a new item, the host must\ncall `analyze(itemId, { stage: 'extract', extraction: { configurationRevision,\nlimits } }, requestId)`, then claim that returned revision and run `extractAnalysis`.\nUse a stable request ID for retries. `repairDispatches` repairs queue delivery;\nit does not supply the missing extraction request. After extraction, use\n`prepareGeneration` and `analyze` to request interpretation.\n\nRetention requires `replayUntil >= expiresAt`; an earlier replay deadline rejects\neven a fresh delivery as `expired`. The replay tombstone must outlive retained\nsource bytes. Set both deadlines explicitly before calling `receive`.\n\nHandler preview `normalizedArgs` must equal the submitted arguments exactly.\nIt is an integrity assertion, not a normalization hook: defaults or tidying must\nhappen before submitting args for preview, so review binds exactly what was sent.\n\nProposal limits must be positive safe integers. `maxHandlers`, `maxCandidates`\nand `maxSuggestions` are each capped at 20; `timeoutMs` is capped at 2147483647.\nInvalid limits are rejected when constructing `IngestionService`, before receipt\nor interpretation. See [REVIEW.md](REVIEW.md) for the published server review host.\n\n`proposals.failureCapture` (`{ enabled, maxBytes?, redact? }`) is off by\ndefault. With `enabled: true` and a host `redact` hook, a failed interpretation\nattempt keeps the model output (or the parsed output that failed validation)\nfor diagnosis: cut to\n`maxBytes` (default 16384, at most 65536 UTF-8 bytes) after the host\n`redact` hook, stored beside the attempt result (not digested), readable only\nthrough the reviewer-authorized `getFailedAttemptCapture(itemId, attemptId?)`\nand removed with the item. A missing or failed hook stores only\n`redaction_failed` metadata, never text. Hosts own the semantic redaction policy;\nno built-in sanitizer is supplied. Invalid settings are rejected at construction.\nSee [PROPOSALS.md](PROPOSALS.md#failed-attempt-capture-opt-in-4285).\n\n## Trusted internal sources\n\nA host with a trusted internal source, such as chat messages recorded in\nsmrt-messages, can receive **by reference**, interpret **inline**, and let the\n**submitter** confirm their own proposal (#4085). None of these modes changes\ncopied receipts: their identity, validation, queueing and retention are\nuntouched, and a replay that switches mode conflicts.\n\n**Receive by reference.** A part may be `{ partId, reference: { owner, id,\nversion? }, snapshot: { text } | { structured } }` instead of bytes. The\nservice derives UTF-8 bytes once (`text/plain` or canonical `application/json`;\ntext, reference fields and owner identity containing NUL or unpaired\nsurrogates are rejected `invalid` on every engine, and `expireOwned` refuses\nthem, so distinct owners can never collapse onto one stored key),\nhashes them into the receipt identity, and stores only that snapshot in the\ndurable evidence row; no AssetStore object or asset record is created, and\n`readEvidence` serves the snapshot after the same hash/length check. The\nreference is identity and provenance, never authority. Such parts must be\ntext or structured; files still arrive as copied bytes.\n\n**Owner-bound retention.** `retention: { kind: 'owner', owner: { kind, id },\nversion, expiresAt, replayUntil, acceptAfter }` binds the receipt to its\nowner's lifecycle. The time deadlines remain required and validated exactly as\nbefore: they are the hard privacy ceiling, and `sweepRetention` and execution's\npolicy ceiling still apply. When the owner is deleted the host calls\n`expireOwned({ kind, id })` from its deletion path; every active receipt bound\nto that owner is revoked and removed through the existing `expire` /\n`repairDeletions` protocol, which redacts the snapshot together with every\nother payload, and the deleted delivery is never re-admitted. The owner key is\nrecorded in the indexed `owner_key` column (`<kind>:<id>`, so `kind` cannot\ncontain `:`). Deletion redaction blanks `owner_key` and `submitter_id` together\nwith every payload: only the receipt identity (`delivery_key`) and the captured\nceiling survive, and the ceiling loses `execution.principalId` unless an\nexternal action is still `executing` or `outcome_unknown`. So once nothing is\nleft to reconcile, a redacted row no longer links an owner to an actor, and a\nrepeated `expireOwned` finds nothing. Redaction also overwrites every\nidentity- or content-derived digest (`payload_digest`, `scope_digest`, analysis\n`input_digest`, attempt `output_digest`, plan `digest` and proposal, decision\nand execution `binding_hash`) with the fixed marker `redacted`; successful\nresult digests remain as the replay tombstone. The principal is retained only\nfor an external action that was still in flight when the item was redacted,\nbecause reconciling it authorizes under that principal; it is dropped when that\nreconciliation reaches a final state, or by the next `repairDeletions` /\n`sweepRetention` pass. For a self-approvable receipt the principal is the\nsubmitter, so until then it identifies the sender. Later deletion sweeps pass\nthis redacted ceiling to the host's `authorize({ operation: 'delete' })`, so\ndelete authorization must not depend on `execution.principalId`: a host that\nrequires it would refuse, and the sweep would stop with later deletion intents\nstill pending. A successful result digest\nis a plain SHA-256 of the canonical result, so it can confirm a guessed result:\nkeep handler results to record ids or opaque references, never content.\n\nA replay of a deleted delivery is rejected and never becomes fresh work. While\ndeletion is still pending it returns `rejected/expired`; once redaction has\ncompleted the identity digests are gone, so it returns `rejected/conflict`.\nSources treat both as terminal and must not retry. `delivery_key` stays in plaintext because it is\nthe unique receipt identity and the package holds no secret to key it: hosts\nmust use an opaque value (for example a random UUID recorded with the message)\nand must not encode owner ids, sender ids or content in `deliveryKey`.\n\nHost ordering for owner deletion: first stop issuing receipts for the owner,\nthen call `expireOwned`, and call it again once in-flight receipts settle (or\nfrom a periodic sweep). A receipt still in flight when its owner is expired\nreturns `{ kind: 'rejected', category: 'expired' }` rather than `accepted`, but\none that reserved its row after the first `expireOwned` is only found by the\nlater call; until then the time ceiling bounds it. Services are confidential-scope bound, so\na host whose owner has receipts in several confidential scopes calls\n`expireOwned` from each scope's service; a foreign tenant or scope sees zero.\n\n**Inline mode.** `receive({ ..., dispatch: 'inline' })` requires text/structured\nparts only, records the route provenance but **no deliverable dispatch intent**\nfor any revision (every intent is retained as `completed`, so no worker or\n`repairDispatches` ever picks it up), and does not enqueue. The host then calls\n`interpretInline(itemId, { requestId, extraction: { configurationRevision,\nlimits } })`, which runs in-process exactly what the job path runs: an\nimmutable `stage: 'extract'` revision claimed under a fenced lease and\nextracted with the built-in UTF-8 extractor, then `prepareGeneration` and the\n`stage: 'interpret'` revision with the same analysis lease, catalog-live\nchecks, provenance and failure categories as `generateProposals`. A retry with\nthe same `requestId` replays the immutable revisions, including when two\nidentical calls race. The interpretation is appended under the item lock only\nwhile its pinned extraction is still the latest revision; if another request\nreprocessed the item meanwhile it fails with `GenerationSourceStaleError` (the\ngeneral latest-extraction check every interpretation append runs, #4228) or,\nfor a pin whose extraction attempt was replaced, `Generation source changed`. The result reports each\nstage's attempt and safe error and, when generation completed, the authorized\n`getCompletedAnalysis` snapshot. Inline is for send-triggered suggestions that\nmust not depend on the scheduler; it is not a bypass of review or execution.\n\n**Submitter-scoped approval.** The service records `submitter_id` as the\nauthenticated `scope.actorId` of the receiving service, server-side; payload\nand source metadata never supply it. `listOwnProposals({ cursor?, limit? })`\nlists only this actor's own active submissions with their saved review pages,\nreloaded under current item/handler/target authority, without the reviewer\ninbox. A submission the actor can no longer read is skipped before projection\nand never named by the cursor; a readable one whose reviews are denied by\ningestion authority is returned as `{ item, reviews: null, unavailable: true }`\nwith no reason, so one revoked grant never hides the others. `submitOwnDecision(input)` accepts `approve`, `reject` or `defer` (never\n`correct`) under the `review_own` authority: the host's execution `authorize`\nmust return `submitter: true` for that actor, the application policy must opt\nin with `submitterReview: true` (narrower layers may only disable it), and the\nitem's recorded submitter must equal the current actor. Every other check is\nthe ordinary one, and `applyAction` remains the single once-only effect path\nunder the approver's live grants. Because the effect runs as the receipt's\ncaptured `capturedCeiling.execution.principalId`, `review_own` additionally\nrequires that principal to be the submitter itself (`submitter_id ===\nscope.actorId === principalId`), checked under the item lock at decision time\nand again at apply; a receipt captured for a bot or service principal can only\nbe approved by a reviewer. The decision is recorded with `mode` (`review` or\n`review_own`) and `selfDecision`, which is true whenever the deciding actor is\nthe item's submitter, in either mode (mirrored as `decisionMode` /\n`selfDecision` on feedback events), so a self-confirmation is never mistaken\nfor independent review and never contributes to automation eligibility. See\n[REVIEW.md](REVIEW.md) and [execution integration](agents/execution.md).\n\n## Dispatch route ownership and legacy recovery\n\nA receipt retains a versioned, server-owned `jobTarget` snapshot, and every\nanalysis dispatch retains the same route. Receipt replay and later analysis\ncannot retarget it. `repairDispatches()` filters by retained route before the\ncalling host's authorization or delivery budget, then rechecks provenance under\nthe item lock. A failed job for another host sharing the same tenant and\nconfidential scope neither runs on this host nor blocks its unrelated receipt.\nMatching routes still require current access within the captured permission\nceiling; their authorization and queue failures remain visible. Job arguments\ncontinue to contain identifiers only.\n\nLegacy receipts lacking route metadata can use existing recorded job authority\nonly when all available provenance agrees. A linked job must match the tenant,\ningestion queue, item, dispatch ID and revision exactly. This preserves recorded\njob authority, not a reconstruction of the host's historical intention. Conflicting\nor malformed provenance fails closed. Deleted jobs are absent provenance: an\nexisting protected route survives deletion, but without one the route remains\nunknown and automatic repair skips it.\n\n`inspectDispatchRoute(itemId)` requires normal read authority and returns a\nstatus, fingerprint and verified target (or null). For genuinely provenance-free\nlegacy receipts a host may opt into `dispatchRecovery.authorize`. Without this\nseparate operator authorization callback recovery is disabled. Quiesce legacy\nworkers and investigate their recorded job history before authorizing recovery;\nmissing history is not proof of a particular intended handler. Call\n`recoverDispatchRoute({ itemId, expectedFingerprint, evidenceRef })` with the\ninspection fingerprint and a nonblank evidence reference of at most 512\ncharacters. The target comes exclusively from that service's trusted,\nconstructor-snapshotted configuration, never from the request.\n\nRecovery acquires the item lock, rechecks current/captured process permissions\nand the separate host operator permission, and verifies the exact fingerprint.\nIt refuses bound, conflicting or stale authority. It appends retained operator,\ndate, evidence and original fingerprint provenance and binds missing dispatch\nmetadata atomically; it does not enqueue, alter original receipt/analysis/action\nfacts, or replace prior provenance. The exact original fingerprint/evidence retry\nreturns the retained binding, including under concurrent delivery, but still\nrechecks both permissions. A different decision conflicts. Once reviewed,\nnormal matching-host `repairDispatches()` may enqueue work separately. Hosts\nwithout legacy tenants need not configure recovery at all.\n"
1713
+ "content": "# @happyvertical/smrt-ingestion\n\nDurable receipt, evidence, analysis attempts and action-journal foundations for\n[ADR 0004](../../docs/adr/0004-application-aware-ingestion.md).\n\nThe root, `/models` and `/dto` entry points are browser-safe. Import\n`IngestionService` from `/server` in trusted host code. Generated REST, CLI and\nMCP access to the persistence models is disabled; hosts expose authorized DTO\nmethods through their authenticated application transport.\n\nBefore starting a service, deploy the package manifest through the public s-m-r-t\nmigration APIs or `smrt db:migrate`. `assertReady()` only checks existing tables.\nSQLite and PostgreSQL are supported; other engines fail construction. Index\nadditions ship the same way: `intake_items_received_idx` (newest-first inbox\npaging on `tenant_id, confidential_scope_id, created_at, id`) is declared on the model and reaches existing deployments through the\nnext manifest migration; there are no hand-written migration files in this\npackage, and `smrt doctor --db` / `db:status --parity` report the index until\nit is applied.\n\n`listItemsPage({ limit?, cursor?, state?, itemIds?, scanLimit? })` returns\n`{ items: [{ item, state, cursor }], scanned, nextCursor? }`: one authorized\ninbox page newest receipt first with each row's durable inbox state and\ncontinuation cursor, and the number of rows the call examined. `listItems()`\nremains as a deprecated array-returning method that walks the private page\nscan to the end. Without the optional `cursorKey` service option every cursor\nnames the last row the actor can read, and a page that finds no readable row\nwithin its scan limit fails with `IntakeScanLimitError`; with a server secret\nof at least 32 bytes, cursors are sealed and such a page resumes past what it\nskipped. `scanned` is for host\naccounting only. See [REVIEW.md](REVIEW.md) for the paging and authorization\ncontract.\n\nA host supplies a database, AssetRuntime, authenticated tenant/actor/confidential\nscope, a live authorization callback, a registered jobs target, and an idempotent\n`purgeDerived` callback for host-owned previews, retrieval indexes and caches.\nThe authorization callback receives the captured ceiling and the current\ntransaction executor; it must intersect current grants with that ceiling. Never\nconstruct the scope, retention policy or budgets from incoming document labels.\n\n`receive` accepts bounded original byte buffers, with stable source part IDs,\nparent relationships, optional upstream identity/version and an optional\noriginal `filename` (kept for labels on `IntakeEvidenceDTO.filename`, never\npart of receipt identity). The multipart upload source, email attachments and\nthe watched folder all supply it, as may a host `snapshotReference` part or a\nvendor webhook's verified parts. Every source delivers through one place that\nremoves control, format (bidi, zero-width, tag), line-separator and\ndefault-ignorable characters (Unicode properties, see\n`src/presented-filename.ts`) and drops a name that is then empty or over 512\ncharacters, so an odd name falls back to the default label instead of failing\nthe receipt (`receive` itself rejects such a name from a direct caller as\n`invalid`, and names are cleaned again when read back). Removing every format\nand default-ignorable character has display-only effects, since nothing uses\nthe name as identity: emoji ZWJ sequences and presentation selectors flatten,\nZWNJ/ZWJ are removed from Persian and Indic text (letters may join\ndifferently), and ideographic variation selectors are removed (a CJK variant\nshows its base glyph). Unicode normalization\n(NFC/NFKC) and confusable look-alike characters are out of scope: the name is\nstill untrusted text, so render it as text, never as markup or a path. The\nupstream `sourceReference` is stored but not exposed on the DTO: its ids are\nupstream locators, not display text, and they are never cleaned. For email\nthey are the provider's message locator (plus the attachment part id); for the\nwatched folder the id is the raw folder entry name, whose cleaned form is what\n`filename` exposes. Source adapters own\nstreaming and upstream snapshot retrieval. Receipt identity is tenant + source +\ndelivery key; changing bytes or authenticated scope conflicts. Content equality\nin a different conversation does not deduplicate an arrival. The returned item\nID and version are safe acknowledgment data, never a storage URL.\n\nStorage intents contain a stable asset ID and a public AssetStore-planned URI\nbefore byte I/O. Preservation verifies existing bytes instead of replacing them.\nA receipt becomes ready only after every part is durable and its dispatch intent\ncommits. A source retries the same receive call after failure or lost\nacknowledgment. Hosts schedule `repairDispatches` after restart and periodically;\nit repairs missing/failed queue submissions through SmrtJobCollection. The host\njob restores the captured scope and calls the analysis lifecycle methods. Run\nTaskRunner with the `ingestion` queue enabled.\n\n`analyze` appends immutable input/configuration revisions. `claimAnalysis` uses\nleases, monotonic fences and attempt budgets; `completeAnalysis` rejects expired\nor superseded workers. Terminal attempt output/provenance is preserved. Queue\nsubmission budgets also stop repeated handler failures in visible\n`needs_attention`. Reprocessing does not mint actions. `createAction`,\n`publishProposal` and `appendPlan` persist explicit intentions and exact attempt\nbindings for the later review/execution layer. They do not approve or apply\nbusiness operations. Decision/execution/feedback models are persistence\nfoundations; policy, handler expansion, review and business apply belong to the\nlater epic children.\n\n`expire` revokes access and pending work before removing bytes and calling\n`purgeDerived`. Interrupted deletion is retried by `repairDeletions` and\n`sweepRetention`. Minimal receipt/action identities remain, including successful\naction result digests; all content-bearing journal payloads are redacted. Private\ncleanup locators remain in deletion tombstones so repeated sweeps can remove a\nlate stale writer's orphan bytes even after the asset owner row is gone. Source\nadapters must enforce their configured delivery-age cutoff; expired replay IDs\nnever become fresh work. Continue running the cleanup sweep for the retained\nstorage namespace. Upstream source mutation/deletion cannot change copied\noriginals; explicit privacy deletion revokes every ingestion-derived payload and\nrequires host cache/index purge.\n\nSee [EXTRACTION.md](EXTRACTION.md) for bounded extraction, lease-bound input\nsnapshots, provider isolation requirements and the extraction corpus.\n\nValidation commands are `pnpm test`, `pnpm test:postgres`, `pnpm test:providers`, `pnpm typecheck` and\n`pnpm build` (including built browser import inspection). The PostgreSQL lane\nrequires the repository's configured database service and never silently skips.\nThe provider lane exercises local SDK adapters and process isolation; it does not\nclaim live remote recognition quality. `pnpm test:components` and `pnpm test:e2e`\ncover the maintained review surface. `pnpm test:evaluation` checks repository\nevaluation tooling contracts; measured held-out quality requires a separately\nfrozen protocol and an approved aggregate provider budget. See the [behavior matrix](../../docs/test-matrix/3670-ingestion-foundation.md).\n\n## Authoritative review and execution\n\nConfigure `IngestionOptions.execution` for policy, immutable previews, authenticated\ndecisions and idempotent domain execution. See [execution integration](agents/execution.md)\nfor the public handler catalog, required host authorization/target locks, deployment\nmigrations and jobs continuation integration. The maintained application reference\nin `reference/handlers.ts` creates draft ContentDocuments and attaches retained\nevidence through public domain APIs; it is not a published runtime adapter.\n\n## Configured source adapters\n\nThe server entry exports `createSourceDeliveryHandler`, `EmailSourceAdapter`,\n`WatchFolderSourceAdapter`, and `createVendorWebhookHandler`. Construct a\n`SourceBinding` from authenticated host configuration: enabled flag, scoped\n`IngestionService`, stable source ID/version, captured authority ceiling,\nretention policy, allowed media types and receipt limits. Re-resolve this binding\nwhen grants/configuration change. Source payloads never supply authority.\n\nThe reference POST handler authenticates before reading bytes and requires\n`Idempotency-Key`. JSON accepts exactly `capturedAt` and one of `text`,\n`structured`, or `reference: { owner: 'asset'|'message', id, version }`.\nThe host's `snapshotReference` must authorize current ownership and return copied\nbytes; an absent snapshot fails rather than acknowledging a mutable locator.\nMultipart accepts one `file` plus `captureId` (equal to the idempotency key),\n`capturedAt`, `captureSource` (`camera`, `native_picker`, `audio`, `document`), and optional\nlowercase SHA-256 `sha256`. This reuses mobile's durable UUID Idempotency-Key,\nMIME-bearing file part and explicit capture metadata convention. Captures retain\noriginal bytes and a parent metadata evidence record. Configure both the wire\nlimit `maxRequestBytes` and receipt `maxBytes`/`maxParts`; include\n`application/json` in the media allowlist for capture/email metadata. Signature\nchecks cover PDF, TIFF, PNG/JPEG/WebP and WAV/Ogg/WebM/MP3/MP4 audio; signature\nacceptance promises preservation, not extraction support or codec validity.\n\nEmail is explicitly opt-in per configured account. `EmailSourceAdapter` takes a\n`readMessage` callback compatible with the messages owner's\n`EmailAccount.readIntakeMessage(providerLookup, maxBytes, imapIdentity?)` snapshot.\nThe provider lookup is Gmail message ID, POP3 UIDL, or IMAP RFC Message-ID; it\nis separate from the delivery dedupe key. IMAP requires `{ folder, uidValidity,\nuid }`: the method selects the mailbox and verifies UIDVALIDITY and returned UID.\nAmbiguous sender IDs resolving to another UID fail explicitly. That method connects,\nfetches the full message including actual attachment bytes, copies the bytes,\nand disconnects. Metadata-only attachments fail explicitly. It never reads an\nattachment path supplied by a provider. A host lists provider deliveries using a\nstable account-specific locator (for IMAP include folder, UIDVALIDITY and UID),\nan explicit revision, and an opaque resumable checkpoint. Same locator/revision\nwith changed bytes conflicts; an explicit new revision creates a new receipt.\nDo not use sender-controlled RFC Message-ID as the sole transport identity.\n`receiveBatch` processes in order and stops at the first failure. The host owns\ndurable checkpoint storage and commits its `acknowledge` callback only after the\nadapter has received accepted/duplicate. If checkpoint persistence fails, retry\nreturns the original receipt. Message/thread/attachment relationships are copied;\ncredentials and arbitrary headers are excluded. The ordinary `syncFrom` metadata\nsync remains separate; `hasAttachments` is never proof of byte preservation.\n\nWatch-folder intake polls only an explicitly configured existing private local\ndirectory. Producers should close their files, preferably publishing with atomic\nrename. Two unchanged size/inode/mtime/ctime observations separated by\n`stabilityMs` are required, and the interval runs again after claim/restart.\nA private `.ingestion-claims/<UUID>/claim.json` intent is synced before atomically\nrenaming the original into that directory. Keep the directory on one filesystem,\nunder the host's ownership; symlink inputs are refused. A file descriptor still\nheld by an uncooperative producer can write after any stability window, so the\nproducer close/publish protocol is required for immutable input. Accepted or\nduplicate receipts move the entire claim to `.ingestion-processed`; do not delete\nclaims to retry. Polling recovers persisted UUIDs after process death, so receipt\nreplay deduplicates ready-without-ack. `failure.json` and poll results expose safe\ncategories while originals remain available. Intent directories without an\noriginal mean death before claim or a competing poll won; the producer original\nhas not been removed. Operators may remove these empty intents after confirming\nno active importer. Keep processed originals according to the host's retention\npolicy. This adapter does not watch arbitrary paths or start background loops.\n\nVendor webhooks use a host `verify(request, originalBytes)` callback after bounded\nraw-body reading. The callback must verify signature, delivery time/replay window\nand account ownership, then return trusted binding and durable delivery identity.\nNo vendor implementation or hardware driver is selected. All adapters invoke\nonly receipt preservation, never domain writes, AI tools or external actions.\n\nSee [PROPOSALS.md](PROPOSALS.md) for bounded application catalog discovery, durable\ninterpretation revisions, optional typed decisions and explicit generated preview.\n\n## Human review\n\nMount the `/svelte` inbox and review components through an authenticated host using\nthe browser-safe `IntakeReviewHost` DTO contract. See [REVIEW.md](REVIEW.md) for\nexact revision-bound callbacks, live evidence access, logical split corrections,\nassignment ownership, and the maintained upload-to-domain-result browser fixture.\n`IntakeWorkspace` puts the inbox, the selected item with its original inline,\nthe message box and approve-and-advance on one page with keyboard next/previous;\nit uses the same callbacks and adds no authority (REVIEW.md, \"Single-page triage\nworkspace\").\n\nOptional explicit feedback and scoped example retrieval are documented in\n[FEEDBACK.md](FEEDBACK.md). Approval and execution outcomes remain separate from\ninterpretation correctness; routing policy adoption is explicit and versioned.\n\nReviewers can start any offered handler for an item (`startReviewerAction`) or\nclose it as handled without action (`resolveItem`); the published host adapter\nwires both as `startAction`/`resolve` and exposes the offered `catalog`. Hosts\nwith their own transport add the two POST operations behind the same session,\nCSRF and reviewer checks as `decide`, and map a thrown\n`HandlerArgumentsInvalidError` (name and value-free `issues`) from `startAction`\nexactly as they do for `preview` so the form stays inline (see\n[REVIEW.md](REVIEW.md)). The `intake_dispositions` table requires\na manifest migration before upgrading.\n\n## Recipe preview\n\n`ingestion.inbox` and `ingestion.review` declare the user-facing inbox and\nreview workflows for app discovery. The reference workflow and measured\nevaluation are now published, but the evaluation did not meet supported-reference\nquality gates and did not establish safety. Automation remains disabled. The\nrecipes do not configure a source or provider, expose generic CRUD routes, or\nenable automatic execution. Mount `IntakeInbox` and `IntakeReview` with an\nauthenticated `IntakeReviewHost`; [REVIEW.md](REVIEW.md) remains the source of\ntruth for its scoped callbacks and freshness checks. See the [adoption\nchecklist](../../docs/evaluation/3677-adoption.md) and [measured\nreport](../../docs/evaluation/3677-measured-evaluation.md) for the release\nlimits.\n\nThe recipe manifest registers those existing browser components as\n`/ingestion/inbox` and `/ingestion/review` routes for a host to mount. The route\nmetadata does not supply a host, source, provider, or demo fixture, and it does\nnot change the preview-only status. Neither recipe declares generic model\nnavigation: the ingestion records have no generated screens, so hosts use the\nauthenticated route surfaces instead. Both recipes declare `both` runtime: their\ncomponents render in a browser, while the authenticated review host and package\ndependency closure require a server.\n\n## First analysis and host constraints\n\n`receive` retains evidence and queues the initial dispatch, but does not choose\nan extraction configuration. Before a worker can analyze a new item, the host must\ncall `analyze(itemId, { stage: 'extract', extraction: { configurationRevision,\nlimits } }, requestId)`, then claim that returned revision and run `extractAnalysis`.\nUse a stable request ID for retries. `repairDispatches` repairs queue delivery;\nit does not supply the missing extraction request. After extraction, use\n`prepareGeneration` and `analyze` to request interpretation.\n\nRetention requires `replayUntil >= expiresAt`; an earlier replay deadline rejects\neven a fresh delivery as `expired`. The replay tombstone must outlive retained\nsource bytes. Set both deadlines explicitly before calling `receive`.\n\nHandler preview `normalizedArgs` must equal the submitted arguments exactly.\nIt is an integrity assertion, not a normalization hook: defaults or tidying must\nhappen before submitting args for preview, so review binds exactly what was sent.\n\nProposal limits must be positive safe integers. `maxHandlers`, `maxCandidates`\nand `maxSuggestions` are each capped at 20; `timeoutMs` is capped at 2147483647.\nInvalid limits are rejected when constructing `IngestionService`, before receipt\nor interpretation. See [REVIEW.md](REVIEW.md) for the published server review host.\n\n`proposals.failureCapture` (`{ enabled, maxBytes?, redact? }`) is off by\ndefault. With `enabled: true` and a host `redact` hook, a failed interpretation\nattempt keeps the model output (or the parsed output that failed validation)\nfor diagnosis: cut to\n`maxBytes` (default 16384, at most 65536 UTF-8 bytes) after the host\n`redact` hook, stored beside the attempt result (not digested), readable only\nthrough the reviewer-authorized `getFailedAttemptCapture(itemId, attemptId?)`\nand removed with the item. A missing or failed hook stores only\n`redaction_failed` metadata, never text. Hosts own the semantic redaction policy;\nno built-in sanitizer is supplied. Invalid settings are rejected at construction.\nSee [PROPOSALS.md](PROPOSALS.md#failed-attempt-capture-opt-in-4285).\n\n## Trusted internal sources\n\nA host with a trusted internal source, such as chat messages recorded in\nsmrt-messages, can receive **by reference**, interpret **inline**, and let the\n**submitter** confirm their own proposal (#4085). None of these modes changes\ncopied receipts: their identity, validation, queueing and retention are\nuntouched, and a replay that switches mode conflicts.\n\n**Receive by reference.** A part may be `{ partId, reference: { owner, id,\nversion? }, snapshot: { text } | { structured } }` instead of bytes. The\nservice derives UTF-8 bytes once (`text/plain` or canonical `application/json`;\ntext, reference fields and owner identity containing NUL or unpaired\nsurrogates are rejected `invalid` on every engine, and `expireOwned` refuses\nthem, so distinct owners can never collapse onto one stored key),\nhashes them into the receipt identity, and stores only that snapshot in the\ndurable evidence row; no AssetStore object or asset record is created, and\n`readEvidence` serves the snapshot after the same hash/length check. The\nreference is identity and provenance, never authority. Such parts must be\ntext or structured; files still arrive as copied bytes.\n\n**Owner-bound retention.** `retention: { kind: 'owner', owner: { kind, id },\nversion, expiresAt, replayUntil, acceptAfter }` binds the receipt to its\nowner's lifecycle. The time deadlines remain required and validated exactly as\nbefore: they are the hard privacy ceiling, and `sweepRetention` and execution's\npolicy ceiling still apply. When the owner is deleted the host calls\n`expireOwned({ kind, id })` from its deletion path; every active receipt bound\nto that owner is revoked and removed through the existing `expire` /\n`repairDeletions` protocol, which redacts the snapshot together with every\nother payload, and the deleted delivery is never re-admitted. The owner key is\nrecorded in the indexed `owner_key` column (`<kind>:<id>`, so `kind` cannot\ncontain `:`). Deletion redaction blanks `owner_key` and `submitter_id` together\nwith every payload: only the receipt identity (`delivery_key`) and the captured\nceiling survive, and the ceiling loses `execution.principalId` unless an\nexternal action is still `executing` or `outcome_unknown`. So once nothing is\nleft to reconcile, a redacted row no longer links an owner to an actor, and a\nrepeated `expireOwned` finds nothing. Redaction also overwrites every\nidentity- or content-derived digest (`payload_digest`, `scope_digest`, analysis\n`input_digest`, attempt `output_digest`, plan `digest` and proposal, decision\nand execution `binding_hash`) with the fixed marker `redacted`; successful\nresult digests remain as the replay tombstone. The principal is retained only\nfor an external action that was still in flight when the item was redacted,\nbecause reconciling it authorizes under that principal; it is dropped when that\nreconciliation reaches a final state, or by the next `repairDeletions` /\n`sweepRetention` pass. For a self-approvable receipt the principal is the\nsubmitter, so until then it identifies the sender. Later deletion sweeps pass\nthis redacted ceiling to the host's `authorize({ operation: 'delete' })`, so\ndelete authorization must not depend on `execution.principalId`: a host that\nrequires it would refuse, and the sweep would stop with later deletion intents\nstill pending. A successful result digest\nis a plain SHA-256 of the canonical result, so it can confirm a guessed result:\nkeep handler results to record ids or opaque references, never content.\n\nA replay of a deleted delivery is rejected and never becomes fresh work. While\ndeletion is still pending it returns `rejected/expired`; once redaction has\ncompleted the identity digests are gone, so it returns `rejected/conflict`.\nSources treat both as terminal and must not retry. `delivery_key` stays in plaintext because it is\nthe unique receipt identity and the package holds no secret to key it: hosts\nmust use an opaque value (for example a random UUID recorded with the message)\nand must not encode owner ids, sender ids or content in `deliveryKey`.\n\nHost ordering for owner deletion: first stop issuing receipts for the owner,\nthen call `expireOwned`, and call it again once in-flight receipts settle (or\nfrom a periodic sweep). A receipt still in flight when its owner is expired\nreturns `{ kind: 'rejected', category: 'expired' }` rather than `accepted`, but\none that reserved its row after the first `expireOwned` is only found by the\nlater call; until then the time ceiling bounds it. Services are confidential-scope bound, so\na host whose owner has receipts in several confidential scopes calls\n`expireOwned` from each scope's service; a foreign tenant or scope sees zero.\n\n**Inline mode.** `receive({ ..., dispatch: 'inline' })` requires text/structured\nparts only, records the route provenance but **no deliverable dispatch intent**\nfor any revision (every intent is retained as `completed`, so no worker or\n`repairDispatches` ever picks it up), and does not enqueue. The host then calls\n`interpretInline(itemId, { requestId, extraction: { configurationRevision,\nlimits } })`, which runs in-process exactly what the job path runs: an\nimmutable `stage: 'extract'` revision claimed under a fenced lease and\nextracted with the built-in UTF-8 extractor, then `prepareGeneration` and the\n`stage: 'interpret'` revision with the same analysis lease, catalog-live\nchecks, provenance and failure categories as `generateProposals`. A retry with\nthe same `requestId` replays the immutable revisions, including when two\nidentical calls race. The interpretation is appended under the item lock only\nwhile its pinned extraction is still the latest revision; if another request\nreprocessed the item meanwhile it fails with `GenerationSourceStaleError` (the\ngeneral latest-extraction check every interpretation append runs, #4228) or,\nfor a pin whose extraction attempt was replaced, `Generation source changed`. The result reports each\nstage's attempt and safe error and, when generation completed, the authorized\n`getCompletedAnalysis` snapshot. Inline is for send-triggered suggestions that\nmust not depend on the scheduler; it is not a bypass of review or execution.\n\n**Submitter-scoped approval.** The service records `submitter_id` as the\nauthenticated `scope.actorId` of the receiving service, server-side; payload\nand source metadata never supply it. `listOwnProposals({ cursor?, limit? })`\nlists only this actor's own active submissions with their saved review pages,\nreloaded under current item/handler/target authority, without the reviewer\ninbox. A submission the actor can no longer read is skipped before projection\nand never named by the cursor; a readable one whose reviews are denied by\ningestion authority is returned as `{ item, reviews: null, unavailable: true }`\nwith no reason, so one revoked grant never hides the others. `submitOwnDecision(input)` accepts `approve`, `reject` or `defer` (never\n`correct`) under the `review_own` authority: the host's execution `authorize`\nmust return `submitter: true` for that actor, the application policy must opt\nin with `submitterReview: true` (narrower layers may only disable it), and the\nitem's recorded submitter must equal the current actor. Every other check is\nthe ordinary one, and `applyAction` remains the single once-only effect path\nunder the approver's live grants. Because the effect runs as the receipt's\ncaptured `capturedCeiling.execution.principalId`, `review_own` additionally\nrequires that principal to be the submitter itself (`submitter_id ===\nscope.actorId === principalId`), checked under the item lock at decision time\nand again at apply; a receipt captured for a bot or service principal can only\nbe approved by a reviewer. The decision is recorded with `mode` (`review` or\n`review_own`) and `selfDecision`, which is true whenever the deciding actor is\nthe item's submitter, in either mode (mirrored as `decisionMode` /\n`selfDecision` on feedback events), so a self-confirmation is never mistaken\nfor independent review and never contributes to automation eligibility. See\n[REVIEW.md](REVIEW.md) and [execution integration](agents/execution.md).\n\n## Dispatch route ownership and legacy recovery\n\nA receipt retains a versioned, server-owned `jobTarget` snapshot, and every\nanalysis dispatch retains the same route. Receipt replay and later analysis\ncannot retarget it. `repairDispatches()` filters by retained route before the\ncalling host's authorization or delivery budget, then rechecks provenance under\nthe item lock. A failed job for another host sharing the same tenant and\nconfidential scope neither runs on this host nor blocks its unrelated receipt.\nMatching routes still require current access within the captured permission\nceiling; their authorization and queue failures remain visible. Job arguments\ncontinue to contain identifiers only.\n\nLegacy receipts lacking route metadata can use existing recorded job authority\nonly when all available provenance agrees. A linked job must match the tenant,\ningestion queue, item, dispatch ID and revision exactly. This preserves recorded\njob authority, not a reconstruction of the host's historical intention. Conflicting\nor malformed provenance fails closed. Deleted jobs are absent provenance: an\nexisting protected route survives deletion, but without one the route remains\nunknown and automatic repair skips it.\n\n`inspectDispatchRoute(itemId)` requires normal read authority and returns a\nstatus, fingerprint and verified target (or null). For genuinely provenance-free\nlegacy receipts a host may opt into `dispatchRecovery.authorize`. Without this\nseparate operator authorization callback recovery is disabled. Quiesce legacy\nworkers and investigate their recorded job history before authorizing recovery;\nmissing history is not proof of a particular intended handler. Call\n`recoverDispatchRoute({ itemId, expectedFingerprint, evidenceRef })` with the\ninspection fingerprint and a nonblank evidence reference of at most 512\ncharacters. The target comes exclusively from that service's trusted,\nconstructor-snapshotted configuration, never from the request.\n\nRecovery acquires the item lock, rechecks current/captured process permissions\nand the separate host operator permission, and verifies the exact fingerprint.\nIt refuses bound, conflicting or stale authority. It appends retained operator,\ndate, evidence and original fingerprint provenance and binds missing dispatch\nmetadata atomically; it does not enqueue, alter original receipt/analysis/action\nfacts, or replace prior provenance. The exact original fingerprint/evidence retry\nreturns the retained binding, including under concurrent delivery, but still\nrechecks both permissions. A different decision conflicts. Once reviewed,\nnormal matching-host `repairDispatches()` may enqueue work separately. Hosts\nwithout legacy tenants need not configure recovery at all.\n\n`IngestionService.getReceiptSource(itemId)` returns the authorized immutable\nsource ID, source version and delivery identity for domain retention/recovery.\nIt uses the same current read/tenant/confidential-scope boundary as evidence\nreads and exposes no source configuration or credentials. Delivery identity is\nprovenance, never authority.\n"
1714
1714
  },
1715
1715
  {
1716
1716
  "path": "EXTRACTION.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-ingestion",
3
- "version": "0.57.37",
3
+ "version": "0.57.38",
4
4
  "description": "Durable tenant-scoped intake and evidence foundations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -50,32 +50,32 @@
50
50
  }
51
51
  },
52
52
  "dependencies": {
53
- "@happyvertical/smrt-core": "0.57.37",
54
- "@happyvertical/smrt-types": "0.57.37",
55
- "@happyvertical/smrt-tenancy": "0.57.37",
56
- "@happyvertical/smrt-assets": "0.57.37",
57
- "@happyvertical/smrt-jobs": "0.57.37",
53
+ "@happyvertical/smrt-core": "0.57.38",
54
+ "@happyvertical/smrt-types": "0.57.38",
55
+ "@happyvertical/smrt-tenancy": "0.57.38",
56
+ "@happyvertical/smrt-assets": "0.57.38",
57
+ "@happyvertical/smrt-jobs": "0.57.38",
58
58
  "@happyvertical/sql": "^0.110.0",
59
59
  "@happyvertical/pdf": "^0.65.9",
60
60
  "@happyvertical/ocr": "^0.62.1",
61
61
  "@happyvertical/speech": "^0.110.0",
62
62
  "@happyvertical/ai": "^0.110.0",
63
63
  "sharp": "^0.35.5",
64
- "@happyvertical/smrt-agents": "0.57.37",
65
- "@happyvertical/smrt-playbooks": "0.57.37",
64
+ "@happyvertical/smrt-agents": "0.57.38",
65
+ "@happyvertical/smrt-playbooks": "0.57.38",
66
66
  "ajv": "8.20.0",
67
- "@happyvertical/smrt-ui": "0.57.37"
67
+ "@happyvertical/smrt-ui": "0.57.38"
68
68
  },
69
69
  "devDependencies": {
70
- "@happyvertical/smrt-vitest": "0.57.37",
70
+ "@happyvertical/smrt-vitest": "0.57.38",
71
71
  "@types/node": "24.13.6",
72
72
  "typescript": "6.0.3",
73
73
  "vite": "8.1.6",
74
74
  "vitest": "4.1.10",
75
- "@happyvertical/smrt-content": "0.57.37",
76
- "@happyvertical/smrt-messages": "0.57.37",
75
+ "@happyvertical/smrt-content": "0.57.38",
76
+ "@happyvertical/smrt-messages": "0.57.38",
77
77
  "@happyvertical/email": "^0.110.0",
78
- "@happyvertical/smrt-users": "0.57.37",
78
+ "@happyvertical/smrt-users": "0.57.38",
79
79
  "@sveltejs/package": "^2.5.8",
80
80
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
81
81
  "svelte": "^5.57.1",