diff --git a/SCOPE.md b/SCOPE.md index b2789bf..5efb3dc 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -24,14 +24,26 @@ Core counts match; prior 24 confirmed submissions were preserved. Human acceptance of source revision 2 is confirmed at 2026-10-04T22:17:19Z, with an exact presentation/disposition link and independent audit reconciliation. -Broader binding -profiles and browser acceptance remain live tasks in INFD-WP-0007; intake +Broader binding profiles and browser discovery remain live tasks in INFD-WP-0007; intake initial delivery is completed in INFD-WP-0005. Issuer rotation is separately governed under Platform RPF-IN-0004. Automatic Hub resolution write-back remains disabled. Release/cutover procedure: `deploy/lifecycle/README.md`. Native receipts: `docs/evidence/2026-10-05-lifecycle-cutover/`. Earlier statuses below are historical. +## Lifecycle authoring source increment — 2026-10-05 + +Implemented but not yet deployed: browser approval/discussion drafts, immutable +supporting text documents with required acknowledgments, document/revision +comparison, and retained review-request downloads. The private API can retrieve +an exact revision's handoff under the existing events permission. These adapters +use the headless service; request preparation creates no native approval. + +Validation: 669 tests passed (one skipped), followed by 50 focused checks and +16 Chromium checks against actual owner components with synthetic identity. +Evidence: `docs/evidence/2026-10-05-lifecycle-authoring/`. Attribute-based legacy +review discovery and broader binding contracts remain open in INFD-WP-0007. + ## Status — 2026-09-27 **Deployed and in live use.** `decisions.coulomb.social` serves diff --git a/docs/evidence/2026-10-05-lifecycle-authoring/README.md b/docs/evidence/2026-10-05-lifecycle-authoring/README.md new file mode 100644 index 0000000..d928dc7 --- /dev/null +++ b/docs/evidence/2026-10-05-lifecycle-authoring/README.md @@ -0,0 +1,28 @@ +# Lifecycle authoring validation — 2026-10-05 + +Source implementation evidence; this increment has not been deployed. + +The browser can prepare approval or discussion drafts, append immutable text +supporting documents and required acknowledgments, compare revisions, publish, +and download the retained review request for the admitted State Hub workload. +The shared service also exposes exact-version handoff retrieval under the +existing lifecycle events permission. Preparation does not create an approval; +only a human can complete a disposition through the existing review surface. + +Validation: + +- Locked project environment, actual owner Flex Auth, Approval Engine, Audit Core + and State Hub sources: 669 passed, 1 skipped. +- Final focused lifecycle UI/review/HTTP regression run: 50 passed, 2 skipped + (optional real-engine checks excluded from this focused run). +- Chromium: 16 checks passed using real HTTPS and the actual lifecycle policy + package, with synthetic identity and TokenReview. This is fixture evidence, + not another native acceptance claim. +- Desktop and mobile screenshots inspected; no viewport overflow observed. +- Coverage includes replay, restart, stale revision refusal, protected export, + CSRF refusal, rejected documents without partial writes, and no approval entry + created by review-request preparation. + +The browser receipt includes the existing fixture human review's single entry; +the new lifecycle preparation adds no entry. Fixture databases, TLS keys and +session credentials are deliberately excluded from this directory. diff --git a/docs/evidence/2026-10-05-lifecycle-authoring/browser-result.json b/docs/evidence/2026-10-05-lifecycle-authoring/browser-result.json new file mode 100644 index 0000000..8a432c4 --- /dev/null +++ b/docs/evidence/2026-10-05-lifecycle-authoring/browser-result.json @@ -0,0 +1,154 @@ +{ + "fixture_only": true, + "browser": "Chromium", + "checks_passed": 16, + "checks": [ + "PKCE callback and secure browser session", + "Home overview lists the open memo addressed to the reviewer", + "Entitled page escapes memo and packet markup", + "Server refuses bypass of missing acknowledgments", + "Explicit acknowledgment unlocks the form", + "Complete attachment download stays behind entitlement", + "Return records a memo response without an engine entry", + "Mobile review fits the viewport", + "One actual approval entry, stable on browser reload", + "Audit receiver holds all commitments; unsigned decision gap retained", + "Browser creates an approval draft and immutable supporting document revision", + "Browser publication and durable handoff download use the actual lifecycle PDP", + "Prepared request creates no approval or human entry; lifecycle audit is delivered", + "Real workload caller refusal hides review content", + "Sign-out clears the session; all forms carry the exact origin", + "No page errors or injected script execution" + ], + "fixture_transport": "real HTTPS on ephemeral ports; synthetic issuer and TokenReview", + "native_human_login_proven": false, + "native_policy_admitted": false, + "factory_attempts": 0, + "paid_model_calls": 0, + "final_component_state": { + "entries": 1, + "intents": [ + { + "disposition_id": "disp-2e416ff7-a0b8-4e6a-bf5f-fd90e65f9d18", + "approval_id": "fixture", + "subject": "human-fixture", + "state": "confirmed", + "attempt": "a693dad4-6d79-4ab3-b177-9a93de7bbdb9", + "approved_at": "2026-10-04T22:44:43+00:00", + "presentation_id": "pres-af92dcbd-51e7-4089-ba1e-76fdffcd9e8c", + "operation_id": "cc819583-73a7-4f24-8723-2aa4aaeb4280" + } + ], + "outbox_states": [ + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered", + "delivered" + ], + "policy_outcomes": [ + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "allow", + "caller_refused" + ], + "decision_attributable": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "entry_posts": 1, + "consume_calls": 0, + "tokenreviews": 15 + } +} diff --git a/docs/evidence/2026-10-05-lifecycle-authoring/lifecycle-desktop.png b/docs/evidence/2026-10-05-lifecycle-authoring/lifecycle-desktop.png new file mode 100644 index 0000000..0cbfb4f Binary files /dev/null and b/docs/evidence/2026-10-05-lifecycle-authoring/lifecycle-desktop.png differ diff --git a/docs/evidence/2026-10-05-lifecycle-authoring/lifecycle-mobile.png b/docs/evidence/2026-10-05-lifecycle-authoring/lifecycle-mobile.png new file mode 100644 index 0000000..158ff9d Binary files /dev/null and b/docs/evidence/2026-10-05-lifecycle-authoring/lifecycle-mobile.png differ diff --git a/docs/specs/DecisionLifecycleService.md b/docs/specs/DecisionLifecycleService.md index 68c8c1f..8552c8f 100644 --- a/docs/specs/DecisionLifecycleService.md +++ b/docs/specs/DecisionLifecycleService.md @@ -1,7 +1,9 @@ # Decision lifecycle service -Status: product direction requested by the founder on 2026-10-04; architecture -and API below are proposed delivery contracts, not shipped endpoints. +Status: initial audience deployed and human-accepted on 2026-10-05. +This is the product contract, including wider capabilities not yet delivered. +LifecycleHTTPContract.md names the implemented routes and source-only additions; +SCOPE.md and the workplan distinguish current delivery from remaining work. Workplan: `INFD-WP-0007`; intake prerequisite: `INFD-WP-0005`. ## Outcome and boundaries diff --git a/docs/specs/LifecycleHTTPContract.md b/docs/specs/LifecycleHTTPContract.md index 30ed4c6..88554ec 100644 --- a/docs/specs/LifecycleHTTPContract.md +++ b/docs/specs/LifecycleHTTPContract.md @@ -1,8 +1,9 @@ # Lifecycle HTTP adapter — registration, revisions and collaboration -2026-10-04; INFD-WP-0007-T04, in progress. `LifecycleAPI` is an unmounted WSGI -adapter over `LifecycleService`. It starts no listener and adds no route to the -existing browser app or public ingress. +2026-10-05; INFD-WP-0007. `LifecycleAPI` is a WSGI adapter over +`LifecycleService`, mounted on the admitted private 8081 listener. It adds no +route to public ingress. The direct retained-handoff GET is a source addition +awaiting a separate release; the other routes below are deployed. | Method and path | Input | Result | | --- | --- | --- | @@ -11,6 +12,7 @@ existing browser app or public ingress. | `POST /v1/decisions` | `memo`, optional `documents` | Register an unbound version-one memo and return event/record/revision/resource receipt | | `POST /v1/decisions/{id}/revisions` | `memo`, `expected_version`, `summary`, optional `documents` | Compare-and-append successor revision; same receipt shape | | `POST /v1/decisions/{id}/review-requests` | `version`, `options` per LifecycleReviewHandoff | Durable request receipt and exact intake envelope | +| `GET /v1/decisions/{id}/review-requests` | Required positive `version` | Exact retained request receipt/envelope, checked as `lifecycle.events`; no submission | | `GET /v1/decisions/{id}/outcomes` | Required positive `version` | Correlated review/disposition evidence and submission state | | `GET /v1/decisions/{id}` | Optional positive integer `version` | Existing Memo JSON for the current or specified revision | | `GET /v1/decisions/{id}/events` | Optional `after` sequence cursor, `limit` 1–100 | `{"events": [...]}` ordered by sequence; use last returned sequence for next page | @@ -28,8 +30,8 @@ revision numbers and truncated bodies are rejected. Disposition routes are not e Authentication runs before routing or reading a mutation body. The composition must explicitly supply a bearer-token verifier returning an authenticated `Caller`. There is no default or testing fallback. Cookies are not credentials -for this adapter; its future human browser integration must provide a separately -reviewed session/CSRF boundary or admitted bearer identity. Neither the intake SA +for this adapter; the browser workspace uses the separate +verified session/CSRF adapter. Neither the intake SA audience nor the browser's approval-engine access token is implicitly admitted. Every handled command invokes the shared policy-consuming service. Clients cannot @@ -44,13 +46,12 @@ identity, 403 policy denial, 404 unavailable record/route, 405 unsupported metho service or unusable policy evidence. Raw exception text and credentials are never returned. Upstream raw diagnostics are not forwarded. -T04 remains in progress: admitted workload authenticators and API listener -composition. The browser workspace uses verified sessions through a separate -adapter (see LifecycleWorkspaceDelivery.md). Current -tests use the real service/repository/PolicyClient with synthetic authenticator -and PDP transport; they establish adapter behavior, not live identity admission. -Runtime must not expose this adapter before lifecycle policy and independent -event-delivery contracts are accepted and readiness can be satisfied. +The deployed runtime authenticates the selected State Hub service account using +Kubernetes TokenReview and the lifecycle audience. See LifecycleProductionRuntime.md. +Human workspace forms use verified KeyCape sessions through a separate adapter. +Native admission, negative identity tests and initial human acceptance are +recorded in ../evidence/2026-10-05-lifecycle-cutover/. New clients still require +owner-reviewed admission; this contract does not widen identity or listeners. ## Memo commands and packet custody @@ -72,7 +73,7 @@ Idempotency-Key. Register/revise receipts include a relative API resource link `/v1/decisions/{id}?version={version}` and a `workspace_url` at `https://decisions.coulomb.social/decisions/{id}?version={version}`. The latter is an authorized authoring workspace, not a binding review link. The API remains -unmounted. Search cursors are caller/tenant/query scoped, expire after five +private. Search cursors are caller/tenant/query scoped, expire after five minutes and are invalidated on process restart. Every page rechecks policy. Each document has exactly `digest`, `media_type`, `content_base64`. At most 100 diff --git a/docs/specs/LifecycleProductionRuntime.md b/docs/specs/LifecycleProductionRuntime.md index 8adffbe..9efd08a 100644 --- a/docs/specs/LifecycleProductionRuntime.md +++ b/docs/specs/LifecycleProductionRuntime.md @@ -1,8 +1,9 @@ -# Initial production runtime candidate +# Initial production runtime INFD-WP-0007 / INFD-WP-0005. Founder selected net-kingdom-admins humans and -system:serviceaccount:state-hub:state-hub on 2026-10-04. This is a preparation -contract; deployment still requires the concrete rollout approval. +system:serviceaccount:state-hub:state-hub on 2026-10-04. The concrete rollout was approved and accepted on 2026-10-05. +This describes the deployed initial runtime; later artifacts require their own +release verification. ## Identity and listeners @@ -28,11 +29,11 @@ package, version and digest from human review. Only humans can bind via the existing protected review flow. Access Engine keeps the authorization decision; Approval Engine keeps authoritative approval state. -## Initial proposal data +## Admitted initial data `deploy/lifecycle/lifecycle-callers.json` names the exact State Hub account and platform tenant. The intake registry permits only decision-record class and -net-kingdom-admins, with a proposed cap of 50 new attempts per UTC day. Replays +net-kingdom-admins, with a cap of 50 new attempts per UTC day. Replays do not spend a second attempt; uncertain creation never automatically reposts. The cap and routing are configuration, not per-decision deployments. @@ -64,7 +65,7 @@ forwarding route. Queueing is trusted workload execution. New review envelopes are data operations and do not change deployments. The exact lifecycle snapshot id/key is preserved without inventing a Hub relationship when serves is absent. Lost replies replay the same envelope; native-create uncertainty blocks the row. -Native deployment/acceptance still remains open. Notification/write-back receiver +Native deployment and human acceptance passed. Notification/write-back receiver admission is unchanged; INFD_HUB_WRITEBACK remains off. The image migrates schema 2 to schema 5 on open. Existing records, presentations, @@ -74,16 +75,16 @@ new evidence, and restore a verified pre-cutover copy on a separate private volu only under the reviewed recovery plan. An in-flight/unresolved human submission must be reconciled, never retried or discarded to make rollback convenient. -## Remaining live gates +## Observed acceptance and remaining scope -- CCR-2026-0032 requester projection: current namespace has only the audit - ExternalSecret; requester custody, native identity/path refusals and rotation - evidence remain with RPF-WP-0051. -- FLEX-WP-0033: publish scanned policy artifact; approve exact policy/RBAC/network - objects and native caller/subject allow/refusal tests. -- INFD-WP-0005 / STATE-WP-0096: activate and verify the prepared private State Hub - dispatcher and its projected tokens; retain gated resolution write-back. -- INFD-WP-0007-T06/T07: backup/restore, exact rollout approval, native human review - and independent audit receipts across two decisions and a revision with unchanged - release/configuration pins. Browser packet authoring and broader binding profiles - remain on T05; the supported review handoff profile is documented separately. +CCR-2026-0032 delivery is applied: actual receiving mount/exchange and bounded +identity/path/scope/revocation tests passed. FLEX-WP-0033 and STATE-WP-0096 +completed native deployment and human acceptance. INFD-WP-0007-T06/T07 retain +backup/restore, caller isolation, unchanged pins across two records/a revision, +restart retention and independent audit proof. See +../evidence/2026-10-05-lifecycle-cutover/acceptance-summary.json. + +Issuer rotation remains separately governed under Platform RPF-IN-0004. +Browser authoring continuation is T05; new source features do not imply a +production update. Wider identity/binding/publication and optional Hub write-back +contracts remain explicit T02 work. No new caller, group or listener is admitted. diff --git a/docs/specs/LifecycleReviewHandoff.md b/docs/specs/LifecycleReviewHandoff.md index f27ab2a..fb6d5f5 100644 --- a/docs/specs/LifecycleReviewHandoff.md +++ b/docs/specs/LifecycleReviewHandoff.md @@ -1,6 +1,8 @@ # Revision-specific lifecycle review handoff -2026-10-04; INFD-WP-0007-T02 through T06. Source implementation, not deployed. +2026-10-05; INFD-WP-0007-T02/T05. The initial handoff is deployed and +human-accepted; browser document authoring and direct handoff retrieval below +are the next source increment, not yet deployed. ## Headless contract @@ -22,8 +24,9 @@ The envelope is also retained in authorized event history for recovery. The handoff currently supports the platform tenant, English organizational `approve` memos at the current renderer release, and the intake's nonempty UTF-8 packet profile. Unsupported projections are refused rather than dropping fields. -Broader L0/L2 profiles, browser packet authoring and automated forwarding remain -live work in T02/T04/T05 and WP-0005. There is no automatic submitter or default +The admitted State Hub dispatcher performs explicit forwarding. Browser packet +authoring and handoff preparation now have a source implementation; broader +L0/L2 binding and automatic discovery remain separate contracts. There is no automatic submitter or default workload credential. A caller-selected submitter is a routing request, not identity. The local use of intake's shape validator does not grant admission: intake always reauthenticates the workload and reloads its actual grant and recipient mapping. @@ -75,8 +78,8 @@ outboxes and receipt requirements. The integration test creates an approval via the real Approval Engine API, records a signed-fixture human entry, retrieves the confirmed result for source revision two, and delivers lifecycle and review commitments to the real Audit Core receiver. Identity and PDP transport are -synthetic. Production grants, private listener/forwarding composition, full -release/rollback evidence and separately approved rollout remain outstanding. +synthetic. Native initial-audience grants, forwarding, human acceptance and +restart/audit evidence are recorded in ../evidence/2026-10-05-lifecycle-cutover/. ## Migration and rollback boundary @@ -88,3 +91,37 @@ retain the normal database backup and image/config pins. An older-image rollback requires a compatible restored database plus reconciliation of any subsequent engine side effects; it cannot safely discard new review/entry evidence. A reviewed forward fix remains preferable once new handoffs have been written. + +## Browser authoring and retained handoff retrieval + +New browser drafts explicitly select organizational human approval or discussion +only. Existing rendered forms without that field retain the earlier draft type; +there is no silent conversion of existing records. Discussion drafts cannot +enter the approval handoff. The shared structural profile check is descriptive, +not an authorization grant. + +A text document is appended through the shared revise command with its actual +UTF-8 bytes, digest and optional required highlight. The complete form is limited +to 64 KiB by the existing adapter; large/binary packets remain available through +the bounded API. Previous packets and highlights are preserved, and a new +revision requires new publication. Comparison shows changed documents/digests +and acknowledgment text as well as the question and brief. + +The browser prepares the same immutable request with an explicit approval purpose +and responsible principal. Its initial routing is State Hub / decision-record / +net-kingdom-admins. This route is checked by the existing PDP, and intake still +authenticates the actual submitter and assigns the configured named recipient. +Preparation alone does not enqueue, create an approval, notify or bind. + +`GET /v1/decisions/{id}/review-requests?version=N` returns the exact retained +`event_id`, `review_memo_id` and `envelope`. It requires a fresh `lifecycle.events` +decision for that revision, preserving the permission already required to read +the envelope in history. Missing requests return 404; invalid/nonpositive versions +return 400. Reads do not create requests or repeat external effects. Historical +exports remain historical: intake refuses a stale source revision. + +The browser equivalent `/decisions/{id}/review-request?version=N` downloads +only the envelope as an attachment. It uses the same service check, without +credential copying or a public privileged forwarding API. A prepared current +request links to this download; a loaded request links to protected human review. +The original idempotency identity survives download, retry and process restart. diff --git a/docs/specs/LifecycleServiceContract.md b/docs/specs/LifecycleServiceContract.md index 00ddea5..86d5473 100644 --- a/docs/specs/LifecycleServiceContract.md +++ b/docs/specs/LifecycleServiceContract.md @@ -3,7 +3,7 @@ 2026-10-04; `INFD-WP-0007-T02/T03`. Implemented in `informed_decision/lifecycle_service.py`, above `LifecycleRepository`. This is a headless Python application interface. An optional runtime composition -enables the browser workspace; production admission is still outstanding. +enables the browser workspace; the initial audience is deployed and accepted. ## Caller and authority @@ -21,7 +21,7 @@ The existing intake TokenReview audience is not silently reused for this service Non-cluster/workload composition remains T02/T04 work. The service consumes `PolicyClient` observations. It neither evaluates policy -nor falls back to local permissions. New proposed action names are +nor falls back to local permissions. The admitted action names are `lifecycle.register`, `lifecycle.revise`, `lifecycle.source`, `lifecycle.evaluation`, `lifecycle.comment`, `lifecycle.read`, `lifecycle.events`, `lifecycle.publish`, `lifecycle.search`, `lifecycle.packet`, @@ -93,8 +93,8 @@ Publication appends an immutable event for the current revision with an explicit nonempty audience (at most 32 subjects and 32 groups, no wildcard). A successor revision does not inherit publication. Audience facts are inputs to Access Engine, never local permissions. Human acceptance requires the separate existing -presentation/disposition path; the source handoff now follows LifecycleReviewHandoff.md. Production forwarding -and broader review profiles remain open. +presentation/disposition path; the source handoff now follows LifecycleReviewHandoff.md. The selected State Hub dispatcher forwards explicit requests; broader review +profiles remain open. Before enabling a listener, T02/T04 must provide: reviewed lifecycle PDP package and action/context fixtures; admitted human/workload identity adapters; closed @@ -108,3 +108,8 @@ Production candidate record facts also include `revision_by`, the immutable acto of the requested revision. This lets its editor retain access to an unpublished successor after refining a published decision. Original authorship remains `registered_by`; both facts participate in the same transaction/read race guard. + +The next source increment adds `review_handoff(caller, memo_id, version)`, a +revision-specific read of the exact immutable request under `lifecycle.events`. +It reuses the event permission and post-read currency checks, adds no grant, and +never forwards or regenerates the envelope. See LifecycleReviewHandoff.md. diff --git a/docs/specs/LifecycleWorkspaceDelivery.md b/docs/specs/LifecycleWorkspaceDelivery.md index 4fff1f6..3a3b378 100644 --- a/docs/specs/LifecycleWorkspaceDelivery.md +++ b/docs/specs/LifecycleWorkspaceDelivery.md @@ -1,6 +1,7 @@ # Lifecycle workspace and delivery -2026-10-04; INFD-WP-0007-T02 through T06, source implementation. +2026-10-05; INFD-WP-0007-T02/T05. Initial workspace deployed; document +authoring and review-request preparation are implemented for the next release. ## Human workspace @@ -10,7 +11,8 @@ sessions establish identity; existing origin and CSRF checks protect forms. Users can search, prepare a draft, record source metadata and evaluations, comment, publish to explicit members/groups, refine, compare consecutive revisions and download authorized packet entries. Source content is not fetched. -Packet uploads currently use the API. All forms receive fresh policy checks; +The next release adds short text documents and optional required highlights +through immutable revisions. Large or binary packets continue to use the API. All forms receive fresh policy checks; rendering a button grants no authority. Publication records an audience for one revision. Access Engine determines access @@ -33,7 +35,8 @@ requires explicit `lifecycle_delivery: true` with that package, and generates exact egress and an owner-review ingress proposal. It references the existing mounted workload policy credential path; this does not authorize that workload for a new package. Existing admission inputs render without lifecycle changes. -The API still requires its own admitted bearer verifier and listener composition. +The deployed private API has its own admitted TokenReview verifier and separate +listener; browser sessions are not bearer credentials there. ## Independent evidence @@ -57,15 +60,21 @@ records, publishes/comments/refines and drains five events with unchanged runtim configuration bytes. Its identity and PDP transport are synthetic. It proves source behavior, not deployed workload authority or all seven acceptance cases. -## Remaining delivery work +## Delivery boundary and continuation -T02/T04 require owner-admitted lifecycle policy actions/context, workload identity -and API ingress. The policy must cover all actions in LifecycleServiceContract, -including collection search and per-record checks. Existing compact-sitting pins -do not supply these grants. WP-0005 retains private intake composition work. +The initial rollout passed native agent-to-human-to-agent acceptance, restart +retention, public/private isolation and Audit Core reconciliation. See +../evidence/2026-10-05-lifecycle-cutover/. Those receipts describe the deployed +804d89fb image; they do not prove the next source increment is live. -The source handoff now integrates human-only acceptance for the admitted intake -profile; T03/T05 retain broader profiles and browser authoring/forwarding work. -T06 requires full acceptance evidence, sender/cadence admission, immutable image, -database backup/migration and rollback verification. T07 requires the separately -approved concrete live rollout. No production pins or resources changed here. +Browser authoring now exposes explicit approval/discussion draft types, document +addition with required acknowledgment, packet/highlight comparisons, and durable +review-request preparation/download. The adapter calls only the shared service; +no store access, local permit or automatic privileged forwarding is introduced. +LifecycleReviewHandoff.md specifies the shared retrieval and profile contract. + +Review overview discovery remains exact-record-only under compact-sitting v12. +The lifecycle workspace discovers authorized records and exposes their outcomes. +A later change to legacy overview discovery needs an Access Engine contract, +not a consumer-side policy bypass. Broader binding profiles, external identities, +source connectors and optional Hub write-back remain T02/T05 work. diff --git a/informed_decision/lifecycle_http.py b/informed_decision/lifecycle_http.py index d97bcff..df7c8be 100644 --- a/informed_decision/lifecycle_http.py +++ b/informed_decision/lifecycle_http.py @@ -99,6 +99,10 @@ class LifecycleAPI: if set(params) != {"version"}: raise ValueError() return 200, self.service.outcomes(caller, memo_id, int(params["version"][0])) + if method == "GET" and resource == "review-requests": + if set(params) != {"version"}: + raise ValueError() + return 200, self.service.review_handoff(caller, memo_id, int(params["version"][0])) kinds = {"comments": "comment", "sources": "source", "evaluations": "evaluation"} if method != "POST" or resource not in {*kinds, "register", "revisions", "publications", "review-requests"}: raise LifecycleError(405, "method_not_allowed") diff --git a/informed_decision/lifecycle_review.py b/informed_decision/lifecycle_review.py index ffbbfc6..27c7898 100644 --- a/informed_decision/lifecycle_review.py +++ b/informed_decision/lifecycle_review.py @@ -10,6 +10,26 @@ from .records import dumps, memo_from from .store import Conflict, EvidenceUnavailable +def supports_review(memo): + """Structural compatibility only; publication and authorization remain required.""" + return (memo.locale == 'en' and memo.binding_level is BindingLevel.ORGANIZATIONAL + and memo.step_kind is StepKind.APPROVE and memo.ui_release == UI_RELEASE) + + +def handoff(repository, tenant, memo_id, version): + if type(version) is not int or version < 1: + raise ValueError('positive revision required') + with repository.store._connection() as db: + repository._record(db, tenant, memo_id) + repository.store._memo(db, memo_id, version) + row = db.execute("SELECT id,body FROM lifecycle_events WHERE tenant=? AND memo_id=? " + "AND version=? AND kind='review_requested'", (tenant, memo_id, version)).fetchone() + if row is None: + raise EvidenceUnavailable('review request unavailable') + body = json.loads(row['body']) + return {'event_id': row['id'], **{k: body[k] for k in ('review_memo_id', 'envelope')}} + + def review_request(db, review_id): row = db.execute("SELECT * FROM lifecycle_events WHERE kind='review_requested' " "AND json_extract(body,'$.review_memo_id')=?", (review_id,)).fetchone() @@ -53,8 +73,7 @@ def prepare(repository, context, memo_id, version, options, *, key, guard): memo = repository.store._memo(db, memo_id) if memo.version != version: raise Conflict('review requires the current revision') - if (memo.locale != 'en' or memo.binding_level is not BindingLevel.ORGANIZATIONAL - or memo.step_kind is not StepKind.APPROVE or memo.ui_release != UI_RELEASE): + if not supports_review(memo): raise ValueError('unsupported human review profile') if repository._policy_context(db, context.tenant, memo_id, version)['published_audience'] is None: raise Conflict('publish this revision before requesting review') diff --git a/informed_decision/lifecycle_service.py b/informed_decision/lifecycle_service.py index b5a34b7..cf3ddbe 100644 --- a/informed_decision/lifecycle_service.py +++ b/informed_decision/lifecycle_service.py @@ -182,6 +182,14 @@ class LifecycleService: current() return result + def review_handoff(self, caller, memo_id, version): + """Retrieve the exact retained event; this never resubmits an approval.""" + from .lifecycle_review import handoff + current, _ = self._authorize(caller, memo_id, "events", {"version": version}) + result = handoff(self.repository, caller.attribution.tenant, memo_id, version) + current() + return result + def search(self, caller, *, query="", cursor=None): self._caller(caller) if not isinstance(query, str) or len(query) > 200: diff --git a/informed_decision/lifecycle_ui.py b/informed_decision/lifecycle_ui.py index 5ed6748..6cf032e 100644 --- a/informed_decision/lifecycle_ui.py +++ b/informed_decision/lifecycle_ui.py @@ -1,13 +1,16 @@ """Human workspace adapter: forms and rendering, with no storage or policy rules.""" from dataclasses import replace +import base64 +import hashlib import re from urllib.parse import urlencode import uuid from .lifecycle_commands import decode_memo from .lifecycle_service import Caller, LifecycleError -from .memo import Memo, BindingLevel, BindingSlice, Principal, Scope, StepKind +from .lifecycle_review import supports_review +from .memo import Memo, BindingLevel, BindingSlice, Principal, Scope, StepKind, PacketItem, Highlight from .records import dumps from .store import EvidenceUnavailable from .ui import document, hidden, text @@ -59,14 +62,23 @@ class LifecycleWorkspace: if method == "GET": fields = (_textarea("question", "Question") + _textarea("brief", "Context, alternatives and recommendation") + '' - '') + '' + '' + '
Saving a draft does not request or record approval.
') content = 'This review belongs to an earlier revision and cannot receive a new answer.
' else: content += 'Review handoff: ' + text(review["intake_state"].replace("_", " ")) + '
' + if review["intake_state"] == "not_submitted": + content += (f'Download review request' + ' for the admitted State Hub agent to submit. Preparing this request has not created an approval.
') for item in outcome["dispositions"]: answer = item["disposition"] content += f'{text(answer["actor"]["sub"])}: {text(answer["verb"])}' @@ -162,6 +208,21 @@ class LifecycleWorkspace: content += ' · approval entry ' + text(item["submission_state"]) content += '
' content += '' + elif supports_review(memo): + content += 'Publish this revision before preparing its review. The initial review group is ' + 'net-kingdom-admins, with the configured platform reviewer as recipient.
' + 'Preparation saves a request for the admitted State Hub agent to submit. ' + 'You can download it afterwards; no human answer is recorded here.
') + content += _form(base + "/request-review", session, revision + + _textarea("principal", "Responsible party or system") + + _textarea("purpose", "Purpose of this approval"), "Prepare review request") + else: + content += 'Add a supporting document, then publish the new revision to prepare human review.
' + content += 'This draft’s review type is not supported by the current human approval flow.
' if previous is not None: content += f'Before
{text(before)}
' f'After
{text(after)}
') + if previous.packet != memo.packet: + content += '{label}
{text(item.hash)}{label}
The document becomes part of a new revision. Earlier documents and answers remain unchanged. ' + 'Publish the new revision before requesting review. This form accepts short text documents; ' + 'larger or binary documents can be supplied by an agent.
') + content += _form(base + "/document", session, revision + _textarea("label", "Document title") + + _textarea("content", "Document text") + + '' + + _textarea("summary", "What changed"), "Save document in new revision") + '