@happyvertical/smrt-ingestion 0.57.4 → 0.57.6

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,19 +3,19 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-ingestion",
6
- "packageVersion": "0.57.4",
6
+ "packageVersion": "0.57.6",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "4f5d29292192b99da4a183bf97c5f690fab3a2a0cf6ca0193c0eac1f821ade65",
11
- "packageJson": "5e4a04fcc1ad4448d4adab2713edf41f1124c72e64436887b90baec535e20653",
10
+ "manifest": "e9ceb1ecd89b36c6f49decf96fb532e2d2bd978f3394f25295ca3cb2bb3953e3",
11
+ "packageJson": "c9fae2cf0e186e2cad34f8e1f7873f0d60a761496ab51e1174e690a1a06abf6e",
12
12
  "agents": "dea14442054ec1de798cba9be1fbdd955f44b0e4672639cbf3db8101d290c1fb",
13
13
  "moduleDoc:agents/execution.md": "b9ce254892a5dbd44c762b6f8b718d2f2705779d67b893418fc8fcc5677e2179",
14
- "moduleDoc:README.md": "c7a4399b07fb5e2b8e26f4ef19364ca0a39046d4575813d8ec9386fb2c6aa241",
14
+ "moduleDoc:README.md": "75ad0e22d27c1cc8b18cd0666f1928a510f45403aeb3be8e5decef8d756b4846",
15
15
  "moduleDoc:EXTRACTION.md": "b735a25a7be75f3a13f3f9e92b0c25834c1394835c16ccd73a076d5720c14243",
16
16
  "moduleDoc:PROPOSALS.md": "15109f9a3f1fd5fb5c4390b4d8e691cdef7a2a35e7491791baeb30f602c342e9",
17
17
  "moduleDoc:FEEDBACK.md": "1da3dcf033d60aaf3e67c9813efcfe58cb69d3c238aa4526c33edaa884357f06",
18
- "moduleDoc:REVIEW.md": "ca3e7d9275d42ca3daabfdced0492df80770a438d007cc0d06608192cc841200",
18
+ "moduleDoc:REVIEW.md": "dc7514212b58191a4b9ea77d665340cb6e9c8bb18458098cf7b231c45b87f3d1",
19
19
  "agentSurface:src/test-support/execution.ts": "1df56aaf1a692d822ed45ecb7e50df7d6806977ff3c03f9eeaf171c64f496865",
20
20
  "agentSurface:src/test-support/proposals.ts": "ab60bd15118400974893041ead2d3e95276c35b4b355bb91b971668fd20aa1dd"
21
21
  },
@@ -1604,7 +1604,7 @@
1604
1604
  {
1605
1605
  "path": "README.md",
1606
1606
  "module": "README",
1607
- "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.\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 and optional upstream identity/version. 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\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\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"
1607
+ "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.\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 and optional upstream identity/version. 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\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\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## 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"
1608
1608
  },
1609
1609
  {
1610
1610
  "path": "EXTRACTION.md",
@@ -1624,7 +1624,7 @@
1624
1624
  {
1625
1625
  "path": "REVIEW.md",
1626
1626
  "module": "REVIEW",
1627
- "content": "# Authenticated review surfaces\n\n`@happyvertical/smrt-ingestion/svelte` exports `IntakeInbox`, `IntakeReview` and\n`EvidenceViewer`. The browser-safe DTO entry exports their `IntakeReviewHost`\ncallback contract. Import styling/theme configuration from smrt-ui in the host.\nComponents use smrt-ui primitives and the `ingestion.*` i18n catalog.\n\nThe host authenticates every request, resolves tenant/actor/confidential scope\nfrom its session, enforces CSRF on mutations, and constructs `IngestionService`.\nNever accept scope, reviewer, permissions or a principal from callback JSON.\nOnly supply authorized evidence URLs and escaped text; original URLs are\nsame-origin authenticated handlers that recheck `readEvidence` on every read.\nCandidate discovery uses `findCandidates` with current grants. Presentation\nstate and disabled buttons provide no server authority.\n\n`listReviews(itemId, {cursor?,limit?})` reloads saved action identities and their\ncurrent plan membership without browser storage. It returns at most 50 actions,\nwith an opaque action-ID cursor. Every payload requires current item, handler,\nparent plan and target access under the owning transaction. Denied reads fail\nclosed. Target reads check current authorization separately from historical\nrevision freshness: unfinished drift returns state-only stale reviews without\nremoving unrelated entries; succeeded results remain readable after authorized\ntarget changes. Apply still enforces exact bound target revisions. Expired entries\ncontain only review identity/state. Stale entries never disclose old args or target\nlabels; an authorized parent plan additionally exposes `stalePlan` identity, key,\ncurrent plan revision and handler identity. Hosts must collect fresh explicit args\nfrom the reviewer/current authorized evidence and call `previewPlan` with that\nexact plan CAS and the current completed analysis attempt. Action revision is not\nplan revision. Re-expansion preserves action identity and requires fresh approval.\nSuccess results are rechecked through the durable result reader.\nPlan membership contains the exact parent args/version so edits call\n`previewPlan`, not per-step correction. An item may have more review pages; the\nhost must aggregate state across pages when projecting a production inbox state.\n\nThe host's `load` callback supplies a current `ItemReviewView`, using\n`getCompletedAnalysis` with no historical fallback. Generated suggestions bind\nthe **current generation attempt**, not `generation.source.attemptId` (the prior\nextraction). `previewGeneratedProposals` publishes exact reviews and continues to reject stale\ngeneration candidate pins. When `listReviews` reports `generationStale`, the host\nomits obsolete generation output and renders authorized saved reviews/results.\nCompleted saved actions do not hide healthy unprepared suggestions. The saved-binding hint is not\nan authoritative generation-availability check: a fresh human preview can bind a\ncurrent target while the immutable generation still pins its former revision.\n`getCompletedAnalysis` remains strict and throws server-only\n`GenerationSnapshotStaleError` only after every current catalog/provider/candidate\nread is authorized and an immutable candidate revision differs. The maintained\nhost catches only that class, reacquires `listReviews` under current owning\nauthority, and returns that new page without generation output. It never reuses\nthe earlier page after an error. Denials and unknown failures propagate; healthy\ngeneration still exposes unprepared sibling suggestions. Stale entries expose handler\nand attempt identity only when that attempt is still the current completed/partial\nanalysis. An empty-args editor submits fresh explicit human arguments through\n`previewProposal` or `previewPlan` with the saved CAS; it never reuses old generated\narguments, rewrites the generation snapshot or transfers approval. Analysis-stale\nentries without an attempt require normal reprocessing. Approve,\nreject, defer and correct call `submitDecision`; corrected args create a new\nrevision which requires a separate human approval. After an approve/defer/reject\ndecision, optional `editAction` calls `previewProposal` with the current action\nrevision to publish a new review; completed/executing/unknown actions cannot be\nrevised. Local dirty args disable Apply but confer no authority. Plan edits re-expand through\n`previewPlan` with the current **plan** revision. `applyAction` accepts only an\naction ID. Unknown external outcomes require server reconciliation; the UI\nnever offers a blind resend. Stable result IDs survive reload and retry.\n\nReplace `contextKey` whenever authenticated tenant/session context changes.\nChanging it or itemId destroys the old view and fences late callback results.\nDenied refresh clears prior originals, candidates, arguments and results.\nMissing confidence is unavailable; page/time links exist only for observed\nlocations. Provider errors and unresolved/partial results remain review states.\n\n## Assignment and logical splits\n\nAssignment is explicitly host-owned, durable and versioned. `assign` receives\nan expected assignment version and request ID; the host must authorize the\nmanager, constrain eligible assignees, CAS the durable record and reject stale\nwrites. Assignment does not grant read, review or execution rights. The maintained\nfixture provisions its own SQLite assignment table before serving requests; no\nproduction ingestion table or migration is introduced for assignment.\n\n`reviseLogicalSplit` authorizes the current generation through the same-executor\ncatalog/candidate gate and the existing reviewer grant and policy reviewer list.\nProcess access alone cannot mint or replay a human correction; reviewer authority\nis checked before mutation and again before publication. A generation without a\nreviewable catalogue cannot be corrected through this reviewer API.\nIt requires all observed pages exactly once, an exact\ncurrent attempt/revision and a request ID. It records the authenticated actor\nand predecessor as a **human correction** in a new immutable extraction\nconfiguration, queues re-extraction and immediately makes previous suggestions\nand approvals stale. It never changes original bytes/evidence or labels a human\nedit as model output. The host processes the new extraction, then calls\n`prepareGeneration` and the normal fenced interpretation stage. The supported\nimmediate-predecessor extraction pin is unchanged. The generator receives a\nhuman correction only after its digest matches the authoritative source. Ordinary\n`analyze` calls cannot mint an extraction correction; the owning split API is the\nauthenticated origin, and forged interpretation corrections fail before provider\ndisclosure. If its proposed grouping differs from the correction, suggestions are withheld\nwith `human_split_not_respected`. New proposals require new explicit review.\nThe reference host runs these stages synchronously for browser determinism;\nproduction hosts dispatch the existing durable analysis jobs.\n\n`feedback` is an optional explicit correctness callback, separate from approval\nchoice and execution success. No learning, feedback storage or rule promotion is\nimplemented by these components; #3676 owns those services. Do not pass feedback\nor a feedback callback to readers without the relevant host grant.\n\n## Maintained browser proof\n\n`pnpm test:components` runs real Svelte component tests; `pnpm test:e2e` runs the\npackage-local authenticated Vite application under Chromium. The fixture uses\nHttpOnly same-site sessions, CSRF, real SQLite/assets/ingestion/content APIs,\nproduction unpdf child extraction and a deterministic proposal generator. Its\nhardcoded test login is fixture-only and is never a production auth provider.\nImage/audio originals remain inspectable when no OCR/speech provider is\nconfigured; the suite makes no recognition-quality claim and spends no provider\nbudget. `reference/review-host.ts` is a maintained development fixture, not a\npublished server entry. Browser tests show actual approved args and persisted\nContentDocument results, stale tabs, reload, retries and negative access.\n\nRun full package `test`, `test:postgres`, `test:providers`, `build`, `typecheck`,\n`test:components`, `test:e2e`; use the root Node/pnpm versions. Set a short\n`CI_TEST_TMPDIR`/`TMPDIR`, and optionally\n`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to the supported installed Chromium.\n`pnpm test:evaluation` checks repository evaluation tooling contracts; measured\nheld-out quality remains a separate frozen-protocol, budget-gated #3677 run.\n\nThe existing CI browser job includes ingestion changes and shared UI primitive\nchanges in affected mode, and runs the ingestion browser suite in full mode. Its\nnormal dependency build precedes the suite and failed runs upload browser evidence.\n\nThe maintained host derives inbox state by traversing bounded public review pages,\nauthorizing every action/parent/target before classification. It retains only\nscalar totals and the requested page; an off-page denial denies the whole view.\nThe component's Load more and subsequent refresh/mutation reload every displayed\npage using fresh cursors, then publish the combined authorized set together. Old\npage payloads are cleared while loading; any unavailable page clears all payloads.\nThis keeps plan controls and earlier steps visible across page boundaries without\nreusing a prior authorization decision.\n\n## Published server adapter\n\nImport `createIntakeReviewHost` and `IntakeReviewHostOptions` from\n`@happyvertical/smrt-ingestion/server`. Call `createIntakeReviewHost(service,\noptions)` with a service constructed from the current authenticated session.\nThe returned `IntakeReviewHost` implements inbox state projection across saved\nreview pages, current generation loading, stable `ui:<index>` intention keys,\naction/plan preview CAS lookup, decisions, apply, corrections, candidates, split\nand feedback. Repeated preparation of an unchanged authorized suggestion retains\nits current review instead of creating a new revision. Changed arguments still\nrequire explicit re-preview and a fresh human decision.\nLists scan 20 items per page; filtered pages may be empty with a `nextCursor`.\nContinue until the cursor is absent. Listing does not load original bytes.\n\nSupply four application adapters: `upload(files, requestId)` (authenticated\nreceipt plus first extraction request), `assignment(itemId)` (durable read),\n`assign(input)` (manager authorization, eligible assignees, durable CAS and request\nreplay protection), and `evidence(part)` (safe label/text or authenticated\nsame-origin URL). Optional `label(item)` changes presentation. Optional\n`enableFeedback: true` exposes the optional feedback callback when the service\nhas feedback configured. Optional `onSplit(itemId, revision)` wakes the host worker after the durable extraction\nrequest; repair dispatches if wakeup fails. The factory creates no assignment\nschema and grants no permissions. Create it per session, authenticate every HTTP\ncall and enforce CSRF before dispatching a mutation.\n\n`GenerationAnalysisOutput` is exported by `/dto` and describes the interpretation\nwrapper `{ proposals: GenerationOutput }` under `analysis.result.output`.\nThe factory unwraps this into `ItemReviewView.generation`. Stale generation is\nomitted while authorized saved reviews remain available; access denials propagate.\nThe reference application still owns its fixture provisioning and HTTP transport.\n"
1627
+ "content": "# Authenticated review surfaces\n\n`@happyvertical/smrt-ingestion/svelte` exports `IntakeInbox`, `IntakeReview` and\n`EvidenceViewer`. The browser-safe DTO entry exports their `IntakeReviewHost`\ncallback contract. Import styling/theme configuration from smrt-ui in the host.\nComponents use smrt-ui primitives and the `ingestion.*` i18n catalog.\n\nThe host authenticates every request, resolves tenant/actor/confidential scope\nfrom its session, enforces CSRF on mutations, and constructs `IngestionService`.\nNever accept scope, reviewer, permissions or a principal from callback JSON.\nOnly supply authorized evidence URLs and escaped text; original URLs are\nsame-origin authenticated handlers that recheck `readEvidence` on every read.\nCandidate discovery uses `findCandidates` with current grants. Presentation\nstate and disabled buttons provide no server authority.\n\n`listReviews(itemId, {cursor?,limit?})` reloads saved action identities and their\ncurrent plan membership without browser storage. It returns at most 50 actions,\nwith an opaque action-ID cursor. Every payload requires current item, handler,\nparent plan and target access under the owning transaction. Denied reads fail\nclosed. Target reads check current authorization separately from historical\nrevision freshness: unfinished drift returns state-only stale reviews without\nremoving unrelated entries; succeeded results remain readable after authorized\ntarget changes. Apply still enforces exact bound target revisions. Expired entries\ncontain only review identity/state. Stale entries never disclose old args or target\nlabels; an authorized parent plan additionally exposes `stalePlan` identity, key,\ncurrent plan revision and handler identity. Hosts must collect fresh explicit args\nfrom the reviewer/current authorized evidence and call `previewPlan` with that\nexact plan CAS and the current completed analysis attempt. Action revision is not\nplan revision. Re-expansion preserves action identity and requires fresh approval.\nSuccess results are rechecked through the durable result reader.\nPlan membership contains the exact parent args/version so edits call\n`previewPlan`, not per-step correction. An item may have more review pages; the\nhost must aggregate state across pages when projecting a production inbox state.\n\nThe host's `load` callback supplies a current `ItemReviewView`, using\n`getCompletedAnalysis` with no historical fallback. Generated suggestions bind\nthe **current generation attempt**, not `generation.source.attemptId` (the prior\nextraction). `previewGeneratedProposals` publishes exact reviews and continues to reject stale\ngeneration candidate pins. When `listReviews` reports `generationStale`, the host\nomits obsolete generation output and renders authorized saved reviews/results.\nCompleted saved actions do not hide healthy unprepared suggestions. The saved-binding hint is not\nan authoritative generation-availability check: a fresh human preview can bind a\ncurrent target while the immutable generation still pins its former revision.\n`getCompletedAnalysis` remains strict and throws server-only\n`GenerationSnapshotStaleError` only after every current catalog/provider/candidate\nread is authorized and an immutable candidate revision differs. The maintained\nhost catches only that class, reacquires `listReviews` under current owning\nauthority, and returns that new page without generation output. It never reuses\nthe earlier page after an error. Denials and unknown failures propagate; healthy\ngeneration still exposes unprepared sibling suggestions. Stale entries expose handler\nand attempt identity only when that attempt is still the current completed/partial\nanalysis. An empty-args editor submits fresh explicit human arguments through\n`previewProposal` or `previewPlan` with the saved CAS; it never reuses old generated\narguments, rewrites the generation snapshot or transfers approval. Analysis-stale\nentries without an attempt require normal reprocessing. Approve,\nreject, defer and correct call `submitDecision`; corrected args create a new\nrevision which requires a separate human approval. After an approve/defer/reject\ndecision, optional `editAction` calls `previewProposal` with the current action\nrevision to publish a new review; completed/executing/unknown actions cannot be\nrevised. Local dirty args disable Apply but confer no authority. Plan edits re-expand through\n`previewPlan` with the current **plan** revision. `applyAction` accepts only an\naction ID. Unknown external outcomes require server reconciliation; the UI\nnever offers a blind resend. Stable result IDs survive reload and retry.\n\nReplace `contextKey` whenever authenticated tenant/session context changes.\nChanging it or itemId destroys the old view and fences late callback results.\nDenied refresh clears prior originals, candidates, arguments and results.\nMissing confidence is unavailable; page/time links exist only for observed\nlocations. Provider errors and unresolved/partial results remain review states.\n\n## Assignment and logical splits\n\nAssignment is explicitly host-owned, durable and versioned. `assign` receives\nan expected assignment version and request ID; the host must authorize the\nmanager, constrain eligible assignees, CAS the durable record and reject stale\nwrites. Assignment does not grant read, review or execution rights. The maintained\nfixture provisions its own SQLite assignment table before serving requests; no\nproduction ingestion table or migration is introduced for assignment.\n\n`reviseLogicalSplit` authorizes the current generation through the same-executor\ncatalog/candidate gate and the existing reviewer grant and policy reviewer list.\nProcess access alone cannot mint or replay a human correction; reviewer authority\nis checked before mutation and again before publication. A generation without a\nreviewable catalogue cannot be corrected through this reviewer API.\nIt requires all observed pages exactly once, an exact\ncurrent attempt/revision and a request ID. It records the authenticated actor\nand predecessor as a **human correction** in a new immutable extraction\nconfiguration, queues re-extraction and immediately makes previous suggestions\nand approvals stale. It never changes original bytes/evidence or labels a human\nedit as model output. The host processes the new extraction, then calls\n`prepareGeneration` and the normal fenced interpretation stage. The supported\nimmediate-predecessor extraction pin is unchanged. The generator receives a\nhuman correction only after its digest matches the authoritative source. Ordinary\n`analyze` calls cannot mint an extraction correction; the owning split API is the\nauthenticated origin, and forged interpretation corrections fail before provider\ndisclosure. If its proposed grouping differs from the correction, suggestions are withheld\nwith `human_split_not_respected`. New proposals require new explicit review.\nThe reference host runs these stages synchronously for browser determinism;\nproduction hosts dispatch the existing durable analysis jobs.\n\n`feedback` is an optional explicit correctness callback, separate from approval\nchoice and execution success. No learning, feedback storage or rule promotion is\nimplemented by these components; #3676 owns those services. Do not pass feedback\nor a feedback callback to readers without the relevant host grant.\n\n## Maintained browser proof\n\n`pnpm test:components` runs real Svelte component tests; `pnpm test:e2e` runs the\npackage-local authenticated Vite application under Chromium. The fixture uses\nHttpOnly same-site sessions, CSRF, real SQLite/assets/ingestion/content APIs,\nproduction unpdf child extraction and a deterministic proposal generator. Its\nhardcoded test login is fixture-only and is never a production auth provider.\nImage/audio originals remain inspectable when no OCR/speech provider is\nconfigured; the suite makes no recognition-quality claim and spends no provider\nbudget. `reference/review-host.ts` is a maintained development fixture, not a\npublished server entry. Browser tests show actual approved args and persisted\nContentDocument results, stale tabs, reload, retries and negative access.\n\nRun full package `test`, `test:postgres`, `test:providers`, `build`, `typecheck`,\n`test:components`, `test:e2e`; use the root Node/pnpm versions. Set a short\n`CI_TEST_TMPDIR`/`TMPDIR`, and optionally\n`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to the supported installed Chromium.\n`pnpm test:evaluation` checks repository evaluation tooling contracts; measured\nheld-out quality remains a separate frozen-protocol, budget-gated #3677 run.\n\nThe existing CI browser job includes ingestion changes and shared UI primitive\nchanges in affected mode, and runs the ingestion browser suite in full mode. Its\nnormal dependency build precedes the suite and failed runs upload browser evidence.\n\nThe maintained host derives inbox state by traversing bounded public review pages,\nauthorizing every action/parent/target before classification. It retains only\nscalar totals and the requested page; an off-page denial denies the whole view.\nThe component's Load more and subsequent refresh/mutation reload every displayed\npage using fresh cursors, then publish the combined authorized set together. Old\npage payloads are cleared while loading; any unavailable page clears all payloads.\nThis keeps plan controls and earlier steps visible across page boundaries without\nreusing a prior authorization decision.\n\n## Published server adapter\n\nImport `createIntakeReviewHost` and `IntakeReviewHostOptions` from\n`@happyvertical/smrt-ingestion/server`. Call `createIntakeReviewHost(service,\noptions)` with a service constructed from the current authenticated session.\nThe returned `IntakeReviewHost` implements inbox state projection across saved\nreview pages, current generation loading, stable `ui:<index>` intention keys,\naction/plan preview CAS lookup, decisions, apply, corrections, candidates, split\nand feedback. Repeated preparation of an unchanged authorized suggestion retains\nits current review instead of creating a new revision. Changed arguments still\nrequire explicit re-preview and a fresh human decision.\nLists scan 20 items per page; filtered pages may be empty with a `nextCursor`.\nContinue until the cursor is absent. Listing does not load original bytes.\n\nSupply four application adapters: `upload(files, requestId)` (authenticated\nreceipt plus first extraction request), `assignment(itemId)` (durable read),\n`assign(input)` (manager authorization, eligible assignees, durable CAS and request\nreplay protection), and `evidence(part)` (safe label/text or authenticated\nsame-origin URL). Optional `label(item)` changes presentation. Optional\n`enableFeedback: true` exposes the optional feedback callback when the service\nhas feedback configured. Optional `onSplit(itemId, revision)` wakes the host worker after the durable extraction\nrequest; repair dispatches if wakeup fails. The factory creates no assignment\nschema and grants no permissions. Create it per session, authenticate every HTTP\ncall and enforce CSRF before dispatching a mutation.\n\n`GenerationAnalysisOutput` is exported by `/dto` and describes the interpretation\nwrapper `{ proposals: GenerationOutput }` under `analysis.result.output`.\nThe factory unwraps this into `ItemReviewView.generation`. Stale generation is\nomitted while authorized saved reviews remain available; access denials propagate.\nThe reference application still owns its fixture provisioning and HTTP transport.\n\n## Explicit upload retries\n\nThe inbox retains selected original files and the receive request identity when\nupload fails or acknowledgment is lost. Retry Upload explicitly with the same\nselection; Refresh does not replace that identity. Selecting files again starts a\nnew receipt identity, even when names match. Successful upload clears the selection\nand identity. Replacing the authenticated host/context or destroying the component\ndiscards both and fences late completion from the prior context. No automatic\nupload retry or authorization bypass occurs.\n\nThe inbox clears only its native file-input value when opening the picker, so\nchoosing the same local file explicitly starts a new upload identity. Opening\nthen dismissing the chooser preserves the separately retained originals and\nrequest identity for retry; it does not clear the shared FilePicker's native-form\nbehavior. Successful upload and host/context disposal still discard retry state.\n"
1628
1628
  }
1629
1629
  ],
1630
1630
  "agentSurface": {
@@ -34,9 +34,12 @@ let items = $state<InboxEntry[]>([]),
34
34
  error = $state(false);
35
35
  let alive = true,
36
36
  epoch = 0;
37
+ let uploadRequestId: string | undefined;
37
38
  onDestroy(() => {
38
39
  alive = false;
39
40
  epoch++;
41
+ files = [];
42
+ uploadRequestId = undefined;
40
43
  });
41
44
  async function load(more = false) {
42
45
  const ticket = ++epoch;
@@ -76,7 +79,10 @@ async function mutate(operation: () => Promise<unknown>, selected = false) {
76
79
  try {
77
80
  const result = await operation();
78
81
  if (!alive || ticket !== epoch) return;
79
- files = [];
82
+ if (selected) {
83
+ files = [];
84
+ uploadRequestId = undefined;
85
+ }
80
86
  if (selected && result && typeof result === 'object' && 'itemId' in result)
81
87
  onselect?.(String(result.itemId));
82
88
  await load();
@@ -84,12 +90,18 @@ async function mutate(operation: () => Promise<unknown>, selected = false) {
84
90
  if (alive && ticket === epoch) {
85
91
  items = [];
86
92
  assignments = {};
87
- files = [];
88
93
  error = true;
89
94
  pending = false;
90
95
  }
91
96
  }
92
97
  }
98
+ async function upload() {
99
+ if (pending || !files.length) return;
100
+ uploadRequestId ??= crypto.randomUUID();
101
+ const requestId = uploadRequestId;
102
+ const originals = [...files];
103
+ await mutate(() => host.upload(originals, requestId), true);
104
+ }
93
105
  onMount(() => {
94
106
  void load();
95
107
  });
@@ -101,8 +113,9 @@ onMount(() => {
101
113
  <Input aria-label={t(M['ingestion.assignee'])} bind:value={assignee} disabled={pending} />
102
114
  <Button onclick={() => load()} disabled={pending}>{t(M['ingestion.refresh'])}</Button>
103
115
  </div>
104
- <FilePicker bind:files multiple label={t(M['ingestion.files'])} description={t(M['ingestion.filesHelp'])} disabled={pending} />
105
- <Button disabled={pending || !files.length} onclick={() => mutate(() => host.upload([...files],crypto.randomUUID()),true)}>{t(M['ingestion.upload'])}</Button>
116
+ <!-- Keep retry Files separately; an unchanged native FileList must not suppress a deliberate reselection. -->
117
+ <FilePicker bind:files onclick={(event) => { event.currentTarget.value = ''; }} onchangefiles={() => { uploadRequestId = undefined; }} multiple label={t(M['ingestion.files'])} description={t(M['ingestion.filesHelp'])} disabled={pending} />
118
+ <Button disabled={pending || !files.length} onclick={upload}>{t(M['ingestion.upload'])}</Button>
106
119
  {#if error}<p role="alert">{t(M['ingestion.error'])}</p>{/if}
107
120
  {#if pending}<p role="status">{t(M['ingestion.loading'])}</p>{/if}
108
121
  {#if !pending && !error && !items.length}<p>{t(M['ingestion.empty'])}</p>{/if}
@@ -1 +1 @@
1
- {"version":3,"file":"InboxView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InboxView.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAkHD,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"InboxView.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InboxView.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AA+HD,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-ingestion",
3
- "version": "0.57.4",
3
+ "version": "0.57.6",
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.4",
54
- "@happyvertical/smrt-types": "0.57.4",
55
- "@happyvertical/smrt-tenancy": "0.57.4",
56
- "@happyvertical/smrt-assets": "0.57.4",
57
- "@happyvertical/smrt-jobs": "0.57.4",
58
- "@happyvertical/sql": "^0.105.1",
53
+ "@happyvertical/smrt-core": "0.57.6",
54
+ "@happyvertical/smrt-types": "0.57.6",
55
+ "@happyvertical/smrt-tenancy": "0.57.6",
56
+ "@happyvertical/smrt-assets": "0.57.6",
57
+ "@happyvertical/smrt-jobs": "0.57.6",
58
+ "@happyvertical/sql": "^0.105.2",
59
59
  "@happyvertical/pdf": "^0.65.9",
60
60
  "@happyvertical/ocr": "^0.62.1",
61
- "@happyvertical/speech": "^0.105.1",
62
- "@happyvertical/ai": "^0.105.1",
61
+ "@happyvertical/speech": "^0.105.2",
62
+ "@happyvertical/ai": "^0.105.2",
63
63
  "sharp": "^0.35.5",
64
- "@happyvertical/smrt-agents": "0.57.4",
65
- "@happyvertical/smrt-playbooks": "0.57.4",
64
+ "@happyvertical/smrt-agents": "0.57.6",
65
+ "@happyvertical/smrt-playbooks": "0.57.6",
66
66
  "ajv": "8.20.0",
67
- "@happyvertical/smrt-ui": "0.57.4"
67
+ "@happyvertical/smrt-ui": "0.57.6"
68
68
  },
69
69
  "devDependencies": {
70
- "@happyvertical/smrt-vitest": "0.57.4",
70
+ "@happyvertical/smrt-vitest": "0.57.6",
71
71
  "@types/node": "24.13.6",
72
72
  "typescript": "6.0.3",
73
73
  "vite": "8.1.4",
74
74
  "vitest": "4.1.10",
75
- "@happyvertical/smrt-content": "0.57.4",
76
- "@happyvertical/smrt-messages": "0.57.4",
77
- "@happyvertical/email": "^0.105.1",
78
- "@happyvertical/smrt-users": "0.57.4",
75
+ "@happyvertical/smrt-content": "0.57.6",
76
+ "@happyvertical/smrt-messages": "0.57.6",
77
+ "@happyvertical/email": "^0.105.2",
78
+ "@happyvertical/smrt-users": "0.57.6",
79
79
  "@sveltejs/package": "^2.5.8",
80
80
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
81
81
  "svelte": "^5.57.1",