@happyvertical/smrt-ingestion 0.57.21 → 0.57.23

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 (87) hide show
  1. package/FEEDBACK.md +104 -0
  2. package/PROPOSALS.md +77 -0
  3. package/README.md +126 -0
  4. package/REVIEW.md +272 -4
  5. package/agents/execution.md +72 -0
  6. package/dist/chunks/{execution-9CfjxX8I.js → execution-BzVQax0n.js} +421 -23
  7. package/dist/chunks/execution-BzVQax0n.js.map +1 -0
  8. package/dist/chunks/{feedback-CuUzWKzP.js → feedback-D3UTF8fm.js} +119 -21
  9. package/dist/chunks/feedback-D3UTF8fm.js.map +1 -0
  10. package/dist/chunks/{extraction-CJ-zrbg0.js → proposal-contracts-77Uo1B1j.js} +39 -39
  11. package/dist/chunks/proposal-contracts-77Uo1B1j.js.map +1 -0
  12. package/dist/chunks/{reviewer-instruction-Db3jDN9a.js → reviewer-instruction-CEc3AOuU.js} +2 -2
  13. package/dist/chunks/{reviewer-instruction-Db3jDN9a.js.map → reviewer-instruction-CEc3AOuU.js.map} +1 -1
  14. package/dist/dto.d.ts +69 -0
  15. package/dist/dto.d.ts.map +1 -1
  16. package/dist/execution-contracts.d.ts +4 -1
  17. package/dist/execution-contracts.d.ts.map +1 -1
  18. package/dist/execution-dto.d.ts +16 -0
  19. package/dist/execution-dto.d.ts.map +1 -1
  20. package/dist/execution.d.ts +58 -3
  21. package/dist/execution.d.ts.map +1 -1
  22. package/dist/execution.js +2 -2
  23. package/dist/feedback-dto.d.ts +12 -4
  24. package/dist/feedback-dto.d.ts.map +1 -1
  25. package/dist/feedback-internal.d.ts +9 -0
  26. package/dist/feedback-internal.d.ts.map +1 -1
  27. package/dist/feedback.d.ts +3 -0
  28. package/dist/feedback.d.ts.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/manifest.json +321 -3
  31. package/dist/models.d.ts +15 -0
  32. package/dist/models.d.ts.map +1 -1
  33. package/dist/models.js +73 -2
  34. package/dist/models.js.map +1 -1
  35. package/dist/policy.d.ts +3 -0
  36. package/dist/policy.d.ts.map +1 -1
  37. package/dist/proposal-errors.d.ts +4 -0
  38. package/dist/proposal-errors.d.ts.map +1 -1
  39. package/dist/proposal-sdk.d.ts +54 -1
  40. package/dist/proposal-sdk.d.ts.map +1 -1
  41. package/dist/proposals.d.ts +2 -1
  42. package/dist/proposals.d.ts.map +1 -1
  43. package/dist/proposals.js +215 -10
  44. package/dist/proposals.js.map +1 -1
  45. package/dist/provider-failure.d.ts +3 -0
  46. package/dist/provider-failure.d.ts.map +1 -0
  47. package/dist/redaction.d.ts +9 -0
  48. package/dist/redaction.d.ts.map +1 -0
  49. package/dist/review-dto.d.ts +14 -4
  50. package/dist/review-dto.d.ts.map +1 -1
  51. package/dist/review-host.d.ts.map +1 -1
  52. package/dist/server.d.ts +82 -10
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +366 -47
  55. package/dist/server.js.map +1 -1
  56. package/dist/smrt-knowledge.json +128 -22
  57. package/dist/svelte/components/ActionReview.svelte +39 -9
  58. package/dist/svelte/components/ActionReview.svelte.d.ts +8 -1
  59. package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -1
  60. package/dist/svelte/components/EvidenceViewer.svelte +60 -4
  61. package/dist/svelte/components/EvidenceViewer.svelte.d.ts +12 -1
  62. package/dist/svelte/components/EvidenceViewer.svelte.d.ts.map +1 -1
  63. package/dist/svelte/components/InboxView.svelte +160 -18
  64. package/dist/svelte/components/InboxView.svelte.d.ts +28 -1
  65. package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -1
  66. package/dist/svelte/components/IntakeWorkspace.svelte +27 -0
  67. package/dist/svelte/components/IntakeWorkspace.svelte.d.ts +19 -0
  68. package/dist/svelte/components/IntakeWorkspace.svelte.d.ts.map +1 -0
  69. package/dist/svelte/components/ReviewView.svelte +264 -23
  70. package/dist/svelte/components/ReviewView.svelte.d.ts +18 -1
  71. package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -1
  72. package/dist/svelte/components/TriageView.svelte +669 -0
  73. package/dist/svelte/components/TriageView.svelte.d.ts +16 -0
  74. package/dist/svelte/components/TriageView.svelte.d.ts.map +1 -0
  75. package/dist/svelte/i18n.d.ts +39 -0
  76. package/dist/svelte/i18n.d.ts.map +1 -1
  77. package/dist/svelte/i18n.js +39 -0
  78. package/dist/svelte/index.d.ts +1 -0
  79. package/dist/svelte/index.d.ts.map +1 -1
  80. package/dist/svelte/index.js +1 -0
  81. package/dist/svelte/view-utils.d.ts +65 -0
  82. package/dist/svelte/view-utils.d.ts.map +1 -1
  83. package/dist/svelte/view-utils.js +145 -0
  84. package/package.json +13 -13
  85. package/dist/chunks/execution-9CfjxX8I.js.map +0 -1
  86. package/dist/chunks/extraction-CJ-zrbg0.js.map +0 -1
  87. package/dist/chunks/feedback-CuUzWKzP.js.map +0 -1
package/REVIEW.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # Authenticated review surfaces
2
2
 
3
- `@happyvertical/smrt-ingestion/svelte` exports `IntakeInbox`, `IntakeReview` and
4
- `EvidenceViewer`. The browser-safe DTO entry exports their `IntakeReviewHost`
3
+ `@happyvertical/smrt-ingestion/svelte` exports `IntakeInbox`, `IntakeReview`,
4
+ `IntakeWorkspace` and `EvidenceViewer`. The browser-safe DTO entry exports their `IntakeReviewHost`
5
5
  callback contract. Import styling/theme configuration from smrt-ui in the host.
6
6
  Components use smrt-ui primitives and the `ingestion.*` i18n catalog.
7
+ Existing `IntakeInbox` hosts now keep the previous rows visible, with their
8
+ controls disabled, while a refresh or filter change loads; any failed read
9
+ (Load more included) still clears them.
7
10
 
8
11
  The host authenticates every request, resolves tenant/actor/confidential scope
9
12
  from its session, enforces CSRF on mutations, and constructs `IngestionService`.
@@ -103,6 +106,49 @@ Denied refresh clears prior originals, candidates, arguments and results.
103
106
  Missing confidence is unavailable; page/time links exist only for observed
104
107
  locations. Provider errors and unresolved/partial results remain review states.
105
108
 
109
+ ## Submitter-scoped decisions
110
+
111
+ For trusted internal sources the person who submitted an item may confirm or
112
+ decline their own proposal without the reviewer inbox. `listOwnProposals`
113
+ returns only items whose server-recorded `submitter_id` equals the current
114
+ authenticated actor, each with its bounded saved review page reloaded under
115
+ current item, handler and target authority. An item the actor can no longer
116
+ read (the host `authorize` returns false or throws `IntakeAccessDeniedError`)
117
+ is skipped before projection: the page is filled from readable items only, in
118
+ 50-row batches, and `nextCursor` names the last emitted item and is present
119
+ only when a further readable item was actually found. Neither the entries, the
120
+ page size nor the cursor reveal a hidden item. If one call has to skip 1000
121
+ hidden submissions it fails with `Own proposal scan limit` rather than return a
122
+ short page or a cursor that stops inside the hidden run; this error is the only
123
+ signal, and it says only that such a run exists. Any other `authorize` error
124
+ propagates instead of shrinking the list. An item that is still readable but
125
+ whose reviews are denied is returned as `{ item, reviews: null, unavailable:
126
+ true }`: that covers ingestion's own denials and any throw from the host
127
+ `assertTarget` (deleted or revoked targets), whose text is never returned.
128
+ Integrity, validation and database errors from ingestion itself propagate. The
129
+ reviewer `listReviews` surface keeps propagating host target errors unchanged.
130
+ `submitOwnDecision` takes the same `ReviewInput` CAS fields and request ID as
131
+ `submitDecision` and accepts `approve`, `reject` and `defer` only; `correct`
132
+ belongs to the reviewer surface. It runs under the `review_own` execution
133
+ operation: the host's `authorize` must return `submitter: true`, the resolved
134
+ policy must have `submitterReview` enabled, and the recorded submitter must be
135
+ the current actor and also the receipt's captured execution principal (the
136
+ identity the effect runs as), checked under the item lock and again at apply.
137
+ A receipt captured for a bot or service principal is never self-approvable. Policy enablement, handler/operation allowlists, captured
138
+ ceiling, confidential access, retention and the first-valid-decision lock are
139
+ unchanged. The decision is a human decision by that actor, recorded with
140
+ `mode: 'review_own'`; reviewer decisions carry `mode: 'review'`. In both modes
141
+ `selfDecision` is true exactly when the deciding actor is the item's
142
+ submitter, so a reviewer approving their own submission is never independent
143
+ review. A self-decision is not a reviewer judgment: it never feeds
144
+ automation eligibility (machine authorization remains the separate evaluated
145
+ `kind: 'machine'` path) and its feedback events carry the same marker. The
146
+ `unavailable` entry discloses nothing about the denied handler or target.
147
+ `applyAction` then runs once under the approver's live grants exactly as
148
+ for reviewer decisions. Hosts render this surface from their own authenticated
149
+ session; presentation state confers no authority, and a source payload cannot
150
+ nominate a submitter.
151
+
106
152
  ## Assignment and logical splits
107
153
 
108
154
  Assignment is explicitly host-owned, durable and versioned. `assign` receives
@@ -283,6 +329,215 @@ choice and execution success. No learning, feedback storage or rule promotion is
283
329
  implemented by these components; #3676 owns those services. Do not pass feedback
284
330
  or a feedback callback to readers without the relevant host grant.
285
331
 
332
+ ## Reviewer-chosen actions and "handled, no action"
333
+
334
+ A reviewer is not limited to model suggestions. `ItemReviewView.catalog` carries
335
+ the handlers the item's catalog currently offers (`listHandlers`, discovery media
336
+ types and live preview authority; it is presentation, never a grant). The optional
337
+ `startAction` callback forwards `StartActionInput` (`itemId`, `handlerId@version`,
338
+ explicit `args`, `requestId`, optional `reason`) to `startReviewerAction`, which
339
+ requires the current reviewer grant under that handler, a completed or partial
340
+ attempt on the current analysis revision, and an operation handler the item's
341
+ evidence media types offer. Plan handlers go through `previewPlan`. The server
342
+ creates an action keyed `reviewer:<requestId>` with origin `reviewer-selected`
343
+ (`createAction` refuses that key prefix and origin, so process authority cannot
344
+ mint reviewer provenance; re-previewing such an action with `previewProposal`
345
+ needs the reviewer grant and keeps its handler fixed, so only its arguments can
346
+ be corrected),
347
+ runs the handler's normal `validate`/`preview`, publishes a `waiting_review`
348
+ revision and requires the same explicit approval and apply as a generated
349
+ proposal. Replays with the same request return the same review; changed arguments
350
+ or reason conflict. Such actions are never automation-eligible: `authorizeAutomatic`
351
+ refuses them regardless of policy opt-ins. The UI pre-fills arguments only from a
352
+ same-handler suggestion (for example a `needs_review` one) and a single original's
353
+ evidence references, lists the required fields still missing, and submits exactly
354
+ what the reviewer typed. An item whose current attempt failed outright must be
355
+ reprocessed first; an interpretation that completed with `provider_error`,
356
+ `unknown` or no suggestions can be acted on directly. `ReviewAction.origin`
357
+ exposes the provenance so `ActionReview` can label human-started actions.
358
+
359
+ The optional `resolve` callback forwards `ResolveItemInput`
360
+ (`disposition: 'resolve_no_action'`, `requestId`, optional `reason`) to
361
+ `resolveItem`. It needs reviewer authority under any handler the item's captured
362
+ ceiling offers and refuses while an action is `authorized` with a live approval,
363
+ `executing`, `outcome_unknown` or `succeeded`; waiting, deferred, rejected and
364
+ failed actions, and authorized ones whose approval expired, may remain (neither
365
+ an exhausted failed action nor a lapsed approval can be rejected, so they must
366
+ not keep the queue open). Afterwards only reject remains: those actions can no
367
+ longer be approved, corrected, deferred, re-previewed, retried, applied or
368
+ automated. `submitDecision` refuses every decision but `reject`, no new revision
369
+ is published for the item, `applyAction` rechecks the disposition before any
370
+ effect, and `analyze`, `reviseLogicalSplit` and `createAction` refuse the item
371
+ under its lock before any write, so it is never reanalysed, re-split or given a
372
+ new action. `resolveItem` supersedes any queued or running analysis under the
373
+ same lock and `claimAnalysis` refuses a resolved item, so no provider is called
374
+ after a human closed it; the item's `processingState` keeps its last value (it
375
+ describes analysis, not review), and the inbox state `resolved` is authoritative.
376
+ `publishProposal` (process authority) refuses a resolved item and any
377
+ reviewer-selected action. The UI hides Approve, Defer, Apply, every edit control, suggestion
378
+ preview and split controls on a resolved item.
379
+ Resolved items hide the instruction editor and stop reinterpretation polling, even if an older processing projection is still queued. Recording an instruction cannot append a new revision after resolution.
380
+
381
+ Paged lists project the resolved state and disposition time in the same bounded, scope-filtered query, so state filters and row controls agree with a full item reload.
382
+
383
+ The append-only `intake_dispositions` record is terminal: replaying the same
384
+ request returns the same disposition, any other request conflicts, and
385
+ `startReviewerAction` refuses the item. `IntakeItemDTO.disposition` projects only
386
+ `{ state: 'resolved', at }` to any item reader (`listItems` reads it in the same
387
+ statement as the page); the reviewer and reason are returned only by
388
+ `getItemDisposition`, which requires reviewer authority, and the review hosts put
389
+ them on `ItemReviewView.disposition` (omitted when that read is denied, so the
390
+ view shows only "Handled, no action"). `loadReviewSummary` reports inbox state `resolved` unless an action
391
+ actually succeeded. Privacy deletion and retention expiry redact the reason and
392
+ reviewer (`intake_dispositions.data`) and keep only the kind. The disposition is
393
+ distinct from `reject`, which still means the proposal was wrong. Closing an item
394
+ records `reviewer_routing` feedback immediately; a started action records it only
395
+ when the reviewer approves it (see FEEDBACK.md). Capture is an optional projection
396
+ that never vetoes the decision or disposition.
397
+
398
+ `startReviewerAction` applies the handler's discovery media types and the live
399
+ reviewer grant/policy for that handler. It intentionally does not apply the
400
+ proposal catalog's provider-policy filter or `maxHandlers` slice: those bound what
401
+ a model may be offered, and a human choosing a handler sends nothing to a
402
+ provider. The UI picker shows the offered catalog, so a handler hidden from it is
403
+ only reachable by a reviewer who already holds `review` authority for it. The maintained
404
+ browser fixture serves both operations behind CSRF and the same session scope.
405
+
406
+ ## Single-page triage workspace
407
+
408
+ `IntakeWorkspace` (#4215) composes the existing pieces so a reviewer works the
409
+ whole queue on one page: the paged inbox as a list pane, the review view of the
410
+ selected item beside it (the original above its action form), and the
411
+ reviewer-instruction message box of the item pane. Props: `host`, `contextKey`
412
+ (replace on session change, as for the other components), optional initial
413
+ `itemId`, `pollMs`, `rowAside` and `onselect(itemId | undefined)`, which only
414
+ reports the selection (to keep a URL in step) and never navigates. It adds **no
415
+ host callback and no server authority**: every read is the host's `list` and
416
+ `load`, every action still goes through the handler preview and an explicit
417
+ approval and apply under the approver's live grants, and the message steers the
418
+ model only, exactly as in `IntakeReview`.
419
+
420
+ - **Inline original.** In the item pane `EvidenceViewer` shows one part at a
421
+ time (`layout="pager"`), opening on the first part that renders inline (the
422
+ uploaded file rather than its capture record), with previous/next original.
423
+ A PDF with more than one *observed* page (extraction locations or proposed
424
+ page groups; `pages` prop) gets previous/next page, which reloads the frame
425
+ at the same-origin URL plus `#page=n`; only `evidenceUrl`-approved URLs ever
426
+ reach a frame. `IntakeReview` gets the same page navigation over its
427
+ unchanged stacked list of parts. The chosen part and page belong to the item
428
+ view (bindable `chosen`/`page`), so they survive in-place reloads, Prepare
429
+ review, Approve and Apply.
430
+ - **Keyboard.** J/K and Alt+Down/Alt+Up move to the next/previous row (loading
431
+ the next page at the end), ignored while focus is in a text field, select or
432
+ editable region; "Previous item"/"Next item" buttons carry
433
+ `aria-keyshortcuts`. The selected row is `aria-current`, and every selection
434
+ moves focus to the item pane region, named by the row's label. A navigation
435
+ that waited for the next page is dropped if the reviewer chose another item
436
+ meanwhile.
437
+ - **Unsent input.** The pane holds unsent input when it has instruction text,
438
+ an edited suggestion draft or split draft, a chosen start handler or start
439
+ reason, a no-action reason, or an action editor with edited arguments, a
440
+ reason or a feedback comment. It is read from the drafts themselves, so it
441
+ holds while a reload has cleared the view; while the view is denied nothing
442
+ is shown and nothing counts (the drafts stay in memory and return with an
443
+ authorized refresh, as before). A reviewer-initiated move (J/K, Alt+arrows,
444
+ Next/Previous item, a row's Review button) then asks inline, in a group
445
+ labelled by its note: "Stay on this item" takes focus and returns it to the
446
+ field the reviewer last used; "Discard and open the other item" proceeds.
447
+ When an item is closed without action (here or by another session), input a
448
+ closed item cannot take
449
+ (instruction, suggestion and split drafts, a started action, a no-action
450
+ reason) is discarded.
451
+ - **Live state.** A row's status follows the item: "Interpreting…", "Ready to
452
+ review", "Needs review" with missing argument names or the generation
453
+ outcome, "Awaiting approval", "Approved, not yet applied", "Failed" with a
454
+ safe category, "Handled, no action", or the listed inbox state. Rows the
455
+ reviewer has not opened show the listed state only; reasons come from a
456
+ loaded view, never from server exception text. The open item's own polling
457
+ (`ReviewView`, unchanged backoff and three-failure fail-closed) reports each
458
+ displayed view and the workspace patches that row in place
459
+ (`InboxView.patch`, keeping the listed label and preview, and the row's
460
+ assignment unless the patch carries a newer assignment version, so a view
461
+ read before an assignment never rolls the row or the next expected version
462
+ back), so the row follows each newer read of the form and the list is
463
+ **not** re-listed per tick. An item the
464
+ reviewer leaves while its stage is in flight, or before the pane showed the
465
+ result of its last operation (an instruction just recorded), keeps
466
+ following: the workspace re-reads followed items with the same `load` (first
467
+ review page) on the pane's backoff, at most **5** reads per tick, one after
468
+ another, least recently read first (so every followed row is reached), with
469
+ only one tick running at a time. At most **20** items are followed; beyond
470
+ that the longest-followed one (the earliest started, whatever the read
471
+ rotation) stops. An item left while its pane was mid-operation (an Apply not
472
+ yet answered) keeps following while a read still shows an action authorized
473
+ or executing: exactly **3** more reads when it never answers, the fourth
474
+ ending the follow. That debt belongs to the operation, so it survives
475
+ reopening and leaving the item again until a view shows the operation
476
+ answered or the reads run out; leaving mid-operation again starts a full
477
+ debt for the new operation. Each follow has a
478
+ generation: an answer from an earlier follow of the same item (one asked
479
+ before the reviewer reopened or left it again) is dropped, and its failure
480
+ does not count toward the new follow's three. An item stops being followed
481
+ when it settles, its view is denied, three reads in a row throw, its row
482
+ leaves the displayed list (a filter or a refreshed page removed it), the
483
+ list fails closed, or the cap drops it; a denial, the failure stop or the
484
+ cap drops its cached status and restores its entry exactly as its page read
485
+ returned it (never a patched entry). Opening an item drops its cached
486
+ status until the pane shows a view; a denial in the open pane (a failed
487
+ action, the pane's own fail-closed poll, or a first load that failed) does
488
+ the same as a follow denial, clears the announcement, and the item is not
489
+ followed when left. A settled stage that was seen running is announced in a
490
+ polite status region ("badger-po.pdf: Ready to review"), only while its row
491
+ is displayed; a refreshed page or a failed list clears it.
492
+ Other rows update on Refresh, a filter change or when opened; there is no
493
+ streaming. While a refresh or "Load more" is pending, the displayed rows stay
494
+ (controls disabled) and keep taking patches. Every read (a page, a pane
495
+ view, including an unchanged poll, and a follow read) is numbered when its
496
+ request starts, and each row shows the entry of its newest read, whatever
497
+ the source: a page read before a pane or follow read never regresses the
498
+ row, and a page read after them replaces what they showed. The same rule
499
+ governs a follow: a follow read older than the row's newest read changes
500
+ no status, does not stop the follow and announces nothing (a newer read
501
+ settles it), so the announcement never contradicts the row. Cached status reasons follow the same ordering, even when entries have identical
502
+ fingerprints. "Load more" continues the
503
+ filter the rows were listed under, not filter inputs edited since, and does
504
+ not repeat a row already shown. A failed "Load more" fails closed like any
505
+ failed read (the host cannot tell a transient failure from a revoked
506
+ grant): the rows clear and the list shows its error.
507
+ - **Approve and advance.** After an approve, apply or "Handled, no action" whose
508
+ reload left nothing for the reviewer (closed without action, or an action
509
+ succeeded and none is waiting for approval, waiting to be applied, executing,
510
+ unknown or failed; stale, rejected and deferred actions do not hold it), the
511
+ item's row is patched and leaves the list when it no longer matches the
512
+ listed state/assignee filter, and the next row that is not closed or
513
+ completed opens and takes focus, with no navigation and no re-listing. An
514
+ approval that leaves an apply step keeps the item open with Apply in place.
515
+ Unloaded saved-review pages and an active background stage also prevent
516
+ settlement. Advancing skips completed inbox pages until a reviewable row
517
+ appears; a page that makes no progress stops the walk.
518
+ It never advances over unsent input on the same item: the item stays open,
519
+ the input stays, and a note says it is finished but has unsent input; the
520
+ reviewer moves on explicitly, through the confirmation; input typed while
521
+ the next page loads counts too. When no open row is displayed, the advance
522
+ appends pages of finished rows until one has an open row or no page
523
+ remains, at most **5** appends. After each append, the advance (like
524
+ J/K at the end of the list) finds its place again by id: just after the
525
+ item it left, or, if that row has gone, at the first row that followed it
526
+ and is still listed, or at the first new row, so a row that left the list
527
+ meanwhile never shifts it onto the wrong item. Its notes (and "nothing else
528
+ needs review") are written into one persistent polite status region. The queue is never reported empty when
529
+ that is unknown: past the bound, or when an append cannot start (a refresh
530
+ is pending) or is superseded, the item stays open with a note to try Next
531
+ item again, which still works; when an append fails, the list has failed
532
+ closed (no rows, Next disabled), so the item stays open with a note to
533
+ refresh the list to continue. The open item stays listed while the
534
+ reviewer works on it even if its state stops matching the filter; it
535
+ leaves once they move on.
536
+ When nothing is left the pane says so. The per-row "Handled, no action"
537
+ control is hidden in the workspace (`resolveInRow={false}`) so closing always
538
+ goes through the item pane and advances.
539
+
540
+
286
541
  ## Maintained browser proof
287
542
 
288
543
  `pnpm test:components` runs real Svelte component tests; `pnpm test:e2e` runs the
@@ -323,8 +578,13 @@ Import `createIntakeReviewHost` and `IntakeReviewHostOptions` from
323
578
  options)` with a service constructed from the current authenticated session.
324
579
  The returned `IntakeReviewHost` implements inbox state projection across saved
325
580
  review pages, current generation loading, stable `ui:<index>` intention keys,
326
- action/plan preview CAS lookup, decisions, apply, corrections, candidates, split
327
- and feedback. Repeated preparation of an unchanged authorized suggestion retains
581
+ action/plan preview CAS lookup, decisions, apply, corrections, candidates, split,
582
+ feedback, the offered `catalog`, `startAction` and `resolve`. Any catalog
583
+ failure (not configured, bound exceeded, denied) omits `catalog`, so the picker
584
+ disappears while existing reviews stay reviewable; item, review and evidence
585
+ denials still propagate as before; the reference host (`reference/review-host.ts`)
586
+ degrades the same way. Repeated
587
+ preparation of an unchanged authorized suggestion retains
328
588
  its current review instead of creating a new revision. Changed arguments still
329
589
  require explicit re-preview and a fresh human decision.
330
590
  `list({ state?, assigneeId?, cursor?, limit? })` returns one page newest receipt
@@ -518,3 +778,11 @@ choosing the same local file explicitly starts a new upload identity. Opening
518
778
  then dismissing the chooser preserves the separately retained originals and
519
779
  request identity for retry; it does not clear the shared FilePicker's native-form
520
780
  behavior. Successful upload and host/context disposal still discard retry state.
781
+
782
+ Workspace ordering also protects an open item during an awaited automatic page walk:
783
+ the selected row remains until navigation succeeds, except that a failed list read
784
+ clears the inbox. Applying an action is disabled
785
+ while that action card holds unsent arguments, reason, or feedback comment. A list
786
+ refresh retires earlier follow snapshots; only follow reads started after that
787
+ page request may override its rows, so a newer refresh cannot be regressed by an
788
+ already settled or older in-flight follow read.
@@ -37,6 +37,34 @@ requires human review; machine authorization requires explicit evaluated opt-ins
37
37
  at all three authority layers and remains separate provenance from human review.
38
38
  The reference handlers always require review; no evaluation claim is made here.
39
39
 
40
+ `operation: 'review_own'` is the submitter-scoped decision gate (#4085). The
41
+ service accepts it only when the host's `authorize` result carries
42
+ `submitter: true`, the resolved policy's `submitterReview` is enabled (the
43
+ application layer opts in; tenant/source layers may only set it `false`), and
44
+ the item's server-recorded `submitter_id` (the receiving service's authenticated
45
+ `scope.actorId`) equals the current actor and the captured
46
+ `execution.principalId`; apply rechecks that a `review_own` decision's reviewer
47
+ is that submitter and principal. All other `review` checks apply
48
+ unchanged; `correct` is refused. The decision row records `mode` and
49
+ `selfDecision` (decider is the submitter, in either mode), and `feedbackEvent` copies them as `decisionMode`/`selfDecision`;
50
+ `authorizeAutomatic` never reads human decisions, so a self-decision cannot
51
+ become automation evidence. `listOwnProposals` reads with the ordinary `read`
52
+ operation per handler after an item-level `read` check. Unreadable items are
53
+ skipped before projection (1000 hidden rows per call, then `Own proposal scan
54
+ limit`) and the cursor names only emitted items with a confirmed readable
55
+ successor. On this surface every host `assertTarget` throw is mapped to the
56
+ exported `IntakeAccessDeniedError` (message `Intake unavailable`, the type of
57
+ every execution denial), so the item becomes an `unavailable` entry and host
58
+ error text is not returned; hosts need no change. Other errors propagate,
59
+ including a throwing grant store in `authorize`. The feedback anti-widening
60
+ check also refuses a re-evaluation that turns `submitterReview` on.
61
+ Deletion redaction blanks `submitter_id` and `owner_key` with the payloads and
62
+ drops `execution.principalId` unless an external action is still `executing`
63
+ or `outcome_unknown`; `finish` and the reconcile state update release it once
64
+ none is left, and later deletion sweeps do the same.
65
+ Hosts whose `authorize` switches exhaustively on the operation fail closed for
66
+ the new value until they opt in.
67
+
40
68
  `mutationBoundary: 'serialized'` is an explicit host guarantee, not an inferred
41
69
  property of a permissions array: grant revocation must serialize with the supplied
42
70
  transaction, or the owning mutation must revalidate under its own lock. The host
@@ -47,6 +75,50 @@ row lock before public `Contents` reads. A current `PrincipalRun` intersects liv
47
75
  permissions with the captured ceiling and checks the exact registered collection
48
76
  operation at every effect. No `runOnce` atomicity is assumed.
49
77
 
78
+ ## Reviewer-chosen actions and dispositions
79
+
80
+ `startAction(StartActionInput)` lets an authenticated reviewer begin any
81
+ operation handler the item's evidence media types offer, without a model
82
+ suggestion. It checks `review` access under that handler before any callback,
83
+ refuses plan handlers and resolved items, binds the current analysis revision's
84
+ completed/partial attempt, inserts an action keyed `reviewer:<requestId>` with
85
+ `data.origin = 'reviewer-selected'`, and reuses `buildBinding`/`publish`: schema
86
+ validation, `validate`, `preview`, exact normalized arguments, target locks,
87
+ evidence pins and the request ledger behave exactly as for `previewProposal`.
88
+ Replays re-preview against the original revision and return the same review.
89
+ Approval, apply, retention, redaction and reconciliation are unchanged.
90
+ `authorizeAutomatic` refuses `reviewer-selected` actions and resolved items;
91
+ human routing is never machine authority.
92
+
93
+ `resolveItem(ResolveItemInput)` appends one terminal `intake_dispositions` row
94
+ (`disposition = 'resolved_no_action'`, reason/reviewer in redactable `data`)
95
+ under the item lock. Reviewer authority is taken from the first handler the
96
+ captured ceiling and policy offer; no new grant exists. It refuses while any
97
+ action is `authorized` with an unexpired approval for its current review
98
+ version, `executing`, `outcome_unknown` or `succeeded`; `failed` actions and
99
+ lapsed approvals do not block it (neither can be rejected). Afterwards
100
+ `submitDecision` refuses everything but `reject`, `publish` refuses any new
101
+ revision (correction, re-preview, plan preview), the apply gate (`approved`)
102
+ refuses any execution or retry, and `IngestionService.analyze`,
103
+ `reviseLogicalSplit` and `createAction` refuse the item under its lock before
104
+ any write. `createAction` (process authority) also refuses the reserved
105
+ `reviewer:` key prefix and `reviewer-selected` origin, which only
106
+ `startReviewerAction` mints; `previewProposal` on such an action requires
107
+ `review` access and the current handler@version. `resolveItem` supersedes
108
+ queued/running analyses and completes dispatch intents under the item lock, and
109
+ `claimAnalysis` returns `null` for a resolved item; `publishProposal` refuses
110
+ a resolved item and any reviewer-selected action. `[feedbackActionTransaction]`
111
+ takes `targetRevisions` (default true); routing lineage re-checks pass false. The feedback context's
112
+ `settled` flag gates selection only; lineage re-checks ignore it (FEEDBACK.md). `intake_dispositions` is a new model: deploy
113
+ its manifest migration (`pnpm ... db:migrate`) before upgrading hosts;
114
+ `assertReady` checks it, and both privacy deletion and retention expiry
115
+ (`redactExecution`) redact its payload with the other item-owned tables.
116
+ `IntakeItemDTO.disposition` is `{ state: 'resolved', at }` (row `created_at`,
117
+ never redacted); `getItemDisposition` returns reviewer and reason under
118
+ `reviewerAccess`. `resolveItem` captures an optional `reviewer_routing` feedback projection in the
119
+ same transaction; a reviewer-selected action is captured when its approval is
120
+ recorded (FEEDBACK.md).
121
+
50
122
  ## Durable review and effects
51
123
 
52
124
  Preview appends a proposal bound to exact analysis attempt/evidence, handler,