@happyvertical/smrt-ingestion 0.57.20 → 0.57.21

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.
Files changed (92) hide show
  1. package/FEEDBACK.md +33 -0
  2. package/PROPOSALS.md +127 -5
  3. package/README.md +42 -2
  4. package/REVIEW.md +339 -3
  5. package/agents/execution.md +15 -0
  6. package/dist/chunks/{execution-NoDh3pjs.js → execution-9CfjxX8I.js} +110 -22
  7. package/dist/chunks/execution-9CfjxX8I.js.map +1 -0
  8. package/dist/chunks/{proposal-errors-VxcTU6Db.js → extraction-CJ-zrbg0.js} +4 -10
  9. package/dist/chunks/extraction-CJ-zrbg0.js.map +1 -0
  10. package/dist/chunks/{feedback-CJB900iG.js → feedback-CuUzWKzP.js} +59 -18
  11. package/dist/chunks/feedback-CuUzWKzP.js.map +1 -0
  12. package/dist/chunks/reviewer-instruction-Db3jDN9a.js +33 -0
  13. package/dist/chunks/reviewer-instruction-Db3jDN9a.js.map +1 -0
  14. package/dist/dto.d.ts +6 -0
  15. package/dist/dto.d.ts.map +1 -1
  16. package/dist/execution-internal.d.ts +2 -0
  17. package/dist/execution-internal.d.ts.map +1 -1
  18. package/dist/execution.d.ts +43 -1
  19. package/dist/execution.d.ts.map +1 -1
  20. package/dist/execution.js +1 -1
  21. package/dist/feedback-dto.d.ts +6 -0
  22. package/dist/feedback-dto.d.ts.map +1 -1
  23. package/dist/feedback.d.ts +7 -0
  24. package/dist/feedback.d.ts.map +1 -1
  25. package/dist/manifest.json +19 -1
  26. package/dist/models.d.ts.map +1 -1
  27. package/dist/models.js +10 -1
  28. package/dist/models.js.map +1 -1
  29. package/dist/presented-filename.d.ts +11 -0
  30. package/dist/presented-filename.d.ts.map +1 -0
  31. package/dist/proposal-contracts.d.ts +31 -6
  32. package/dist/proposal-contracts.d.ts.map +1 -1
  33. package/dist/proposal-dto.d.ts +17 -0
  34. package/dist/proposal-dto.d.ts.map +1 -1
  35. package/dist/proposal-errors.d.ts +16 -0
  36. package/dist/proposal-errors.d.ts.map +1 -1
  37. package/dist/proposal-validation.d.ts +26 -0
  38. package/dist/proposal-validation.d.ts.map +1 -1
  39. package/dist/proposals.d.ts +5 -1
  40. package/dist/proposals.d.ts.map +1 -1
  41. package/dist/proposals.js +261 -69
  42. package/dist/proposals.js.map +1 -1
  43. package/dist/review-dto.d.ts +95 -2
  44. package/dist/review-dto.d.ts.map +1 -1
  45. package/dist/review-host.d.ts +52 -3
  46. package/dist/review-host.d.ts.map +1 -1
  47. package/dist/review-instruction-view.d.ts +12 -0
  48. package/dist/review-instruction-view.d.ts.map +1 -0
  49. package/dist/review-summary.d.ts.map +1 -1
  50. package/dist/reviewer-instruction.d.ts +20 -0
  51. package/dist/reviewer-instruction.d.ts.map +1 -0
  52. package/dist/server.d.ts +171 -4
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +835 -49
  55. package/dist/server.js.map +1 -1
  56. package/dist/smrt-knowledge.json +29 -21
  57. package/dist/sources/common.d.ts +11 -0
  58. package/dist/sources/common.d.ts.map +1 -1
  59. package/dist/sources/email.d.ts.map +1 -1
  60. package/dist/sources/http.d.ts.map +1 -1
  61. package/dist/sources/watch-folder.d.ts.map +1 -1
  62. package/dist/svelte/components/ActionReview.svelte +15 -46
  63. package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -1
  64. package/dist/svelte/components/CandidatePicker.svelte +72 -0
  65. package/dist/svelte/components/CandidatePicker.svelte.d.ts +15 -0
  66. package/dist/svelte/components/CandidatePicker.svelte.d.ts.map +1 -0
  67. package/dist/svelte/components/InboxView.svelte +14 -3
  68. package/dist/svelte/components/InboxView.svelte.d.ts +8 -1
  69. package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -1
  70. package/dist/svelte/components/IntakeInbox.svelte +6 -3
  71. package/dist/svelte/components/IntakeInbox.svelte.d.ts +4 -1
  72. package/dist/svelte/components/IntakeInbox.svelte.d.ts.map +1 -1
  73. package/dist/svelte/components/IntakeReview.svelte +4 -2
  74. package/dist/svelte/components/IntakeReview.svelte.d.ts +2 -0
  75. package/dist/svelte/components/IntakeReview.svelte.d.ts.map +1 -1
  76. package/dist/svelte/components/ReviewView.svelte +403 -62
  77. package/dist/svelte/components/ReviewView.svelte.d.ts +2 -0
  78. package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -1
  79. package/dist/svelte/i18n.d.ts +17 -0
  80. package/dist/svelte/i18n.d.ts.map +1 -1
  81. package/dist/svelte/i18n.js +17 -0
  82. package/dist/svelte/index.d.ts +1 -1
  83. package/dist/svelte/index.d.ts.map +1 -1
  84. package/dist/svelte/view-utils.d.ts +2 -0
  85. package/dist/svelte/view-utils.d.ts.map +1 -1
  86. package/dist/svelte/view-utils.js +12 -0
  87. package/package.json +13 -13
  88. package/dist/chunks/execution-NoDh3pjs.js.map +0 -1
  89. package/dist/chunks/execution-internal-DWteDHM2.js +0 -7
  90. package/dist/chunks/execution-internal-DWteDHM2.js.map +0 -1
  91. package/dist/chunks/feedback-CJB900iG.js.map +0 -1
  92. package/dist/chunks/proposal-errors-VxcTU6Db.js.map +0 -1
@@ -3,21 +3,21 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-ingestion",
6
- "packageVersion": "0.57.20",
6
+ "packageVersion": "0.57.21",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "7757d52126ee1f6a4bf3a758679215b3b56db9cf636f46b17461afc649b8aa26",
11
- "packageJson": "775f21cdd2913315800c62508443c679b1007283b1d16263b5ae02de19586caf",
10
+ "manifest": "5cdb4ffe251fa7aa7708fe0b3285ac593e9394ab745a8c7e3eab0e4480dd4720",
11
+ "packageJson": "d1fed1f96571a1eac715f565eba9c548ae09a0001c0d52c41ae2bd202937a9d0",
12
12
  "agents": "dea14442054ec1de798cba9be1fbdd955f44b0e4672639cbf3db8101d290c1fb",
13
- "moduleDoc:agents/execution.md": "b9ce254892a5dbd44c762b6f8b718d2f2705779d67b893418fc8fcc5677e2179",
14
- "moduleDoc:README.md": "75ad0e22d27c1cc8b18cd0666f1928a510f45403aeb3be8e5decef8d756b4846",
13
+ "moduleDoc:agents/execution.md": "6929247cfd8262a1199b24e0de73a152e1312ac63f36db5a401434394e29669d",
14
+ "moduleDoc:README.md": "7003696370c1cd18b7c46e99faa83bd6e6ae2070633e0ad727c1cd3cbf6cb495",
15
15
  "moduleDoc:EXTRACTION.md": "b735a25a7be75f3a13f3f9e92b0c25834c1394835c16ccd73a076d5720c14243",
16
- "moduleDoc:PROPOSALS.md": "5478b20c7d32a31e0c74500c978c4148cff1939b50c078ec9179556f8e4f810c",
17
- "moduleDoc:FEEDBACK.md": "1da3dcf033d60aaf3e67c9813efcfe58cb69d3c238aa4526c33edaa884357f06",
18
- "moduleDoc:REVIEW.md": "dc7514212b58191a4b9ea77d665340cb6e9c8bb18458098cf7b231c45b87f3d1",
19
- "agentSurface:src/test-support/execution.ts": "1df56aaf1a692d822ed45ecb7e50df7d6806977ff3c03f9eeaf171c64f496865",
20
- "agentSurface:src/test-support/proposals.ts": "e9fa6aadf355aa315a90dd876f06fe65704ef51f343bb9f93f9a7e2c01b2e6ae"
16
+ "moduleDoc:PROPOSALS.md": "aa54dd4312a4e089995b2a60016aef02f7c50fc0767638152050bbd4e6a8a3be",
17
+ "moduleDoc:FEEDBACK.md": "68fe716f5e1e45cca5bcdd50ce6cfcc4dc190c561c4afceadb21077087a71902",
18
+ "moduleDoc:REVIEW.md": "efecc34a6faca55bc8d1332f690167b97562e872d7e0b3b2d83362916ab23ad5",
19
+ "agentSurface:src/test-support/execution.ts": "9de2fb8a6005bf0e7ca94fb2bfdb73b9dcc30ffbf6bd6e9e0caad7d6cf6f5986",
20
+ "agentSurface:src/test-support/proposals.ts": "c4824329672879f93152778a8f91d9f96e5a089fdc8af6331b4341a3bd50b682"
21
21
  },
22
22
  "exports": [
23
23
  ".",
@@ -1599,12 +1599,12 @@
1599
1599
  {
1600
1600
  "path": "agents/execution.md",
1601
1601
  "module": "execution",
1602
- "content": "# Review and execution integration\n\n`/execution` exports `IntakeExecutionService`, handler/host types, policy\nresolution, binding digests and `continueIntakeReview`. `IngestionService` exposes\nthese operations when its optional `execution` configuration is present. Its\nloader is lazy: importing `/server` alone does not import the principal stack.\n`/dto` exposes execution messages using type-only exports. No generated REST,\nCLI or MCP mutation endpoint is enabled; applications authenticate their own\nservice surface and create `IngestionScope` from the session.\n\n## Catalog and authority\n\nApplications supply a trusted `IntakeExecutionOptions.handlers` catalog.\n`OperationHandler` has qualified ID, immutable version, description, object JSON\nschemas for arguments/results, allowlisted `resultModels`, `validate`, `preview`,\na registered `{model, action, version}` operation and explicit effect/idempotency/\nopen-world capability declaration. Preview returns display, exact normalized\narguments and target revision preconditions. `normalizedArgs` must equal the\nsubmitted args exactly; default or normalize before submission, never in preview. There is no arbitrary tool execution\nor expression evaluation. Reviewer display wraps application fields under `preview`\nand symbolic metadata under `dependencies`; plan steps additionally wrap that\nobject under `step` alongside the plan preview. Application display keys cannot\noverwrite framework metadata or be silently overwritten by it. A version change\nrequires a new preview and approval.\n`PlanHandler` references a real registered playbook key and resolved definition\nhash; expansion contains only ordered operation handlers and typed earlier-step\nresult references. Discovery/proposal generation consumes this catalog rather\nthan creating another registry. Semantic model output is never host configuration.\n\nAuthenticated receipt context captures `capturedCeiling.execution` with principal\nID and permission/handler/operation ceilings. The execution host's `authorize`\nresolves current grants and application/tenant/source/(optional request) policy\non the supplied executor, checking the active requester and confidential scope.\nLists intersect, budgets minimize, review requirements OR, and disabled policy\nwins. Policy versions must change when policy semantics change. Normal operation\nrequires human review; machine authorization requires explicit evaluated opt-ins\nat all three authority layers and remains separate provenance from human review.\nThe reference handlers always require review; no evaluation claim is made here.\n\n`mutationBoundary: 'serialized'` is an explicit host guarantee, not an inferred\nproperty of a permissions array: grant revocation must serialize with the supplied\ntransaction, or the owning mutation must revalidate under its own lock. The host\nmust implement that guarantee before enabling handlers. `assertTarget` similarly\nlocks current tenant/confidential ownership and revision against concurrent\nwriters using the supplied executor. The reference host demonstrates a scoped\nrow lock before public `Contents` reads. A current `PrincipalRun` intersects live\npermissions with the captured ceiling and checks the exact registered collection\noperation at every effect. No `runOnce` atomicity is assumed.\n\n## Durable review and effects\n\nPreview appends a proposal bound to exact analysis attempt/evidence, handler,\narguments, policy and target preconditions. Decision requests use unique durable\nrequest IDs; reusing an ID with changed content is rejected. First valid review\nwins under the item lock. Corrections append a new revision; reject/defer remain\nnon-executable. Plan corrections use re-expansion. Human decisions expire and\nlive authority is checked again before each step. Raw foundation proposals cannot\nbypass execution's schema, preview, evidence and approval verification.\n\nDatabase handlers must use `context.db` for every domain effect and must perform\nno external I/O. Domain result and successful execution commit together. A domain savepoint rolls failed effects\nback while preserving the item lock and atomically consuming the retry attempt. Completed\nactions survive job cleanup and evidence retention; expired originals leave a\nminimal result digest tombstone, preventing a second effect. Active result reads\nrecheck host visibility and result target ownership.\n\nExternal handlers reserve a stable action idempotency key before submission.\nTimeout, process death or ambiguous outcome requires reconciliation; an expired\nlease never means permission to resend. `reconcile` must return authoritative\n`succeeded`, `not_applied` or `unknown`. `not_applied` is safe only when the provider\ncan exclude a late acceptance of the original send; uncertainty stays `unknown`.\nThe same key is reused after a conclusive non-application. Cancellation prevents\npending work but cannot retract a send already issued. Applications must arrange\nprovider reconciliation before privacy retention removes the binding; a redacted\nunknown action fails closed and cannot automatically execute again.\n\nEach plan step has a stable action identity. Completed steps are immutable and\nreturn their original result. Symbolic record IDs bind to exact earlier proposal\nrevision, successful result digest/model and target revision; they cannot point\nacross items or form cycles. Failed dependent steps never repeat predecessors.\nThe plan preview and target preconditions are part of its digest and visible in\neach step review; changed preview/preconditions require a fresh review.\nAbort/continue follows the pinned public playbook definition, with fresh authority\nat every step.\n\n`continueIntakeReview` uses the existing jobs continuation context. Applications\ncreate tasks with owner, tenant and exact `{recordId, revision, inputKey}` binding\nand configure `authorizeMcpTask` to revalidate it. A continuation answer only wakes\nthe task; the service reloads the authoritative decision. Waiting persists without\nan occupied worker and survives runner restart.\n\n## Deployment and evidence\n\nApply normal manifest migrations before runtime. Execution imports the public\nagents/users principal stack, which registers profiles including `ProfileAsset`;\nexecution-enabled hosts must migrate their dependency graph (including profile\nasset joins used by asset deletion). `/server` receipt-only imports register none\nof these additional models. Content is not a runtime dependency of ingestion.\n`reference/handlers.ts` is an application/test fixture, excluded from published\nentries and declarations; importing it deliberately adds Content and its asset\njoins, whose migrations the application then owns. No runtime DDL is performed.\n\nReal SQLite and PostgreSQL execution suites exercise the public Content draft and\nattachment APIs, transaction rollback, actor/context denials, simultaneous applies\nand reviewers, retention replay, external process death and actual TaskRunner\nrestart. See the issue 3674 test matrix for the complete release evidence mapping.\n\n## Layered retention and request automation\n\nEvery automation-bearing policy layer, including an optional request layer, must\nmatch the evaluated version. An absent request automation block adds no evaluator\nrestriction; it cannot replace the three required application/tenant/source opt-ins.\n\nAuthorized execution-policy access monotonically narrows the existing intake\nexpiry to `min(existing expires_at, item.created_at + resolved retentionMs)`.\nThe anchor is receipt creation, never review, retry or replay time. A longer later\npolicy never extends a recorded deadline. This hard maximum applies to the whole\nintake item: the existing `sweepRetention` protocol removes retained originals and\nderivatives, preserving replay identities. No second expiry column or hidden clock\nis introduced, and no blob I/O occurs in an execution transaction.\n\nAt an observed expired deadline, execution reads/completions erase proposal,\ndecision, execution, plan, action and feedback payloads under the item lock. Review\nreads return existing identity/hash fields with `state: 'expired'` and empty\n`display`; completed action replay returns its digest-only tombstone. Failed writes, including ordinary validation/CAS/policy rejection and domain\nsavepoint rollback, cannot roll back an already-authorized retention ceiling or\nredaction. Restoring a recorded authorized maximum needs no new content-read grant;\nlater revocation cannot undo it. Initial denied authorization records no intent.\nCompleted replay checks the deadline again after target authorization\ncallbacks before returning cached content. Proposal publication and automatic\nauthorization also recheck after preview/evaluator callbacks, before publishing\nretained proposal or decision payloads. Unknown\nexternal outcomes remain unknown/reconcile-only, never eligible for blind resend.\n"
1602
+ "content": "# Review and execution integration\n\n`/execution` exports `IntakeExecutionService`, handler/host types, policy\nresolution, binding digests and `continueIntakeReview`. `IngestionService` exposes\nthese operations when its optional `execution` configuration is present. Its\nloader is lazy: importing `/server` alone does not import the principal stack.\n`/dto` exposes execution messages using type-only exports. No generated REST,\nCLI or MCP mutation endpoint is enabled; applications authenticate their own\nservice surface and create `IngestionScope` from the session.\n\n## Catalog and authority\n\nApplications supply a trusted `IntakeExecutionOptions.handlers` catalog.\n`OperationHandler` has qualified ID, immutable version, description, object JSON\nschemas for arguments/results, allowlisted `resultModels`, `validate`, `preview`,\na registered `{model, action, version}` operation and explicit effect/idempotency/\nopen-world capability declaration. Preview returns display, exact normalized\narguments and target revision preconditions. `normalizedArgs` must equal the\nsubmitted args exactly; default or normalize before submission, never in preview. There is no arbitrary tool execution\nor expression evaluation. Reviewer display wraps application fields under `preview`\nand symbolic metadata under `dependencies`; plan steps additionally wrap that\nobject under `step` alongside the plan preview. Application display keys cannot\noverwrite framework metadata or be silently overwritten by it. A version change\nrequires a new preview and approval.\n`PlanHandler` references a real registered playbook key and resolved definition\nhash; expansion contains only ordered operation handlers and typed earlier-step\nresult references. Discovery/proposal generation consumes this catalog rather\nthan creating another registry. Semantic model output is never host configuration.\n\nAuthenticated receipt context captures `capturedCeiling.execution` with principal\nID and permission/handler/operation ceilings. The execution host's `authorize`\nresolves current grants and application/tenant/source/(optional request) policy\non the supplied executor, checking the active requester and confidential scope.\nLists intersect, budgets minimize, review requirements OR, and disabled policy\nwins. Policy versions must change when policy semantics change. Normal operation\nrequires human review; machine authorization requires explicit evaluated opt-ins\nat all three authority layers and remains separate provenance from human review.\nThe reference handlers always require review; no evaluation claim is made here.\n\n`mutationBoundary: 'serialized'` is an explicit host guarantee, not an inferred\nproperty of a permissions array: grant revocation must serialize with the supplied\ntransaction, or the owning mutation must revalidate under its own lock. The host\nmust implement that guarantee before enabling handlers. `assertTarget` similarly\nlocks current tenant/confidential ownership and revision against concurrent\nwriters using the supplied executor. The reference host demonstrates a scoped\nrow lock before public `Contents` reads. A current `PrincipalRun` intersects live\npermissions with the captured ceiling and checks the exact registered collection\noperation at every effect. No `runOnce` atomicity is assumed.\n\n## Durable review and effects\n\nPreview appends a proposal bound to exact analysis attempt/evidence, handler,\narguments, policy and target preconditions. Decision requests use unique durable\nrequest IDs; reusing an ID with changed content is rejected. First valid review\nwins under the item lock. Corrections append a new revision; reject/defer remain\nnon-executable. Plan corrections use re-expansion. Human decisions expire and\nlive authority is checked again before each step. Raw foundation proposals cannot\nbypass execution's schema, preview, evidence and approval verification.\n\nDatabase handlers must use `context.db` for every domain effect and must perform\nno external I/O. Domain result and successful execution commit together. A domain savepoint rolls failed effects\nback while preserving the item lock and atomically consuming the retry attempt. Completed\nactions survive job cleanup and evidence retention; expired originals leave a\nminimal result digest tombstone, preventing a second effect. Active result reads\nrecheck host visibility and result target ownership.\n\nExternal handlers reserve a stable action idempotency key before submission.\nTimeout, process death or ambiguous outcome requires reconciliation; an expired\nlease never means permission to resend. `reconcile` must return authoritative\n`succeeded`, `not_applied` or `unknown`. `not_applied` is safe only when the provider\ncan exclude a late acceptance of the original send; uncertainty stays `unknown`.\nThe same key is reused after a conclusive non-application. Cancellation prevents\npending work but cannot retract a send already issued. Applications must arrange\nprovider reconciliation before privacy retention removes the binding; a redacted\nunknown action fails closed and cannot automatically execute again.\n\nEach plan step has a stable action identity. Completed steps are immutable and\nreturn their original result. Symbolic record IDs bind to exact earlier proposal\nrevision, successful result digest/model and target revision; they cannot point\nacross items or form cycles. Failed dependent steps never repeat predecessors.\nThe plan preview and target preconditions are part of its digest and visible in\neach step review; changed preview/preconditions require a fresh review.\nAbort/continue follows the pinned public playbook definition, with fresh authority\nat every step.\n\nHuman corrections are the logical split and the reviewer instruction. Both are\nminted only by their authenticated review API under reviewer authority, written\ninto a new immutable extraction configuration, and reach interpretation only as\na digest-verified copy. Neither grants handlers, candidates, permissions or\nautomation; `authorizeAutomatic` and preview/approval are unchanged for actions\nproduced under an instruction. Recording either supersedes current suggestions\nand queues the existing dispatch stages (a new extraction revision, then\ninterpretation — provider extraction budget is spent per correction because the\npredecessor pin is not reused); hosts wake their worker through\n`onSplit`/`onInstruct` and rely on dispatch repair otherwise. While a stage is\nqueued/running the item reports `processing` regardless of earlier actions; a\nfailed stage (extraction or interpretation) exposes its safe category through\n`getReinterpretationState`, and a retry anchored on the latest completed\ngeneration carries forward any newer split correction.\n\n`continueIntakeReview` uses the existing jobs continuation context. Applications\ncreate tasks with owner, tenant and exact `{recordId, revision, inputKey}` binding\nand configure `authorizeMcpTask` to revalidate it. A continuation answer only wakes\nthe task; the service reloads the authoritative decision. Waiting persists without\nan occupied worker and survives runner restart.\n\n## Deployment and evidence\n\nApply normal manifest migrations before runtime. Execution imports the public\nagents/users principal stack, which registers profiles including `ProfileAsset`;\nexecution-enabled hosts must migrate their dependency graph (including profile\nasset joins used by asset deletion). `/server` receipt-only imports register none\nof these additional models. Content is not a runtime dependency of ingestion.\n`reference/handlers.ts` is an application/test fixture, excluded from published\nentries and declarations; importing it deliberately adds Content and its asset\njoins, whose migrations the application then owns. No runtime DDL is performed.\n\nReal SQLite and PostgreSQL execution suites exercise the public Content draft and\nattachment APIs, transaction rollback, actor/context denials, simultaneous applies\nand reviewers, retention replay, external process death and actual TaskRunner\nrestart. See the issue 3674 test matrix for the complete release evidence mapping.\n\n## Layered retention and request automation\n\nEvery automation-bearing policy layer, including an optional request layer, must\nmatch the evaluated version. An absent request automation block adds no evaluator\nrestriction; it cannot replace the three required application/tenant/source opt-ins.\n\nAuthorized execution-policy access monotonically narrows the existing intake\nexpiry to `min(existing expires_at, item.created_at + resolved retentionMs)`.\nThe anchor is receipt creation, never review, retry or replay time. A longer later\npolicy never extends a recorded deadline. This hard maximum applies to the whole\nintake item: the existing `sweepRetention` protocol removes retained originals and\nderivatives, preserving replay identities. No second expiry column or hidden clock\nis introduced, and no blob I/O occurs in an execution transaction.\n\nAt an observed expired deadline, execution reads/completions erase proposal,\ndecision, execution, plan, action and feedback payloads under the item lock. Review\nreads return existing identity/hash fields with `state: 'expired'` and empty\n`display`; completed action replay returns its digest-only tombstone. Failed writes, including ordinary validation/CAS/policy rejection and domain\nsavepoint rollback, cannot roll back an already-authorized retention ceiling or\nredaction. Restoring a recorded authorized maximum needs no new content-read grant;\nlater revocation cannot undo it. Initial denied authorization records no intent.\nCompleted replay checks the deadline again after target authorization\ncallbacks before returning cached content. Proposal publication and automatic\nauthorization also recheck after preview/evaluator callbacks, before publishing\nretained proposal or decision payloads. Unknown\nexternal outcomes remain unknown/reconcile-only, never eligible for blind resend.\n"
1603
1603
  },
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\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"
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. 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\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",
@@ -1614,17 +1614,17 @@
1614
1614
  {
1615
1615
  "path": "PROPOSALS.md",
1616
1616
  "module": "PROPOSALS",
1617
- "content": "# Application-aware proposal generation\n\n`@happyvertical/smrt-ingestion/proposals` is server-only. Configure\n`IngestionOptions.proposals` and the existing `execution.handlers` catalog.\nHandlers opt into discovery with `discovery.mediaTypes`, `references`, and an\noptional `candidates` callback. There is one catalog: the same operation or\nordered playbook adapter is discovered, validated, previewed, and later applied\nby the execution service. Generation never approves or applies.\n\nCredential-free review hosts may configure `IngestionOptions.proposalRead` with\n`ProposalReadConfiguration`: public generator/decision identities, version,\nprompt version, limits and thresholds only. It reads completed proposals,\npreserves current authority and snapshot validation, and permits human preview;\nit never accepts a generator or provider client. If a process supplies both\nsettings, their public configuration must match exactly. Preparing or generating\nproposals still requires `proposals` with a real generator.\n\n## Discovery and authority\n\n`listHandlers(itemId)` returns a bounded detached catalog. The owning execution\nservice checks the current principal, captured permissions/handler/operation\nceiling, policy, confidential scope, public registered operation facts, and\nordered playbook definition. Every configured provider must occur in the current\npolicy's `providers` allowlist. Missing grants remove handlers before model input.\nAn authorized catalog larger than the configured bound is rejected; the host must\nnarrow it explicitly instead of relying on silent truncation.\n\n`findCandidates({itemId,handlerId,handlerVersion,query,limit})` returns\n`{items,truncated}`. Its trusted handler callback receives `HandlerContext` with\nthe authorized transaction executor. Query by tenant and confidential scope\n**before** selecting, ranking, or building labels. Use a bounded query (often\n`limit + 1`) and report `hasMore`. The service projects candidate identity, model,\nrevision and label, then checks every target with the owning execution callback.\nThere are no hidden counts or pagination tokens. Truncated discovery forces\n`needs_review` and cannot enter generated preview.\n\nCompleted generation reload checks process authority, the current attempt, and\nowner/candidate visibility in the same final transaction. Database-backed grant\ncallbacks must use the supplied executor. The owning retention lifecycle encloses\nthat transaction so a denied read cannot roll back an authorized shorter privacy\nceiling; restoration starts only after the outer transaction has rolled back.\n\nCandidate callbacks perform bounded database reads only: no provider I/O, network,\nor domain mutations. Target locks and existing retention enforcement remain\nowned by execution. Hosts configure their database's statement deadlines; an\narbitrary trusted callback is not a sandbox. Each existing-record argument must\nbe a declared top-level `references` field of kind `candidate` with its qualified\nmodel, or `evidence`. Closed JSON argument schemas are required. The host owns\nthat declaration and its target authorization semantics.\n\n## Durable stages\n\n1. Complete extraction through the existing analysis ledger.\n2. Call `prepareGeneration(itemId, extractionAttemptId)`. It reads the current\n completed/partial immutable extraction and returns configuration with\n `stage: 'interpret'`, pinned source attempt/revision/input/output/evidence\n digests, prompt/configuration/catalog/provider versions, limits and thresholds.\n3. Pass that configuration to `analyze(itemId, configuration, requestKey)` and\n claim its new revision with `claimAnalysis`.\n4. The host's existing job callback routes on the frozen `configuration.stage`:\n extraction invokes `extractAnalysis`; interpretation invokes\n `generateProposals(lease)`.\n5. Reload the current completed generation using `getCompletedAnalysis(itemId)`,\n or pass an explicit attempt ID to additionally reject stale client state.\n Queued/running revisions never return an older completed attempt.\n\nA generation revision does not rewrite terminal extraction. Reprocessing starts\nwith a new extraction revision, followed by interpretation, while the host keeps\nthe same business intention keys. An interrupted still-current generation lease\ncan be retried; terminal generation output remains immutable. `getGenerationInput`\nreads historical extraction only through the current generation lease and its\nfrozen source pin. It verifies same-item scope, immutable hashes, durable evidence,\nand current process permission before returning detached data. The source must be\nthe immediately preceding revision, so an intervening reprocess invalidates a\nprepared generation. Completed generation reads additionally recheck current\ncatalog and target visibility before disclosing persisted candidate labels.\nExpired, cancelled, revoked, retried or superseded workers cannot read or publish late output. The\ncurrent lease and each offered handler/candidate are rechecked immediately before\neach outbound generative/decision boundary and before publication.\n\n## Providers and validation\n\n`createSDKProposalGenerator(client, identity, {maxTokens,timeoutMs})` uses an\nexisting AI SDK `chat` client. It sends a fixed system instruction and separate\nJSON evidence/catalog data, requests JSON, supplies cancellation and bounds, uses\nno tools or continuation, and rejects known truncation or tool calls,\nunexpected models, malformed JSON and oversized output. Configure SDK clients\nwithout automatic retry that would bypass application boundary checks. The\nadapter's public identity is exactly provider/model/version; credentials and raw\nprovider errors are never provenance.\n\nA trusted custom `ProposalGenerator` must likewise honor cancellation, bound its\nresponse, and explicitly declare validated `complete` or `unknown` completion.\nThe SDK currently normalizes missing/unknown finish reasons to `stop`\n([owning SDK follow-up](https://github.com/happyvertical/sdk/issues/1381)), so its\nadapter conservatively declares `unknown`, persisted as\n`generation_completion_unknown`. Otherwise valid suggestions remain available\nfor human preview; automatic eligibility always remains false. The service\ndeadline aborts requests and prevents late publication; it does not claim to terminate computation at a remote\nprovider or isolate native allocations. Extractor process limits remain separate.\n\nModel output can contain only offered handler versions, schema-valid arguments,\nretained evidence locations and offered alternative handler/candidate keys.\nCandidate IDs must occur in the offered set, and evidence IDs in retained source\nresults. Unknown enums, invented IDs, extra authority/tool/approval fields and\nmalformed structures fail closed. Missing required top-level fields are retained\nas `missingFields`, with `needs_review`; values are never invented to satisfy a\nschema. Observed page/time/source granularity is preserved. Logical scan splits\nuse `proposeDocumentSplits`; originals and hashes remain intact.\n\n`createSDKProposalDecisionClient(client)` adapts an existing SDK client’s optional\ncapability without changing its errors or protocol. Optional `decision` uses public core `executeDecision` and the existing SDK\npredicate, choice, score and batch contracts. Supported-effect predicates,\nhandler classification, candidate entity matching, and action ambiguity/risk\nare distinct questions. Their validated probabilities and provenance are stored\nseparately from generative provenance and unknown OCR certainty. The complete\nserialized decision request, including suggestions and questions, must fit the\nminimum configured/current policy byte ceiling before capability probing and\nagain before deciding. A policy change between these boundaries fails closed.\nTies, support probability or selected-handler confidence below\n`minimumDecisionProbability`, and actually incomplete extraction require review.\nUnknown provider\ncompletion metadata is preserved as `source_completion_unknown`; it does not\nblock otherwise schema-valid **human** preview or imply completeness. Automatic\naction eligibility remains false. An unconfigured decision path\nis recorded explicitly. Configured errors produce a durable safe failure outcome;\nthey never silently switch to a generative decision fallback.\n\nOutput records zero, one or multiple suggestions, alternatives, missing fields,\nsource links, proposed splits, versions, allowlisted usage, warnings and explicit\n`unknown`, `ambiguous`, `no_action`, `needs_review` or `provider_error` outcomes.\n`automaticActionEligible` is always false. Provider input/output and the complete\nserialized persisted envelope are bounded by configuration, live policy and the\nreceipt ceiling. Oversized output becomes a bounded review/limit result; when\nno safe envelope fits, the owning service records fixed `failAnalysis` metadata\noutside the provider payload budget without retaining provider output.\n\n## Explicit preview and stable intentions\n\nThe host chooses persisted suggestion indexes and calls:\n\n```ts\nawait service.previewGeneratedProposals({\n itemId,\n attemptId: generationAttemptId,\n selections: [{\n index: 0,\n intentionKey: 'document:meeting-2026-10-09',\n expectedRevision: 0,\n requestId: 'review-request-1',\n }],\n});\n```\n\n`intentionKey` is the host's stable business-intention identity across generation\nrevisions. Never derive a fresh key from model output, suggestion order or a retry.\nSelections must reference current persisted validated `ready_for_review` output.\nEach batch requires distinct suggestion indexes and intention keys; duplicate\nindexes are rejected before any operation or plan preview is written.\nThe service rechecks current catalog/schema/target/source provenance, then calls\nthe existing operation or plan preview. Each result still requires the ordinary\nhuman review and execution APIs. Reprocessing creates no new business identity;\nsuccessful-action replay protection stays with execution.\n\n## Evidence and limits\n\nThe test matrix is `docs/test-matrix/3673-ingestion-proposals.md`. SQLite and real\nPostgreSQL suites exercise authority, revisions, abstention, hostile output,\ncompound proposals, split provenance and reviewed domain effects. The upload\nreference composes authenticated multipart delivery of a generated embedded-text\nPDF through the actual packaged PDF child, durable interpretation, and the existing\nordered ContentDocument draft/retained-asset plan. It checks duplicate delivery,\nunauthenticated and foreign-tenant denial, explicit approval of both steps,\nidempotent execution replay, page provenance, and unchanged original bytes.\nThe compound-email case uses a declared messages-owner snapshot fixture.\n\nLocal HTTP fixtures execute the actual AI SDK transport and cancellation contract.\nGenerative responses and the separate injected PDF split boundaries establish\ndeterministic orchestration, not model accuracy or OCR quality. No held-out routing\nevaluation or automatic approval is claimed. Missing/unknown AI completion remains\nexplicit under [SDK #1381](https://github.com/happyvertical/sdk/issues/1381).\n\nProposal limits are checked at `IngestionService` construction: positive safe\nintegers, at most 20 handlers/candidates/suggestions and 2147483647 ms timeout.\nInterpretation output uses the exported `GenerationAnalysisOutput` wrapper at\n`analysis.result.output.proposals`; the published review host unwraps it.\nAn explicit feedback `retrieve` denial omits optional examples and feedback\nprovenance and continues generation. Item/execution denials, unexpected failures,\nand revocation after examples were selected still fail closed before disclosure.\n"
1617
+ "content": "# Application-aware proposal generation\n\n`@happyvertical/smrt-ingestion/proposals` is server-only. Configure\n`IngestionOptions.proposals` and the existing `execution.handlers` catalog.\nHandlers opt into discovery with `discovery.mediaTypes`, `references`, and an\noptional `candidates` callback. There is one catalog: the same operation or\nordered playbook adapter is discovered, validated, previewed, and later applied\nby the execution service. Generation never approves or applies.\n\nCredential-free review hosts may configure `IngestionOptions.proposalRead` with\n`ProposalReadConfiguration`: public generator/decision identities, version,\nprompt version, limits and thresholds only. It reads completed proposals,\npreserves current authority and snapshot validation, and permits human preview;\nit never accepts a generator or provider client. If a process supplies both\nsettings, their public configuration must match exactly. Preparing or generating\nproposals still requires `proposals` with a real generator.\n\n## Discovery and authority\n\n`listHandlers(itemId)` returns a bounded detached catalog. The owning execution\nservice checks the current principal, captured permissions/handler/operation\nceiling, policy, confidential scope, public registered operation facts, and\nordered playbook definition. Every configured provider must occur in the current\npolicy's `providers` allowlist. Missing grants remove handlers before model input.\nAn authorized catalog larger than the configured bound is rejected; the host must\nnarrow it explicitly instead of relying on silent truncation.\n\n`findCandidates({itemId,handlerId,handlerVersion,query,limit})` returns\n`{items,truncated}`. Its trusted handler callback receives `HandlerContext` with\nthe authorized transaction executor. Query by tenant and confidential scope\n**before** selecting, ranking, or building labels. Use a bounded query (often\n`limit + 1`) and report `hasMore`. The service projects candidate identity, model,\nrevision and label, then checks every target with the owning execution callback.\nThere are no hidden counts or pagination tokens. Truncated discovery forces\n`needs_review`; a person may still preview it explicitly, and automation never\nbecomes eligible.\n\nCompleted generation reload checks process authority, the current attempt, and\nowner/candidate visibility in the same final transaction. Database-backed grant\ncallbacks must use the supplied executor. The owning retention lifecycle encloses\nthat transaction so a denied read cannot roll back an authorized shorter privacy\nceiling; restoration starts only after the outer transaction has rolled back.\n\nCandidate callbacks perform bounded database reads only: no provider I/O, network,\nor domain mutations. Target locks and existing retention enforcement remain\nowned by execution. Hosts configure their database's statement deadlines; an\narbitrary trusted callback is not a sandbox. Each existing-record argument must\nbe a declared top-level `references` field of kind `candidate` with its qualified\nmodel, or `evidence`. Closed JSON argument schemas are required. The host owns\nthat declaration and its target authorization semantics.\n\n## Durable stages\n\n1. Complete extraction through the existing analysis ledger.\n2. Call `prepareGeneration(itemId, extractionAttemptId)`. It reads the current\n completed/partial immutable extraction and returns configuration with\n `stage: 'interpret'`, pinned source attempt/revision/input/output/evidence\n digests, prompt/configuration/catalog/provider versions, limits and thresholds.\n3. Pass that configuration to `analyze(itemId, configuration, requestKey)` and\n claim its new revision with `claimAnalysis`.\n4. The host's existing job callback routes on the frozen `configuration.stage`:\n extraction invokes `extractAnalysis`; interpretation invokes\n `generateProposals(lease)`.\n5. Reload the current completed generation using `getCompletedAnalysis(itemId)`,\n or pass an explicit attempt ID to additionally reject stale client state.\n Queued/running revisions never return an older completed attempt.\n\n`analyze` refuses an interpretation configuration whose pinned source is not\nthe item's latest extraction, checked under the item lock before anything is\nsuperseded (`GenerationSourceStaleError`, nothing appended). A preparation\nsuperseded by a later extraction therefore fails at append time rather than\nafter a revision was created.\n\nA generation revision does not rewrite terminal extraction. Reprocessing starts\nwith a new extraction revision, followed by interpretation, while the host keeps\nthe same business intention keys. An interrupted still-current generation lease\ncan be retried; terminal generation output remains immutable. `getGenerationInput`\nreads historical extraction only through the current generation lease and its\nfrozen source pin. It verifies same-item scope, immutable hashes, durable evidence,\nand current process permission before returning detached data. The source must be\nthe immediately preceding revision, so an intervening reprocess invalidates a\nprepared generation. Completed generation reads additionally recheck current\ncatalog and target visibility before disclosing persisted candidate labels.\nExpired, cancelled, revoked, retried or superseded workers cannot read or publish late output. The\ncurrent lease and each offered handler/candidate are rechecked immediately before\neach outbound generative/decision boundary and before publication.\n\n## Providers and validation\n\n`createSDKProposalGenerator(client, identity, {maxTokens,timeoutMs})` uses an\nexisting AI SDK `chat` client. It sends a fixed system instruction and separate\nJSON evidence/catalog data, requests JSON, supplies cancellation and bounds, uses\nno tools or continuation, and rejects known truncation or tool calls,\nunexpected models, malformed JSON and oversized output. Configure SDK clients\nwithout automatic retry that would bypass application boundary checks. The\nadapter's public identity is exactly provider/model/version; credentials and raw\nprovider errors are never provenance.\n\nA trusted custom `ProposalGenerator` must likewise honor cancellation, bound its\nresponse, and explicitly declare validated `complete` or `unknown` completion.\nThe SDK currently normalizes missing/unknown finish reasons to `stop`\n([owning SDK follow-up](https://github.com/happyvertical/sdk/issues/1381)), so its\nadapter conservatively declares `unknown`, persisted as\n`generation_completion_unknown`. Otherwise valid suggestions remain available\nfor human preview; automatic eligibility always remains false. The service\ndeadline aborts requests and prevents late publication; it does not claim to terminate computation at a remote\nprovider or isolate native allocations. Extractor process limits remain separate.\n\nWhen the pinned extraction carries an authenticated reviewer instruction\n(REVIEW.md), `prepareGeneration` copies it into the interpretation configuration\nand `generateProposals` verifies that copy against the extraction record and its\nown digest before provider disclosure (`integrity` otherwise). The prompt then\ncarries it in its own **trusted reviewer guidance** section after the fixed\nsystem instruction and handler contract: the guidance is named as authenticated\nhuman input, limited to the offered catalog, and the evidence remains untrusted\ndata. `GenerativeProposalInput.reviewerInstruction` exposes `{id, text}` to custom\ngenerators; `provenance.reviewerInstruction` records `{id, digest}`. The\ninstruction never changes `offered`, `catalogDigest`, argument schemas,\npermissions or `automaticActionEligible`; a reply that follows guidance outside\nthe catalog fails validation like any other hostile output. **Budget cost:** the instruction counts once against the input budget\n(`maxInputBytes`, each offered handler's `maxBytes` and the live policy\nceiling), as its JSON-escaped text inside the prompt's guidance section — up\nto 4,000 characters plus escaping and the fixed guidance sentence. The\n`reviewerInstruction` field duplicated for custom generators is not counted\nagain. The decision stage also carries it once: its request state holds the\nevidence-side input with the un-guided base instructions under `evidence`, and\nthe guidance only under a separate `reviewerGuidance` key, never under\n`evidence`; the decision budget measures that serialized request as sent. Size `maxInputBytes` with that headroom. Learned examples are\nstill retrieved for an instructed generation; only a logical split correction\nsuppresses them. Every instruction costs a fresh extraction revision before the\nnew interpretation: the predecessor-pin contract above (source = immediately\npreceding revision, current-revision reads) is deliberately left intact rather\nthan reused for a same-source second interpretation, so OCR/speech hosts budget\none extraction per instruction, as for split corrections.\n\nModel output can contain only offered handler versions, schema-valid arguments,\nretained evidence locations and offered alternative handler/candidate keys.\nCandidate IDs must occur in the offered set, and evidence IDs in retained source\nresults. Unknown enums, invented IDs, extra authority/tool/approval fields and\nmalformed structures fail closed. Missing required top-level fields are retained\nas `missingFields`, with `needs_review`; values are never invented to satisfy a\nschema. Observed page/time/source granularity is preserved. Logical scan splits\nuse `proposeDocumentSplits`; originals and hashes remain intact.\n\nA rejected reply ends the attempt as `provider_error` with the `malformed_output`\nwarning and a bounded, value-free `provenance.failure` `{category, reason}` (#4207).\nThe reason lists schema-owned paths, keywords and schema constants, for example\n`/outcome enum (allowedValues=proposals|unknown|...)` and\n`/suggestions/0 required (missingProperty=evidence)`, or the fixed internal check\nand its path, such as `Unoffered handler: /suggestions/0`. Path segments are\nkept only when they are keys declared under the schema's `properties` or\nindexes into a value that actually is an array in the rejected reply; any other\nsegment, numeric or not (for example a key under an open\n`additionalProperties` argument sub-schema, or a numeric key of an object),\nbecomes `?`, and an extra property's name is never echoed.\nIt never contains document text, argument values, evidence content or\nmodel-chosen keys, is cut at 1000 characters,\nand is dropped before the output-limit fallback. Hosts read it from the\ncompleted/partial analysis at `analysis.result.output.proposals.provenance.failure`.\nThe SDK adapter constrains replies with a `json_schema` response format built\nfrom the validator's schema and the fixed instructions describe the shape; a\ncustom `ProposalGenerator` receives no schema, so its raw reply meets the\nvalidator directly and the same reason is recorded.\n\n`createSDKProposalDecisionClient(client)` adapts an existing SDK client’s optional\ncapability without changing its errors or protocol. Optional `decision` uses public core `executeDecision` and the existing SDK\npredicate, choice, score and batch contracts. Supported-effect predicates,\nhandler classification, candidate entity matching, and action ambiguity/risk\nare distinct questions. Their validated probabilities and provenance are stored\nseparately from generative provenance and unknown OCR certainty. The complete\nserialized decision request, including suggestions and questions, must fit the\nminimum configured/current policy byte ceiling before capability probing and\nagain before deciding. A policy change between these boundaries fails closed.\nActually incomplete extraction always requires review. Unknown provider\ncompletion metadata is preserved as `source_completion_unknown`; it does not\nblock otherwise schema-valid **human** preview or imply completeness. Automatic\naction eligibility remains false. An unconfigured decision path\nis recorded explicitly. Configured errors produce a durable safe failure outcome;\nthey never silently switch to a generative decision fallback.\n\n### Decision readiness gate (#4209)\n\nFor every suggestion `i` the request asks `supported_i` (predicate),\n`route_i` (choice over the offered `handlerId@version` keys plus `none`),\n`risk_i` (ordered score over `clear`, `uncertain`, `ambiguous`) and one\n`entity_i_<field>` choice per candidate-reference argument. The validated\nanswers are always persisted under `provenance.decision.answers`. With the\ndefault `decisionReadiness: 'gate'` they are read as follows, and any failing\ncheck sets the suggestion to `needs_review` (a tie also sets the outcome to\n`ambiguous`):\n\n- `supported.probability >= minimumDecisionProbability` (default `0.8`).\n- `route.choice` equals the suggestion's own `handlerId@handlerVersion`,\n `route.confidence >= minimumDecisionProbability`, and the gap between the two\n largest route probabilities is greater than `tieMargin` (default `0.1`).\n- `risk.score < 1`: only the `clear` level passes; `uncertain` and `ambiguous`\n require review.\n- Each entity match chooses the suggested candidate's key with confidence at or\n above the threshold and a gap above `tieMargin`.\n\nThese values are provider outputs read as if they were calibrated\nprobabilities; nothing in this package calibrates them for this use. The\nteamworks-os measurement in #4209 (self-hosted Laya `typed-decisions`) found no\nseparation between right and wrong suggestions: a correctly read vendor invoice\nscored `supported` 0.37, garbage input 0.45–0.50, a wrongly proposed purchase\norder 0.51, route confidence 0.003–0.15 and `risk` ≥ 0.94, so the gate sent\nevery suggestion to review. Until a labelled sample shows separation, treat the\nanswers as diagnostics and configure:\n\n- `decisionMaxLen`: positive integer forwarded unchanged as the per-request\n `maxLen` of the decision client (the SDK Laya provider's token window; the\n checkpoint default is 1024 and the serialized evidence plus suggestions is\n otherwise truncated). Other clients ignore it.\n- `decisionReadiness: 'record'`: the decision still runs, is bounded and\n authorized exactly as before and its answers, usage, `readiness` and `maxLen`\n are recorded, but the gate never changes `outcome` or `disposition`; readiness\n comes from the validator alone (`missingFields`, outcome, source completeness).\n A client error or timeout is recorded as the `decision_unavailable`\n warning. A reply that comes back and then fails the core `executeDecision`\n identity or answer-shape check is recorded as `decision_malformed` with the\n reply's usage; this is the path for injected `DecisionClient`s. SDK clients,\n including the Laya provider behind `createSDKProposalDecisionClient`,\n validate the reply's shape inside `client.decide` and throw, so a malformed\n SDK reply is recorded as `decision_unavailable` with no usage. Every case\n records the `provenance.decision` identity. The generated suggestions stay\n as they were. The serialized request exceeding the byte budget is a\n configuration failure, not a provider answer: it ends the attempt as `limit`\n in both modes, and in `gate` mode every failure above still ends the attempt\n as `provider_error`. Lease, catalog, policy and provider-allowlist revocation\n fail closed in both modes.\n\nSuccessful output also records `provenance.decision.readiness` (`'gate'` or\n`'record'`) and `maxLen` when set; configuration digests are unchanged, but\npersisted output is therefore not byte-identical to earlier releases.\n\nBoth settings are public configuration: they are part of\n`ProposalReadConfiguration`, the frozen `GenerationStageConfiguration.proposals`\n(present only when set, so existing digests are unchanged) and the completed\nsnapshot authority check. Invalid values fail at proposal service construction.\nNeither mode affects automation: `automaticActionEligible` stays `false`, and\n`authorizeAutomatic` reads only the host's `evaluateAutomatic` callback and\npolicy, never these answers.\n\nOutput records zero, one or multiple suggestions, alternatives, missing fields,\nsource links, proposed splits, versions, allowlisted usage, warnings and explicit\n`unknown`, `ambiguous`, `no_action`, `needs_review` or `provider_error` outcomes.\n`automaticActionEligible` is always false. Provider input/output and the complete\nserialized persisted envelope are bounded by configuration, live policy and the\nreceipt ceiling. Oversized output becomes a bounded review/limit result; when\nno safe envelope fits, the owning service records fixed `failAnalysis` metadata\noutside the provider payload budget without retaining provider output.\n\n## Explicit preview and stable intentions\n\nThe host chooses persisted suggestion indexes and calls:\n\n```ts\nawait service.previewGeneratedProposals({\n itemId,\n attemptId: generationAttemptId,\n selections: [{\n index: 0,\n intentionKey: 'document:meeting-2026-10-09',\n expectedRevision: 0,\n requestId: 'review-request-1',\n }],\n});\n```\n\n`intentionKey` is the host's stable business-intention identity across generation\nrevisions. Never derive a fresh key from model output, suggestion order or a retry.\nSelections must reference current persisted validated `ready_for_review` or\n`needs_review` output (#4208); a `provider_error` attempt, an unoffered handler or\na non-interpretation stage is rejected. A `needs_review` suggestion whose\n`missingFields` are still absent fails the handler schema inside the ordinary\npreview (`Invalid handler payload`) and publishes nothing; the reviewer supplies\nthe missing values through the explicit human path (`previewProposal` /\n`previewPlan`, as the published review host does for `preview({args})`), and the\nhandler's `validate` and `preview` run again on the completed arguments.\nEach batch requires distinct suggestion indexes and intention keys; duplicate\nindexes are rejected before any operation or plan preview is written.\nThe service rechecks current catalog/schema/target/source provenance, then calls\nthe existing operation or plan preview. Each result still requires the ordinary\nhuman review and execution APIs. Reprocessing creates no new business identity;\nsuccessful-action replay protection stays with execution.\n\n## Evidence and limits\n\nThe test matrix is `docs/test-matrix/3673-ingestion-proposals.md`. SQLite and real\nPostgreSQL suites exercise authority, revisions, abstention, hostile output,\ncompound proposals, split provenance and reviewed domain effects. The upload\nreference composes authenticated multipart delivery of a generated embedded-text\nPDF through the actual packaged PDF child, durable interpretation, and the existing\nordered ContentDocument draft/retained-asset plan. It checks duplicate delivery,\nunauthenticated and foreign-tenant denial, explicit approval of both steps,\nidempotent execution replay, page provenance, and unchanged original bytes.\nThe compound-email case uses a declared messages-owner snapshot fixture.\n\nLocal HTTP fixtures execute the actual AI SDK transport and cancellation contract.\nGenerative responses and the separate injected PDF split boundaries establish\ndeterministic orchestration, not model accuracy or OCR quality. No held-out routing\nevaluation or automatic approval is claimed. Missing/unknown AI completion remains\nexplicit under [SDK #1381](https://github.com/happyvertical/sdk/issues/1381).\n\nProposal limits are checked at `IngestionService` construction: positive safe\nintegers, at most 20 handlers/candidates/suggestions and 2147483647 ms timeout.\nInterpretation output uses the exported `GenerationAnalysisOutput` wrapper at\n`analysis.result.output.proposals`; the published review host unwraps it.\nAn explicit feedback `retrieve` denial omits optional examples and feedback\nprovenance and continues generation. Item/execution denials, unexpected failures,\nand revocation after examples were selected still fail closed before disclosure.\n"
1618
1618
  },
1619
1619
  {
1620
1620
  "path": "FEEDBACK.md",
1621
1621
  "module": "FEEDBACK",
1622
- "content": "# Explicit ingestion feedback\n\nConfigure `IngestionOptions.feedback` to opt in. The existing `intake_feedback`\nledger owns these records; no new schema, embedding service or provider is added.\n`FeedbackConfiguration` sets a version, maximum examples, scan count, serialized\nbytes and lexical similarity threshold, plus a live scoped authorization callback.\nHard ceilings are 20 examples, 1,000 scanned records and 1 MB. Choose substantially\nsmaller application limits. The proposal's complete serialized request, including\nexamples, must also fit the current minimum application/source/provider budget.\n\n`recordFeedback` accepts the exact displayed action revision, review version and\nbinding hash, an explicit correct/incorrect judgment, optional reason text and a\nrequest ID. The server resolves reviewer, tenant/confidential scope, retained\nsource evidence, extraction/generation versions and current handler arguments.\nA replay returns the same immutable receipt; changed payloads conflict.\n`supersedesId` appends a replacement for the specified prior interpretation\njudgment. A second successor conflicts. Revised actions make old bindings\nineligible. Approval and successful execution never imply that extracted fields\nare correct.\n\nDecision and downstream execution facts are captured inside their owning\ntransaction, keyed by immutable event ID and signal, when the configured capture\nauthority allows the optional memory projection and it fits the configured byte\ncap. Denial or an oversized automatic projection leaves the underlying\nauthoritative decision/outcome intact, including failure and reconciliation.\nExplicit `recordFeedback` and `observeFeedback` requests still reject invalid or\noversized writes; actual database failures still propagate and roll back. Failed/rolled-back domain\nwork and unknown external outcomes remain distinct. `observeFeedback` can replay\nan authorized existing event; it cannot supply a correctness label or fabricate\nan execution result. An initial denial does not mutate retention. Once authorized,\na narrowed privacy ceiling survives business rollback and later revocation.\n\n## Retrieval and provider boundaries\n\n`retrieveFeedback({ itemId, query })` derives the current offered catalog,\ncandidates and model/prompt/configuration versions on the server. Callers cannot\nprovide candidate authority. The deterministic matcher uses lowercase Unicode\nword-set Jaccard similarity (minimum 0.2), then immutable record ID for ties.\nScanning, recursive provenance checks and returned examples are bounded; a limited scan does not promise an\nexhaustive history search. Both positive and negative explicit examples are kept.\n\nEvery selected source must still be readable in the same tenant and confidential\nscope, with its current action/analysis binding, target grants, handler version,\nmodel/prompt/configuration compatibility and retention. Each source's current\nprovider policy must allow the generation recipient and any configured decision\nrecipient; the source owner rechecks that policy on both initial and final live\ncontexts. Receiving checks cover every offered handler, before and after source\nchecks, using each live grant, actual provider recipients and the narrowest byte\nceiling. Both retrieval and reference revalidation budget the full reconstructed\nexamples. A receiving item's provider grant cannot substitute for a source grant.\nSuperseded, revoked, deleted or incompatible sources are excluded. Current explicit human corrections\ntake precedence: a generation carrying one does not retrieve learned examples.\n\nFull examples exist only in the authorized provider request. The SDK proposal\nadapter includes that typed selection in its serialized user message when\npresent; disabled feedback leaves the original request shape unchanged. Saved generation\nprovenance contains the receiving query, selection configuration digest and\nimmutable feedback IDs/digests, not copied source example content. Selected\nreferences are rechecked before generation, capability probing, decision calls,\npublication and completed-result reads. A newly added example does not invalidate\nan older selection; a revoked or superseded selected example does. Feedback\nlineage is bounded and fails closed. Saved-action reads and pending decisions,\nautomatic authorization, preview publication/replay and execution also revalidate\nselected lineage under the\nowning transaction. Revoking, deleting or superseding a selected example blocks\npending actions; regenerate their proposals and obtain fresh approval to recover.\nA preview replay checks the exact persisted proposal it returns, including its\nanalysis attempt; a newer action revision cannot supply replacement lineage.\nPlan replay and final sealed plan-step publication use the same gate.\nFor already successful actions, current-authorized durable outcomes remain\nreadable/replayable, but invalid learned lineage suppresses saved proposal\ndisplay, arguments and plan payload. An empty selection carries no learned source\nand does not turn optional feedback-capture denial into action denial. Examples\nnever expand the offered catalog, candidate IDs, operation permissions or\nautomation eligibility.\n\nAll durable feedback belongs to the source item's existing retention/redaction\nlifecycle. There is no separate memory store or delayed indexing writer to clean\nup. Hosts using other derived stores still implement the existing `purgeDerived`\ncontract. Core `LearningMemory` is intentionally not used here: its success/failure\nreinforcement and default ancestor-scope fallback do not represent separate\ncorrectness/decision/outcome signals and strict confidential-scope eligibility.\n\n## Routing suggestions and adoption\n\n`suggestRoutingRule` requires repeated current explicit positive judgments\nfrom distinct source items with matching handler/version and arguments, and returns supporting immutable IDs,\nshared query terms and the policy owner's preview/version. Suggestions cannot\nexecute or modify policy. `getRoutingRule` rechecks the displayed binding and all\nsupporting examples. Arguments are routing preferences, never executable grants.\n\nBy default matching compares **all** action arguments, preserving existing host\nbehavior. Create handlers commonly have per-document content and idempotency\nkeys, which must remain distinct. Such hosts can explicitly declare a routing-only\nsubset in trusted server configuration before capturing judgments:\n\n```ts\nfeedback: {\n // ...existing limits, authorize and policy owner\n routing: [{\n handlerId: 'host:create-document',\n handlerVersion: '1',\n version: 'routing-fields-1',\n fields: ['destinationId', 'category'],\n }],\n}\n```\n\nEach declaration must name a configured handler/version and a nonempty, unique\nallowlist of its top-level `argsSchema.properties`; duplicate declarations,\nunknown properties and prototype keys reject. There are no dotted paths,\ncaller-supplied selectors or implicit exclusions (including request IDs).\nOptional absent fields stay absent and differ from explicit `null`; at least one\nselected value must be present. The host must select preferences only, never\ngrants, provider permissions or automation controls.\n\nCapture retains the full immutable action arguments and binding plus the\nversioned subset. For opted-in handlers `RoutingRule.args` contains only selected\nvalues and `RoutingRule.projection` carries the canonical sorted fields and\nversion. Independent support must match that entire contract and those values;\nfull original source arguments, provenance and current access are still checked.\nExisting unprojected feedback is not retroactively reinterpreted: record fresh\nexplicit judgments after enabling or changing a declaration. Changing a version,\nfield list or selected value invalidates old suggestions on reads, adoption and\nreplay, including changes inside policy-owner callbacks. Full argument feedback\nretrieval remains unchanged; hosts may present matching examples from\n`retrieveFeedback`, but suggestion creation revalidates every supplied ID.\n\n`adoptRoutingRule` requires the exact suggestion digest and expected policy\nversion plus current reviewer and adoption authority. The configured policy owner\nmust perform its version compare-and-swap and audit on the supplied database\nexecutor. The ingestion transaction records the exact returned version/audit ID.\nReplays do not adopt twice. Revocation or permission/policy widening rolls back\nboth changes. The host callback may store routing preferences only: it must not\nchange grants, provider permissions or automation controls. Adoption always\nreturns `automaticActionEligible: false`; subsequent proposals retain normal\ncurrent policy and explicit review requirements.\n\n## Review UI and evidence\n\nThe optional `IntakeReviewHost.feedback` callback carries the exact displayed\nrevision and binding. Correct/incorrect controls are explicit and independent of\nApprove/Apply. Unsaved argument edits disable correctness controls until the\ndisplayed binding is current again. Hosts must authenticate that callback and connect it to\n`recordFeedback`; client-supplied reviewer/scope fields are not accepted. The\nmaintained browser fixture demonstrates this transport with CSRF protection and\nrejects substituted revisions.\n\nSee [the behavior matrix](../../docs/test-matrix/3676-ingestion-feedback.md) for\nvalidation and the frozen learning protocol, and the\n[routing projection matrix](../../docs/test-matrix/3732-ingestion-routing-projection.md)\nfor the optional subset contract. The small authored deterministic\ncohort demonstrates contract-level influence, not provider quality or measured\nproduction improvement. Real-provider evaluation belongs to #3677. There are no\nsampled automatic outcomes in this feature; reporting that stratum as unavailable\navoids inventing correctness labels from successful jobs.\n\nWhen interpretation requests optional examples, an explicit denial of feedback\nretrieval degrades to generation without examples or feedback provenance. Direct\n`retrieveFeedback` calls still reject denied reads. Other authorization errors\nare not swallowed, and already selected examples must pass the normal live\nprovider-boundary checks; revocation does not silently preserve selected data.\n"
1622
+ "content": "# Explicit ingestion feedback\n\nConfigure `IngestionOptions.feedback` to opt in. The existing `intake_feedback`\nledger owns these records; no new schema, embedding service or provider is added.\n`FeedbackConfiguration` sets a version, maximum examples, scan count, serialized\nbytes and lexical similarity threshold, plus a live scoped authorization callback.\nHard ceilings are 20 examples, 1,000 scanned records and 1 MB. Choose substantially\nsmaller application limits. The proposal's complete serialized request, including\nexamples, must also fit the current minimum application/source/provider budget.\n\n`recordFeedback` accepts the exact displayed action revision, review version and\nbinding hash, an explicit correct/incorrect judgment, optional reason text and a\nrequest ID. The server resolves reviewer, tenant/confidential scope, retained\nsource evidence, extraction/generation versions and current handler arguments.\nA replay returns the same immutable receipt; changed payloads conflict.\n`supersedesId` appends a replacement for the specified prior interpretation\njudgment. A second successor conflicts. Revised actions make old bindings\nineligible. Approval and successful execution never imply that extracted fields\nare correct.\n\nDecision and downstream execution facts are captured inside their owning\ntransaction, keyed by immutable event ID and signal, when the configured capture\nauthority allows the optional memory projection and it fits the configured byte\ncap. Denial or an oversized automatic projection leaves the underlying\nauthoritative decision/outcome intact, including failure and reconciliation.\nExplicit `recordFeedback` and `observeFeedback` requests still reject invalid or\noversized writes; actual database failures still propagate and roll back. Failed/rolled-back domain\nwork and unknown external outcomes remain distinct. `observeFeedback` can replay\nan authorized existing event; it cannot supply a correctness label or fabricate\nan execution result. An initial denial does not mutate retention. Once authorized,\na narrowed privacy ceiling survives business rollback and later revocation.\n\n## Retrieval and provider boundaries\n\n`retrieveFeedback({ itemId, query })` derives the current offered catalog,\ncandidates and model/prompt/configuration versions on the server. Callers cannot\nprovide candidate authority. The deterministic matcher uses lowercase Unicode\nword-set Jaccard similarity (minimum 0.2), then immutable record ID for ties.\nScanning, recursive provenance checks and returned examples are bounded; a limited scan does not promise an\nexhaustive history search. Both positive and negative explicit examples are kept.\n\nEvery selected source must still be readable in the same tenant and confidential\nscope, with its current action/analysis binding, target grants, handler version,\nmodel/prompt/configuration compatibility and retention. Each source's current\nprovider policy must allow the generation recipient and any configured decision\nrecipient; the source owner rechecks that policy on both initial and final live\ncontexts. Receiving checks cover every offered handler, before and after source\nchecks, using each live grant, actual provider recipients and the narrowest byte\nceiling. Both retrieval and reference revalidation budget the full reconstructed\nexamples. A receiving item's provider grant cannot substitute for a source grant.\nSuperseded, revoked, deleted or incompatible sources are excluded. Current explicit human corrections\ntake precedence: a generation carrying one does not retrieve learned examples.\n\nA judgment recorded on an action whose generation ran under a reviewer\ninstruction (REVIEW.md) stores only a reference, `instruction: {id, digest}`,\nafter verifying the generation's provenance against the authenticated\nextraction record. **Write side:** the reference is ~100 bytes, so instruction\ntext never counts against the judgment's write budget (`feedback.maxBytes` /\n`policy.maxBytes`) and cannot make `recordFeedback` fail with `Feedback\nlimit`. `retrieve` re-verifies the source record and joins the text, returning\n`{id, digest, text}`; a stored reference that no longer matches the source's\nrecord excludes the example. Because one long instruction joined at read time\ncould otherwise push every selection that ranks it over budget and deny\ngeneration scope-wide, `recordFeedback` also builds that single joined example\nexactly as `retrieve` would and refuses with `Feedback instruction exceeds\nretrieval budget` (nothing is written) when it alone exceeds\n`min(feedback.maxBytes, handler policy.maxBytes)`. An example that fits exactly\nrecords and retrieves. **This guard covers only the single-example case** on the\njudged handler's own budget: retrieval still sums up to `maxExamples` examples\nand takes the minimum across *every* offered handler of the receiving item, so a\nmulti-example selection, or a receiving item with a narrower handler budget, can\nstill exceed the budget and fails closed as below. It is returned as part of the example so comparable later items see the guidance\nthat produced the judged suggestion. It is example content only: it does not\ntransfer to the receiving generation's configuration or prompt section, and it\nexpands no catalog, candidate, permission or automation eligibility. **Read-side budget\nimpact:** joined instruction text (up to 4,000 characters, more bytes for\nnon-ASCII) counts toward the exact serialized selection budget — the narrowest\nof `feedback.maxBytes` and each offered handler's `policy.maxBytes` — like any\nother example field. The existing fail-closed rule is unchanged: an over-budget\nselection rejects with `Feedback limit` and generation records\n`provider_error` without provider I/O; examples are not silently trimmed. Hosts\nthat enable reviewer instructions together with feedback should size\n`feedback.maxBytes` (and the policy ceilings) for at least\n`maxExamples × (4,000 characters of instruction + the example's own fields)`,\nor lower `maxExamples`.\n\nFull examples exist only in the authorized provider request. The SDK proposal\nadapter includes that typed selection in its serialized user message when\npresent; disabled feedback leaves the original request shape unchanged. Saved generation\nprovenance contains the receiving query, selection configuration digest and\nimmutable feedback IDs/digests, not copied source example content. Selected\nreferences are rechecked before generation, capability probing, decision calls,\npublication and completed-result reads. A newly added example does not invalidate\nan older selection; a revoked or superseded selected example does. Feedback\nlineage is bounded and fails closed. Saved-action reads and pending decisions,\nautomatic authorization, preview publication/replay and execution also revalidate\nselected lineage under the\nowning transaction. Revoking, deleting or superseding a selected example blocks\npending actions; regenerate their proposals and obtain fresh approval to recover.\nA preview replay checks the exact persisted proposal it returns, including its\nanalysis attempt; a newer action revision cannot supply replacement lineage.\nPlan replay and final sealed plan-step publication use the same gate.\nFor already successful actions, current-authorized durable outcomes remain\nreadable/replayable, but invalid learned lineage suppresses saved proposal\ndisplay, arguments and plan payload. An empty selection carries no learned source\nand does not turn optional feedback-capture denial into action denial. Examples\nnever expand the offered catalog, candidate IDs, operation permissions or\nautomation eligibility.\n\nAll durable feedback belongs to the source item's existing retention/redaction\nlifecycle. There is no separate memory store or delayed indexing writer to clean\nup. Hosts using other derived stores still implement the existing `purgeDerived`\ncontract. Core `LearningMemory` is intentionally not used here: its success/failure\nreinforcement and default ancestor-scope fallback do not represent separate\ncorrectness/decision/outcome signals and strict confidential-scope eligibility.\n\n## Routing suggestions and adoption\n\n`suggestRoutingRule` requires repeated current explicit positive judgments\nfrom distinct source items with matching handler/version and arguments, and returns supporting immutable IDs,\nshared query terms and the policy owner's preview/version. Suggestions cannot\nexecute or modify policy. `getRoutingRule` rechecks the displayed binding and all\nsupporting examples. Arguments are routing preferences, never executable grants.\n\nBy default matching compares **all** action arguments, preserving existing host\nbehavior. Create handlers commonly have per-document content and idempotency\nkeys, which must remain distinct. Such hosts can explicitly declare a routing-only\nsubset in trusted server configuration before capturing judgments:\n\n```ts\nfeedback: {\n // ...existing limits, authorize and policy owner\n routing: [{\n handlerId: 'host:create-document',\n handlerVersion: '1',\n version: 'routing-fields-1',\n fields: ['destinationId', 'category'],\n }],\n}\n```\n\nEach declaration must name a configured handler/version and a nonempty, unique\nallowlist of its top-level `argsSchema.properties`; duplicate declarations,\nunknown properties and prototype keys reject. There are no dotted paths,\ncaller-supplied selectors or implicit exclusions (including request IDs).\nOptional absent fields stay absent and differ from explicit `null`; at least one\nselected value must be present. The host must select preferences only, never\ngrants, provider permissions or automation controls.\n\nCapture retains the full immutable action arguments and binding plus the\nversioned subset. For opted-in handlers `RoutingRule.args` contains only selected\nvalues and `RoutingRule.projection` carries the canonical sorted fields and\nversion. Independent support must match that entire contract and those values;\nfull original source arguments, provenance and current access are still checked.\nExisting unprojected feedback is not retroactively reinterpreted: record fresh\nexplicit judgments after enabling or changing a declaration. Changing a version,\nfield list or selected value invalidates old suggestions on reads, adoption and\nreplay, including changes inside policy-owner callbacks. Full argument feedback\nretrieval remains unchanged; hosts may present matching examples from\n`retrieveFeedback`, but suggestion creation revalidates every supplied ID.\n\n`adoptRoutingRule` requires the exact suggestion digest and expected policy\nversion plus current reviewer and adoption authority. The configured policy owner\nmust perform its version compare-and-swap and audit on the supplied database\nexecutor. The ingestion transaction records the exact returned version/audit ID.\nReplays do not adopt twice. Revocation or permission/policy widening rolls back\nboth changes. The host callback may store routing preferences only: it must not\nchange grants, provider permissions or automation controls. Adoption always\nreturns `automaticActionEligible: false`; subsequent proposals retain normal\ncurrent policy and explicit review requirements.\n\n## Review UI and evidence\n\nThe optional `IntakeReviewHost.feedback` callback carries the exact displayed\nrevision and binding. Correct/incorrect controls are explicit and independent of\nApprove/Apply. Unsaved argument edits disable correctness controls until the\ndisplayed binding is current again. Hosts must authenticate that callback and connect it to\n`recordFeedback`; client-supplied reviewer/scope fields are not accepted. The\nmaintained browser fixture demonstrates this transport with CSRF protection and\nrejects substituted revisions.\n\nSee [the behavior matrix](../../docs/test-matrix/3676-ingestion-feedback.md) for\nvalidation and the frozen learning protocol, and the\n[routing projection matrix](../../docs/test-matrix/3732-ingestion-routing-projection.md)\nfor the optional subset contract. The small authored deterministic\ncohort demonstrates contract-level influence, not provider quality or measured\nproduction improvement. Real-provider evaluation belongs to #3677. There are no\nsampled automatic outcomes in this feature; reporting that stratum as unavailable\navoids inventing correctness labels from successful jobs.\n\nWhen interpretation requests optional examples, an explicit denial of feedback\nretrieval degrades to generation without examples or feedback provenance. Direct\n`retrieveFeedback` calls still reject denied reads. Other authorization errors\nare not swallowed, and already selected examples must pass the normal live\nprovider-boundary checks; revocation does not silently preserve selected data.\n"
1623
1623
  },
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\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"
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\nA `needs_review` suggestion is completable, not only rejectable (#4208). The\ncomponent shows its `missingFields`, an editable `Suggested arguments (JSON)`\ndraft prefilled from the generated arguments, and the candidate picker for\ndeclared candidate fields; Prepare review stays disabled while required fields\nare still missing from an unchanged draft. An unchanged draft previews the\ngenerated snapshot as before; an edited draft is sent as `preview({..., args})`,\nwhich the published host publishes through the ordinary `previewProposal` /\n`previewPlan` with the same `ui:<index>` intention, never by rewriting the\ngeneration. The handler validates the human arguments again before a\n`waiting_review` action exists, approval remains a separate explicit decision,\n`ActionReview` lists required arguments missing from the current draft while\nediting, and no authority or automation eligibility is added: a failed attempt,\nan unoffered handler or a foreign stage is still refused by the host.\n`preview` throws on every failure, as before. When the handler's argument\nschema or `validate` refused the arguments being previewed (operation preview,\nwhich also carries corrections, or plan preview), the server throws\n`HandlerArgumentsInvalidError` (`name` set, messages unchanged). Apply-time and\nexternal-send revalidation and result-schema checks keep a plain `Error`. The component shows an inline message and\nkeeps the view and draft when `preview` either rejects with an error whose\n`name` is `HandlerArgumentsInvalidError` or resolves `{ rejected: 'invalid' }`\n(`PreviewRejection`). A transport that crosses a process boundary preserves\nthe inline UX by keeping that name (for example re-throwing an `Error` with\n`name = 'HandlerArgumentsInvalidError'` in the browser callback) or by\nreturning `{ rejected: 'invalid' }` for that case only, as the maintained\nfixture's `previewForReview` does. Without either, the refusal behaves like\nany other failure: the view is cleared exactly as before this change, and the\nreviewer's in-memory draft returns after an authorized refresh. Every other\nthrown preview or candidate-search failure (denied, revoked, unavailable or\nunknown) clears the view.\nUnsent drafts live only in component memory, keyed by attempt and suggestion\nindex. They survive Refresh, a failed callback, an action on another card and a\ntransient `unavailable`/`stale` reload; a `revoked` or `expired` result\ndiscards them, a successful preview clears that card's draft, and an available\nreload drops drafts for attempts or cards no longer offered.\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## Reviewer instructions\n\n`recordReviewerInstruction({itemId, attemptId, expectedRevision, text,\nrequestId})` records a plain-text instruction for one item (\"This is a customer\nPO from Badger; file it, do not bill it\") as the second kind of **human\ncorrection**. It is authorized exactly like `reviseLogicalSplit`: the current\ngeneration must pass the same-executor catalog/candidate gate and the reviewer\ngrant plus policy reviewer list, and authority is checked before mutation and\nagain before publication. Process access, `analyze`, and a client payload cannot\nmint one: `analyze` rejects a `reviewerInstruction` key on an extraction\nconfiguration, and an interpretation copy that differs from the pinned\nextraction's record fails with `Generation evidence changed` before provider\ndisclosure. Text is required, trimmed-nonempty and at most 4,000 characters.\n\nThe record is immutable: `{id, text, actorId, recordedAt, supersedesId, digest}`\nplus the request binding, written into a new extraction configuration beside\nany existing split correction. Recording supersedes current suggestions and\napprovals, queues the normal fenced re-extraction and interpretation through the\nexisting dispatch path, and returns immediately; the reviewer keeps working\nwhile the item reports `processing`. `prepareGeneration` copies the instruction\ninto the interpretation configuration, the generator receives it only after its\ndigest matches the authoritative extraction record, and saved generation\nprovenance records `reviewerInstruction: {id, digest}`. A later instruction\nrecords `supersedesId` and replaces the earlier one for future interpretations;\n`listReviewerInstructions(itemId)` returns the full history, newest first, and\nhistory is never deleted. The instruction steers the model only: it cannot add\nhandlers, candidates, schemas, permissions or automation eligibility, replies\nare validated as usual, and suggestions still require explicit preview and\napproval. A replay with the same request ID returns the same revision; a changed\ntext under the same request ID conflicts.\n\n**Cost.** Each recorded instruction appends a new extraction revision, so the\nitem is extracted again before it is interpreted again: cheap for embedded-text\nPDFs and plain text, but OCR/speech hosts spend provider budget per instruction,\nexactly as they do per split correction. The pinned extraction is not reused\nbecause the interpretation contract requires its source to be the immediately\npreceding revision and `getCompletedAnalysis(itemId, attemptId)` serves only\nthe current revision (PROPOSALS.md, \"Durable stages\"); reusing it would mean\nweakening that predecessor pin, which protects every generation from an\nintervening reprocess, so this feature follows the split path instead.\n\n**State and failure.** An in-flight stage (queued/running) reports the item as\n`processing` even when earlier, now-stale actions exist. If the re-extraction or\ninterpretation fails, `getReinterpretationState(itemId)` returns the latest\nattempt's safe failure category and the latest completed generation attempt;\nthe host projects them as `instruction.failure` and `instruction.binding`, and\n`recordReviewerInstruction` accepts that historical attempt as its anchor when\n`expectedRevision` is the item's current (failed) revision and no later\n**interpretation** completed and nothing is queued or running, so the reviewer\ncan retry or change the instruction after either stage fails. A completed\nextraction whose own interpretation failed belongs to the failed attempt, not a\nnewer result; a completed extraction whose interpretation has **not been\nappended yet** is in flight (the host is between stages) and is refused with\n`ReviewerInstructionStaleError` (`Stale reviewer instruction`), so a second\nreviewer cannot pre-empt it. That gap expires: once the extraction attempt's\ncompletion plus the item's analysis lease duration (`limits.leaseMs`, the lease\n`claimAnalysis` grants) has passed with no interpretation appended,\n`getReinterpretationState` reports `failure: 'not_started'` with the retry\nbinding and the guard allows the retry. `not_started` means **the\ninterpretation did not start within the lease window**: the host crashed,\nthrew between `prepareGeneration` and `analyze`, or its queue is merely slow.\nPre-empting a slow but healthy host is safe because `analyze` refuses an\ninterpretation whose pinned extraction is no longer the item's latest, under\nthe item lock and before anything is superseded\n(`GenerationSourceStaleError`, nothing appended): the slow host's late append\ncannot clobber the retry. Missing or malformed timing (no attempt, no\n`completedAt`, non-finite `leaseMs`) counts as already expired rather than\nholding retries forever. The \"still being re-read\" note shown after a stale\nresult is cleared by the next reload that shows the item settled; the draft\nthen carries a persistent \"This item changed while you were writing; your\ninstruction was not recorded.\" note (any stale cause: another reinterpretation,\na split, or a host reprocess) until the reviewer edits or resubmits it. The guard and\n`getReinterpretationState` share one predicate, and the latter's\n`reinterpreting` flag (projected as `instruction.reinterpreting`) is true in\nthe gap too, so the component keeps polling and keeps Re-read disabled there.\nThe published host maps two typed errors to `instruct` results:\n`ReviewerInstructionStaleError` → `{ stale: true }` (refresh-and-retry: reload\nwith a note, draft kept) and `ReviewerInstructionInvalidError` → `{ invalid:\ntrue }` (empty, whitespace-only or over 4,000 characters: shown inline, draft\nkept). Neither is a denial. Custom hosts should map them the same way; a host\nthat lets either propagate as a thrown error makes the component treat it like\nany other failed mutation, i.e. a whole-view deny with the \"view unavailable\"\nmessage, as before these results existed. The maintained fixture's\n`/api/instruct` now answers an invalid text with HTTP 200 `{ invalid: true }`\ninstead of 409. A retry from an older anchor builds on the **whole configuration**\nof the newest extraction after it (replacing only the instruction), because that\nis the item's current configuration: corrections chain forward through it and a\nhost reprocess replaces it. So a split followed by a failed stage is never\nsilently dropped, and a failed reprocess's changed configuration is neither\nreverted nor overlaid with a split it dropped. **Concurrency.** `recordReviewerInstruction` and `reviseLogicalSplit` each\nrun their whole decision in one transaction under the item row lock: the\nanchor snapshot read takes the lock first, the execution authorization of\nthat snapshot takes it again (`UPDATE intake_items` row lock), the guard step\nre-asserts it explicitly, and\nthe request-key replay check, the revision and later-work guards, the\ncarried-forward split, the history read that sets `supersedesId`, and the\nappend all run under it. On PostgreSQL a second reviewer at the same revision\nblocks on that lock and is then refused (`ReviewerInstructionStaleError` /\n`Stale logical split`), so the ledger cannot fork; an instruction racing a\nsplit (in either order) is refused rather than built from the pre-split\nconfiguration; and a double-click whose duplicate commits after the unlocked\nreplay check is answered as a replay of the committed request, not a conflict\nor a denial.\n\n**Host reprocess.** A host reprocess (a plain `analyze()` extraction the host\nconfigures itself) cannot carry human corrections — `analyze` rejects\n`humanCorrection`/`reviewerInstruction` on extraction configurations — so it\ndrops the split and the instruction alike, and neither is applied to its\ninterpretations. The instruction is then reported as `dropped`, not pending:\n`getReinterpretationState().instructionInEffect` names the instruction the\nnewest extraction carries (verified by digest) and is null after such a\nreprocess. History is unchanged; the reviewer can re-record the instruction,\nwhich supersedes the dropped one.\n\n`ItemReviewView.instruction` exposes `current` (the latest recorded instruction\nstill in effect, i.e. carried by the newest extraction), optional `dropped`\n(the latest recorded instruction a reprocess dropped), `applied` (the\ninstruction the displayed **generation** ran under; a completed extraction alone\napplies nothing), `stale` (the displayed suggestions predate the current\ninstruction), optional `failure` and optional `binding`. Feedback examples that carry instruction\ntext count it toward the feedback byte budget (FEEDBACK.md). The optional host callback\n`IntakeReviewHost.instruct` is implemented by the published adapter when the\nservice supports it, with an optional `onInstruct(itemId, revision)` wakeup\nmirroring `onSplit`. The review component renders \"Instructions for the\nassistant\" with \"Re-read with these instructions\", shows \"Reinterpreting…\" while\nthe item's stage is queued/running, repeats the existing authenticated `load`\non `pollMs` (default 2 s, backing off to 5×) and patches the view in place —\ndrafts, split edits, focus and the evidence viewer survive and no loading\nplaceholder is shown — stops when the stage is no longer in flight, and then\nshows the instruction that produced the current suggestions, marking a\nsuperseded one as stale. An explicit host denial on a poll tick fails closed at\nonce; a thrown tick is treated as transient (view and drafts kept, a\n\"Couldn't refresh; retrying.\" note, backoff continues), but because hosts may\nalso throw for revoked access, three consecutive thrown ticks fail closed.\n**Hosts must signal revocation by returning `availability: 'revoked'`** (or\nanother non-`available` state) from `load`, not by throwing: a returned denial\nclears the view on that tick, while a host that throws on revocation leaves the\nalready-displayed content visible until the third thrown tick (about 10 s at the\ndefault interval and backoff) before the view fails closed.\nSplit drafts survive a refresh only when the reviewer edited them. A failed stage shows its safe category with the form\nstill available. There is no\nstreaming channel; status is read through the existing item/analysis APIs. The\nmaintained fixture records the instruction durably and then runs the stages in\nthe background so the browser proof exercises the in-place update.\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.\n`list({ state?, assigneeId?, cursor?, limit? })` returns one page newest receipt\nfirst (`intake_items.created_at`, id tiebreak) through `service.listItemsPage`.\n`limit` defaults to 20 and is capped at 100; the cursor is an opaque\n(received-at, id) continuation, never a bare id. Tenant, confidential scope,\nretention and the `state` filter run inside the page query, so a page is full\nand `nextCursor` is present only when more rows follow.\n\nAuthority is rechecked at the same two levels `load` applies, but per page\nrather than per row: the host authorize callback once per distinct captured\nceiling in a batch, and the execution read gate (policy, ceiling, handler and\noperation allowlists, principal) once per distinct (item, handler) bound by\nthe page's saved actions. The list gate is read-only: it evaluates the same\nchecks as `listReviews` without the retention write, so an item that the item\nview would narrow past its deadline is simply not listed. The list gate also\nresolves each bound handler and rejects version drift against the saved\nbinding and omits a cancelled item, as `listReviews` does; it does not\nevaluate per-action target,\nevidence or plan gates, so an item whose target became unreadable still lists\nand its item view refuses. An item whose bound\nhandler the actor cannot currently read is not listed at all, rather than\nlisted with a state derived from actions the actor may not see; actions\nwithout a proposal bind no handler and are not gated, as in `listReviews`.\nHandler bindings (handler id and version, projected in SQL, distinct per\nitem) are read once per fetched batch for the rows that passed the item-level\ngate. Note the host cost plainly: the execution `authorize` callback runs once\nper distinct (item, handler) bound by a scanned row that passed the item-level\ngate. Each item is its own authorization subject, so nothing is deduplicated\nacross items; the count is bounded by rows scanned (at most\n`ITEM_SCAN_CEILING`) and runs in host code rather than as N+1 database reads. Either host callback, the item-level `authorize` or the execution\n`authorize` behind the handler gate, follows the same rule: a refusal (false,\nor an access check that fails) hides the rows it covers, and a callback that\nthrows rejects the whole page (and `listItems()`) with its error, as `load`\ndoes, so a transient failure never lets paging move past readable rows.\nDenied or unreadable rows are skipped without\ndisclosure and the page refills from the next rows; refills grow from the page\nsize to 100 rows per query, so a fully denied scope costs about a dozen page\nreads and as many binding reads per call. One service call examines at most\n`ITEM_SCAN_CEILING` (1000) rows, or the caller's smaller `scanLimit`, and\nreports the count as `scanned`; when a mostly-denied scope reaches the ceiling\nthe page comes back short with a `nextCursor`. Rows are authorized first and\nthe `state` filter is then applied to the fetched batch, so it is bounded by\nthe same scan limit: a rare state yields short pages with a cursor rather than\nan unbounded walk inside one query. A row the actor can read whose state does\nnot match discloses nothing and may anchor the cursor, so a filter with no\nmatch among the next 1000 rows returns an empty page with a cursor and still\npages to the end. The host `list` passes its remaining budget as `scanLimit`, so the\nrows scanned across the service calls of one host call never exceed 1000: a\nshort service page with a cursor, or an exhausted budget while filtering by\nassignee, ends the host call with the same cursor and the caller continues.\nOnly rows the service wrote are supported: cursors carry the canonical\nmillisecond ISO receipt time the service stores. A row whose stored receipt\ntime is not that form (on SQLite anything but `YYYY-MM-DDTHH:MM:SS.mmmZ`,\nincluding a parseable value without milliseconds or with an offset; on\nPostgreSQL a value with a sub-millisecond part) is never listed, whatever the\nactor's access, and counts toward the scan, as does a row whose id is not a\nUUID. On SQLite a non-text stored value (an integer, say) and a receipt value\nor id longer than 64 characters or containing a NUL character are excluded by\nthe page query itself: the first sorts after every text value so the scan\ncould never move past it, the second could not be carried by any cursor, and\nthe third is returned truncated at the NUL, so it would look canonical and\nsort above a legitimate row at the truncated value. Within a call the scan moves\npast a service-foreign row by its exact stored value, and an unsealed cursor\nnames the last row before it that the actor can read. A call that finds no readable row within its scan limit (at\nmost 1000) fails closed with `IntakeScanLimitError` unless a `cursorKey` is\nconfigured, as below. The host\n`list` returns what earlier service calls collected, with their cursor, when\na later call fails that way.\n\n`listItems()` is deprecated and kept only for callers of the array-returning\nAPI. It walks every page, so its cost grows with the scope, including one\nexecution authorization per (item, handler) across the whole scope. It follows\nthe internal scan position, which never leaves the service, so runs of rows\nthe actor cannot read never stop it and it returns `[]` where nothing is\nreadable. Items are newest-first and gated exactly as on a page.\n\nThe page's inbox state is the durable projection of the saved action journal\nand processing state, computed once per scanned row inside the page query;\n`load` remains the authoritative, live-gated projection for one item (it still\nprojects individual stale actions, which the list does not). Cost is\nproportional to the page for unfiltered pages and common states, and to the\nnumber of items scanned until enough matches are found for rare states: the\nstate is not persisted, because the action journal is append-only and\nprojecting it into an indexed column would need a maintained denormalisation\nin the execution service plus a migration, which this change does not take.\nListing does not load original bytes.\n\nCursors never disclose an unreadable row. Without a `cursorKey` in the\nservice options, every cursor (each row's and `nextCursor`) names a row the\nactor can read (the last one when a page stops early); a page that spends its scan limit before filling hands back\nthe last such row, and one that found none fails closed with\n`IntakeScanLimitError` (`Intake scan limit`, no id or time). A run of rows the\nactor cannot read (refused by the captured ceiling or the execution gate, or\nstored with a non-canonical receipt time) at least as long as the scan limit\ntherefore stops an unkeyed walk there; readable rows that the `state` filter\nexcludes never count toward such a run. With a `cursorKey` (a server secret of at least 32 bytes), every\ncursor is sealed with AES-256-GCM, bound to the tenant, confidential scope and\nactor, and a budget-stopped page returns a sealed position past what it\nskipped, so the walk always progresses. Sealed cursors have a fixed length\n(the plaintext is padded to 1024 bytes) and a fresh nonce per seal, so neither\ntheir length nor their bytes reveal anything about the stop row. `scanned` is for trusted host\naccounting and must not be sent to clients; a short page with a cursor tells\na caller only that unlisted rows exist, never which.\n\nNewest-first paging relies on `intake_items_received_idx`\n(`tenant_id, confidential_scope_id, created_at, id`), the full ordering key, so\nan unfiltered page is a backward index scan that stops at the page boundary;\nthe foundation suite asserts that plan on both engines (SQLite\n`EXPLAIN QUERY PLAN`, PostgreSQL `EXPLAIN`), with the per-row state counters\nserved by `intake_actions_item_idx`. On SQLite the page query pins the index with `INDEXED BY` (except under an\nid restriction, where direct id lookups are cheaper), because without planner\nstatistics SQLite otherwise picks `intake_items_expiry_idx` plus a sort; the\nSQLite plan check runs without `ANALYZE`. A deployment must have applied the\nmanifest migration, or the page query fails with a missing-index error. This package ships no\nmigration files: indexes are declared on the model, the build emits them into\n`dist/manifest.json`, and `smrt db:migrate` / the public migration API create\nthem, which is how `intake_items_receipt_idx` shipped. Deploy the manifest\nmigration before upgrading hosts; `db:status --parity` reports the index as\nmissing until then.\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), which `load` calls for every part and `list` never calls;\n`load` sets no row preview on its entry.\n\nRow previews are opt-in through an optional fifth adapter,\n`rowEvidence(rows)`. When it is supplied, `list` reads the page's durable\nevidence metadata with one `service.listEvidence(itemIds)` query (at most 100\nids; items the scope cannot read, and expired or inactive items, are omitted\nwithout disclosure; the host `authorize` runs once per distinct captured\nceiling through the same gate as `listItemsPage`, so a refusal or a\nmalformed captured ceiling hides the item and a failing `authorize` rejects\nthe page; an item whose stored data does not parse is skipped by both\n`listItemsPage` and `listEvidence`, never fatal). It then calls `rowEvidence`\nonce with each row's primary part (`{ itemId, evidence }`) and publishes the\nreturned `{ label, viewUrl?, thumbnailUrl? }` as `InboxEntry.evidence`\n(`{ evidence, label, viewUrl?, thumbnailUrl? }`, never text). Build those URLs\nfrom metadata without reading bytes; if `rowEvidence` throws or returns a\nmalformed answer, the page still lists without previews, and a row without\ndurable parts never gets one. When it is omitted, rows show no original link.\nEither way `list` reads the page's evidence metadata once, in that same query,\nso the row label can use the original filename; it never reads original bytes\nand never calls `evidence(part)`. The primary part is chosen by role, not media\ntype: the shallowest child part when any part has a parent (an upload's file\nunder its capture record, an email's first attachment under the message, even\nwhen that child is JSON), else the first root part (a watched-folder file);\npart ids sort numerically. `IntakeInbox`/`InboxView` accept an\noptional `rowAside` snippet receiving the `InboxEntry`; without it a row renders\nthe host's \"Open original\" link (new tab) and thumbnail when the URLs are\nsame-origin; each link's accessible name is \"Open original: <row label>\", and\na row shows no wrapper when neither URL is usable. The slot is a plain `div`,\nnot an `aside` landmark, so a page does not add one landmark per row. Optional\n`label(item, evidence)` changes presentation; `evidence` is the item's durable\nparts with the primary part first, and the default label is the primary part's\nown original `filename`, else `Intake <8 hex>`. Optional\n`assignments(itemIds)` reads one page's assignments in one batch (otherwise\n`assignment` runs per row), and optional `assignedItemIds(assigneeId)` lets\n`list` push the assignee filter into the page query (junk ids are dropped, not\nfatal); without it an assignee filter scans further pages and filters by\n`assignment`, which stays correct but costs more rows. The page's continuation\nis always the last row actually returned, so an assignment that changes\nbetween the page read and the assignment read costs at most one extra scan and\nnever skips or repeats a row. 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": {
@@ -1635,7 +1635,7 @@
1635
1635
  "code": "not-module-scope",
1636
1636
  "helper": "definePlaybook",
1637
1637
  "message": "`definePlaybook()` must be called at module scope to be emitted into the manifest and knowledge graph. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.",
1638
- "line": 429,
1638
+ "line": 430,
1639
1639
  "column": 7,
1640
1640
  "sourceFile": "src/test-support/execution.ts"
1641
1641
  },
@@ -1643,7 +1643,15 @@
1643
1643
  "code": "not-module-scope",
1644
1644
  "helper": "definePlaybook",
1645
1645
  "message": "`definePlaybook()` must be called at module scope to be emitted into the manifest and knowledge graph. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.",
1646
- "line": 2089,
1646
+ "line": 1080,
1647
+ "column": 7,
1648
+ "sourceFile": "src/test-support/execution.ts"
1649
+ },
1650
+ {
1651
+ "code": "not-module-scope",
1652
+ "helper": "definePlaybook",
1653
+ "message": "`definePlaybook()` must be called at module scope to be emitted into the manifest and knowledge graph. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.",
1654
+ "line": 2293,
1647
1655
  "column": 9,
1648
1656
  "sourceFile": "src/test-support/execution.ts"
1649
1657
  },
@@ -1651,7 +1659,7 @@
1651
1659
  "code": "not-module-scope",
1652
1660
  "helper": "definePlaybook",
1653
1661
  "message": "`definePlaybook()` must be called at module scope to be emitted into the manifest and knowledge graph. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.",
1654
- "line": 1118,
1662
+ "line": 1567,
1655
1663
  "column": 9,
1656
1664
  "sourceFile": "src/test-support/proposals.ts"
1657
1665
  },
@@ -1659,7 +1667,7 @@
1659
1667
  "code": "not-module-scope",
1660
1668
  "helper": "definePlaybook",
1661
1669
  "message": "`definePlaybook()` must be called at module scope to be emitted into the manifest and knowledge graph. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.",
1662
- "line": 1690,
1670
+ "line": 2139,
1663
1671
  "column": 9,
1664
1672
  "sourceFile": "src/test-support/proposals.ts"
1665
1673
  },
@@ -1667,7 +1675,7 @@
1667
1675
  "code": "not-module-scope",
1668
1676
  "helper": "definePlaybook",
1669
1677
  "message": "`definePlaybook()` must be called at module scope to be emitted into the manifest and knowledge graph. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.",
1670
- "line": 4111,
1678
+ "line": 6746,
1671
1679
  "column": 9,
1672
1680
  "sourceFile": "src/test-support/proposals.ts"
1673
1681
  },
@@ -1675,7 +1683,7 @@
1675
1683
  "code": "not-module-scope",
1676
1684
  "helper": "definePlaybook",
1677
1685
  "message": "`definePlaybook()` must be called at module scope to be emitted into the manifest and knowledge graph. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.",
1678
- "line": 4736,
1686
+ "line": 7375,
1679
1687
  "column": 7,
1680
1688
  "sourceFile": "src/test-support/proposals.ts"
1681
1689
  }
@@ -1,4 +1,5 @@
1
1
  import type { ReceiptResult } from '../dto.js';
2
+ import { presentedFilename } from '../presented-filename.js';
2
3
  import type { IngestionService, IntakePart, ReceiveInput } from '../server.js';
3
4
  /** Host configuration, reconstructed from authenticated session/account state. */
4
5
  export interface SourceBinding {
@@ -22,6 +23,16 @@ export declare class SourceInputError extends Error {
22
23
  export declare function record(value: unknown): Record<string, unknown>;
23
24
  export declare function keys(value: Record<string, unknown>, allowed: string[]): void;
24
25
  export declare function text(value: unknown): string;
26
+ /**
27
+ * A name a source presented for a part (upload, attachment, folder entry),
28
+ * kept for labels only (#4214); see `../presented-filename.ts` for the unsafe
29
+ * character set it removes.
30
+ */
31
+ export { presentedFilename };
32
+ /** `{ filename }` for a usable presented name, else nothing to spread. */
33
+ export declare function withPresentedFilename(value: unknown): {
34
+ filename?: string;
35
+ };
25
36
  export declare function timestamp(value: unknown): Date;
26
37
  /** Bound bytes before parsing or handing them to the receipt service. */
27
38
  export declare function boundedBytes(stream: ReadableStream<Uint8Array> | null, limit: number): Promise<Uint8Array>;
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/sources/common.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE/E,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,YAAY,CAAC,iBAAiB,CAAC,CAAC;IACjD,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACrC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC/B,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AACD,MAAM,MAAM,YAAY,GACpB,aAAa,GACb;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;CACvE,CAAC;AACN,qBAAa,gBAAiB,SAAQ,KAAK;aAEvB,QAAQ,EACpB,SAAS,GACT,OAAO,GACP,kBAAkB,GAClB,mBAAmB;gBAJP,QAAQ,EACpB,SAAS,GACT,OAAO,GACP,kBAAkB,GAClB,mBAAmB;CAI1B;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9D;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAG5E;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAI3C;AACD,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAI9C;AACD,yEAAyE;AACzE,wBAAsB,YAAY,CAChC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,EACzC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,UAAU,CAAC,CAsBrB;AACD,sFAAsF;AACtF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,IAAI,CAmCN;AACD,wBAAsB,OAAO,CAC3B,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,IAAI,EACjB,KAAK,EAAE,UAAU,EAAE,GAClB,OAAO,CAAC,YAAY,CAAC,CA2BvB;AACD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAIxD"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/sources/common.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE/E,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,YAAY,CAAC,iBAAiB,CAAC,CAAC;IACjD,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACrC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC/B,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AACD,MAAM,MAAM,YAAY,GACpB,aAAa,GACb;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;CACvE,CAAC;AACN,qBAAa,gBAAiB,SAAQ,KAAK;aAEvB,QAAQ,EACpB,SAAS,GACT,OAAO,GACP,kBAAkB,GAClB,mBAAmB;gBAJP,QAAQ,EACpB,SAAS,GACT,OAAO,GACP,kBAAkB,GAClB,mBAAmB;CAI1B;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9D;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAG5E;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAI3C;AACD;;;;GAIG;AACH,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,0EAA0E;AAC1E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAGA;AACD,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAI9C;AACD,yEAAyE;AACzE,wBAAsB,YAAY,CAChC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,EACzC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,UAAU,CAAC,CAsBrB;AACD,sFAAsF;AACtF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,IAAI,CAmCN;AACD,wBAAsB,OAAO,CAC3B,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,IAAI,EACjB,KAAK,EAAE,UAAU,EAAE,GAClB,OAAO,CAAC,YAAY,CAAC,CAiCvB;AACD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAIxD"}
@@ -1 +1 @@
1
- {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/sources/email.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,YAAY,EAGlB,MAAM,aAAa,CAAC;AAErB,+FAA+F;AAC/F,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,KAAK,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,UAAU,CAAC;KACnB,CAAC,CAAC;CACJ;AACD,MAAM,WAAW,aAAa;IAC5B,sGAAsG;IACtG,OAAO,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AACD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,aAAa,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnE,oFAAoF;IACpF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AACD,iGAAiG;AACjG,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAClD,OAAO,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IA+FvD,YAAY,CAChB,UAAU,EAAE,SAAS,aAAa,EAAE,GACnC,OAAO,CAAC,YAAY,EAAE,CAAC;CAS3B"}
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/sources/email.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,YAAY,EAGlB,MAAM,aAAa,CAAC;AAErB,+FAA+F;AAC/F,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,KAAK,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,UAAU,CAAC;KACnB,CAAC,CAAC;CACJ;AACD,MAAM,WAAW,aAAa;IAC5B,sGAAsG;IACtG,OAAO,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AACD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,aAAa,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnE,oFAAoF;IACpF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AACD,iGAAiG;AACjG,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAClD,OAAO,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAgGvD,YAAY,CAChB,UAAU,EAAE,SAAS,aAAa,EAAE,GACnC,OAAO,CAAC,YAAY,EAAE,CAAC;CAS3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/sources/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAKL,KAAK,aAAa,EAMnB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,iBAAiB,CAAC,CAAC,SAAS,EAAE;QAC5B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;QAC3B,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;CAC3B;AACD,MAAM,WAAW,iBAAiB;IAChC,2FAA2F;IAC3F,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;CACrE;AAeD,8FAA8F;AAC9F,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,iBAAiB,GACzB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA6IzC;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,IAAI,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AACD,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,CACJ,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,UAAU,GACxB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;CAC3C,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAoB1C"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/sources/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAML,KAAK,aAAa,EAMnB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,iBAAiB,CAAC,CAAC,SAAS,EAAE;QAC5B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;QAC3B,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;CAC3B;AACD,MAAM,WAAW,iBAAiB;IAChC,2FAA2F;IAC3F,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;CACrE;AAeD,8FAA8F;AAC9F,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,iBAAiB,GACzB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAoJzC;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,IAAI,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AACD,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,CACJ,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,UAAU,GACxB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;CAC3C,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAoB1C"}
@@ -1 +1 @@
1
- {"version":3,"file":"watch-folder.d.ts","sourceRoot":"","sources":["../../src/sources/watch-folder.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,aAAa,CAAC;IACvB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,iFAAiF;IACjF,UAAU,CAAC,CAAC,QAAQ,EAAE,SAAS,GAAG,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AACD,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,YAAY,CAAC;CACtB;AAYD,gGAAgG;AAChG,qBAAa,wBAAwB;IAGvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAC/D,OAAO,CAAC,OAAO,CAAS;gBACK,OAAO,EAAE,kBAAkB;YAI1C,MAAM;YAmBN,aAAa;YAQb,WAAW;IAuFnB,IAAI,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;CA2F3C"}
1
+ {"version":3,"file":"watch-folder.d.ts","sourceRoot":"","sources":["../../src/sources/watch-folder.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,aAAa,CAAC;IACvB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,iFAAiF;IACjF,UAAU,CAAC,CAAC,QAAQ,EAAE,SAAS,GAAG,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AACD,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,YAAY,CAAC;CACtB;AAYD,gGAAgG;AAChG,qBAAa,wBAAwB;IAGvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAC/D,OAAO,CAAC,OAAO,CAAS;gBACK,OAAO,EAAE,kBAAkB;YAI1C,MAAM;YAmBN,aAAa;YAQb,WAAW;IAwFnB,IAAI,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;CA2F3C"}