Skip to content

Commit 76165fe

Browse files
authored
RedMesh tenant drafts, generated packs and simplified activation (RM-109, RM-110, RM-112) (#529)
* feat(redmesh): tenant draft record and operations (RM-109 phase 2) What changed: - New `tenant_draft` tenancy record kind (`tenancy/drafts.py`): checklist and coverage vocabulary, `covers` rules, applicability, item states and transitions, effective dates, the stored-record validator, the completeness rule and the draft DTO. Added to the adapter `_KINDS` with a `_validate` branch and a `list_tenant_drafts` enumeration. - Seven POST endpoints, full-portfolio Super-Tenant Admin only: create/update/get/list/delete_tenant_draft and upload/download_tenant_draft_document. The service decides under the administration lock; document reads, writes and deletes run in the plugin outside it. A set activation marker locks update, upload and delete (409 draft_locked). - Draft uploads reuse the contract checks (PDF, 50 MB) and write the `redmesh_tenant_contract` envelope at schema_version 1.1 with `draft_id` and `document_kind`; refusals are contract_invalid / document_invalid by slot. The stored ref keeps the `_valid_contract` key set. A replaced file, or one dropped by setting an item `missing`, is deleted after the row is written. - A draft file gone from the store reads as `missing` and the row is corrected on the next write; delete skips it. Delete removes every file, then the record, and emits the `tenant_draft_deleted` audit event. - Data maintenance: `tenant_draft` in `_TENANCY_KINDS`, current when the store validator passes and old/unrecognized otherwise, decided before the related-tenant check; its files are in the inventory via `items.<kind>.document.ref`. Why: - RM-109 phase 2 of the onboarding-drafts contract: a super-tenant prepares a tenant before its documents are signed. Activation (prepare_tenant with draft_id, marker, release, close) is phase 3; resolve_contract and prepare_tenant are unchanged. Checks: - docker exec rm4 python3 -m unittest test_tenant_drafts: 33 tests OK. - test_tenant_contract, test_data_maintenance, test_tenant_administration, test_tenant_delete, test_tenancy_administration_store, test_authz_surface: 176 tests OK (baseline 157 for the first five, +2 data maintenance). - Full discover of red_mesh/tests: 3470 run; 30 errors are modules importing pytest (not installed in the container), 1 timing failure in test_postponed_analyze_native_ipc that passes when run alone. * fix(redmesh): tenant draft answers never read or correct files (RM-109 phase 2) What changed: - Removed the document-store check from every draft answer: get, create, update and upload build the DTO from the stored row only, and no write changes an item because a file could not be read (the gone_refs path and drafts.without_documents are gone). - A file that cannot be read is detected only where it is used: download answers 404 not_found from a single read (read_contract takes an `absent` answer), delete skips it. - A failed delete of an unreferenced file (replaced slot, item set missing, rollback of a refused attach) is logged at error level with draft id, slot and ref; the write stands. - The draft validator also requires a stored document's mime to be application/pdf. - AGENTS.md entry dated to the commit date and reworded for the rule. Why: - R1fsDocumentStore.get answers a timeout as no file, so the previous check persisted a transient failure as `missing`, and it read every file in full on each answer. Review of 71b5c17; contract §Missing files is revised to this rule. Checks: - docker exec rm4 python3 -m unittest test_tenant_drafts: 36 tests OK (new: unreadable file never changes the row, answers need no store, gone file 404/skip, delete-failure log, non-PDF stored ref fails closed). - test_tenant_contract, test_data_maintenance, test_tenant_administration, test_tenant_delete, test_tenancy_administration_store, test_authz_surface: 176 tests OK. - Full discover of red_mesh/tests: 30 errors, all modules importing pytest (not installed in the container); no other failure. * feat(redmesh): tenant draft activation, release and close (RM-109 phase 3) What changed: - prepare_tenant takes an optional draft_id. The plugin reads the draft and re-resolves its files outside the lock, each envelope bound to the draft and its slot (resolve_draft_document); caller creation arguments are ignored. Locked step: draft_changed, marker (activation_in_progress with holder), draft_incomplete with missing[], existing checks, then on first preparation the uploader rule and contract_in_use; the activation marker is written before the receipt (clearing last_release). - Both paths: the uploader of every document must hold the full-portfolio Super-Tenant Admin role through the identity port (no longer the creator); a replay is still bound by the receipt's sha256. contract_in_use scans the raw tenant and receipt rows (503 past the namespace cap). The one-step path refuses a draft envelope (contract_invalid). - Receipt and tenant gain compliance_types, framework_agreement, data_handling and governance {coverage, applicability, effective}, bound like contract (receipt also draft_id); one-step tenants are unchanged. get_tenant_contract returns them, new download_tenant_document serves any of the three, and tenant delete removes all three files. - release_tenant_draft_activation (marker's receipt; tenant_active; deletes the inactive tenant row, the bound domain reservation and the receipt, no file; last_release written first so every crash point resumes; retry answers last_release; not_activating) and close_tenant_draft (activation_not_complete; deletes the record only). Draft answers carry activation.tenant_state. Audit event tenant_draft_activation_released. - Tests: contract_terms() mints a fresh reference per call and install_contract takes one, as R1FS gives each upload its own CID. Why: - RM-109 phase 3 of the onboarding-drafts contract: activation of a tenant draft into the existing tenant creation, with recovery of a stuck activation. Checks: - rm4 unittest: test_tenant_drafts 54 OK; test_tenant_contract, test_data_maintenance, test_tenant_administration, test_tenant_delete, test_tenancy_administration_store, test_authz_surface, test_tenant_drafts: 239 OK. - rm4 full discover of red_mesh/tests: 3496 run, 30 errors, all modules importing pytest (absent in the container); those 30 modules plus the two tests importing them pass under the repo .venv pytest (2775 + 2). * fix(redmesh): draft release edge cases and refusal shape (RM-109 phase 3) What changed: - A tenant marked `deleting` refuses a draft release with the existing 409 tenant_deleting instead of tenant_active. - A release resumed after its receipt was already removed (last_release names the marker's request) clears the marker as a replay, so it emits no second audit event. - A refusal's details are merged before the fixed keys, so they can never override success/status/status_code/error. - `_draft_answer` documents that one corrupt receipt fails the drafts list closed (503) on purpose; the drafts module docstring describes phase 3. - Tests: the release-after-tenant-delete case drives the real delete_tenant, plus a delete that stops between the receipt and the tenant-row delete; tenant_deleting; replayed release without an event; refusal shape; raw_rows failing closed on an undecodable row. Why: - Review of 48063b9. Note: that commit's body says the targeted run was 239 tests; it was 235. Checks: - rm4 unittest: test_tenant_drafts 58 OK; with test_tenant_contract, test_data_maintenance, test_tenant_administration, test_tenant_delete, test_tenancy_administration_store and test_authz_surface: 239 OK. - rm4 full discover of red_mesh/tests: 3500 run, 30 errors (modules importing pytest, absent in the container), 1 timing failure in test_postponed_analyze_native_ipc that passes when run alone. * fix(redmesh): audit a draft release once, when its marker is cleared What changed: - release_tenant_draft_activation reports whether this call cleared the marker (`cleared`) instead of `replayed`; the plugin emits tenant_draft_activation_released on that call only. A release resumed after its receipt delete emits the event when it clears the marker, with rows_deleted counting only that call's deletes; a call that finds no marker and answers last_release emits nothing. The answer is unchanged. Why: - Every release passes the marker clear exactly once, so the event is emitted once per release, including one that crashed part-way (18ae0db left such a release unaudited). Checks: - rm4 unittest: test_tenant_drafts 58 OK (crash after the receipt delete: the finishing call emits exactly one event, a further call none); with test_tenant_contract, test_data_maintenance, test_tenant_administration, test_tenant_delete, test_tenancy_administration_store and test_authz_surface: 239 OK. * feat(redmesh): engagement drafts inside the tenant draft (RM-109 phase 4) What changed: - New `engagement_draft` record kind (`tenancy/engagement_drafts.py`, `ted_<uuid>`, `parent` = `{draft_id}` or `{tenant_id}`), added to the adapter `_KINDS`/`_validate` and to data maintenance with the parent rule (`current` with a live parent, `orphan/parent_gone` otherwise, never `old_tenant`; files via `document.document.ref` and `document.generated.ref`). - Eight STA-only endpoints: create/update/get/list/delete engagement draft, pack upload and download (`redmesh_tenant_contract` 1.1 envelope with `document_kind: engagement_pack` and `engagement_draft_id`), and `activate_engagement_draft` as one locked call: `parent_not_active`, replay once `en_<uuid>` exists (whoever calls), completeness with `missing[]`/`reasons`, the existing engagement body with the signed pack as `agreement` and the generated pack as a second `other` document, pack uploaders checked for the platform role, `contract_in_use` over engagement `documents[]`, row deleted last. - Every engagement draft write is `draft_locked` while the parent tenant draft's marker is set. - `read_engagement_document` accepts the 1.1 pack envelope. - `close_tenant_draft` re-homes the children before deleting the row; `delete_tenant_draft` cascades (children's files, rows, then its own) and answers `engagement_drafts_deleted`. - Collapsed tenant checklist: `contract` covers both tenant records, applicability `required` and no longer decides completeness, `items_total` counts the shown item, `generated` blocks validated on items and the pack slot and kept across `missing`/`awaiting_signature`. - `ted_` ids answer 404 as tenant and as engagement ids. Why: - Contract `onboarding-drafts.md` §Engagement draft and §Checklist (owner, 2026-10-07): the whole pack is prepared inside the tenant draft before the client signs anything; each engagement activates on its own signed pack. Checks: - rm4: focused suites (engagement drafts, tenant drafts, engagements, data maintenance, authz surface, tenant contract/administration/delete) 281 tests OK. - host .venv pytest, full red_mesh tests: 6291 passed, 5 skipped, 3448 subtests. * fix(redmesh): engagement draft review fixes (RM-109 phase 4) What changed: - `validate_tenant_draft` completes an item written before the `generated` slot existed with `generated: None`, on a copy the adapter returns, so phase 2-3 rows read as current instead of `old/unrecognized`, and a read never changes the stored bytes (the maintenance hash still matches). - `attach_tenant_draft_document` refuses `409 same_as_generated` when the upload's sha256 equals the slot's `generated.sha256`, as the engagement pack upload does. - `items.<kind>.covers` and `applicability.<record>` are no longer change keys of `update_tenant_draft` (`400 invalid_request`): the pack fixes them. - `activate_engagement_draft` answers `tenantId` from the locked step; the plugin audits `engagement_created` under it instead of the pre-lock parent. - `delete_tenant_draft` cascade: a `404 not_found` from a child's row delete (removed concurrently) counts as done. - Module memory notes that one malformed `engagement_draft` row fails close, cascade delete and list closed until data maintenance removes it. Why: - Review of eaa98ff (APPROVE_WITH_NITS): keep pre-phase-4 drafts readable, mirror the `same_as_generated` rule on the tenant pack, honour "never edited" for covers and applicability, audit with the tenant the engagement was created under, and tolerate a concurrent child delete. Checks: - rm4: focused suites (engagement drafts, tenant drafts, engagements, data maintenance, authz surface, tenant contract/administration/delete) 283 tests OK. * feat(redmesh): super-tenant profile, party block and generated-pack baseline (RM-110 phase 1) What changed: - `super_tenant_profile` record kind (`tenancy/super_tenant_profile.py`, one row per deployment, `ids = ["deployment"]`) with `get_super_tenant_profile` / `update_super_tenant_profile` (STA-only, partial update, audited as `super_tenant_profile_updated` with the changed keys only); adapter `_KINDS` / `_validate` and the data-maintenance rule (always current, never orphan). - Tenant draft and tenant `legal` gain the optional party fields `address`, `vat_id`, `contact_name`, `contact_email`, `contact_phone`: `_legal` accepts a four-key block unchanged and writes no default, the draft validator completes an old block on a copy, DTOs answer absent optional fields as "", `prepare_tenant(draft_id)` copies and binds the whole block. - `store_generated_document` for a tenant draft (`contract`) or an engagement draft (`engagement_pack`): the backend hashes the bytes and the snapshot, stores both in one `redmesh_tenant_contract` 1.1 envelope (`role: generated`), writes the `generated` block and the new item state `generated` (from missing / generated / awaiting_signature; `409 already_signed` otherwise); the previous generated file is deleted after the new ref is written; refusals before any store write. - `contract_generated` on receipt and tenant: copied at activation when the draft has a baseline, optional and outside the receipt's all-or-none set, bound when present (extra-on-tenant guard), returned by `get_tenant_contract`, deleted with the tenant. Why: RM-110 phase 1 of the onboarding-drafts contract: the provider party block and the customer party fields the generated packs are rendered from, and the backend side of the generation baseline, so the Navigator renderer (phase 2) has a place to store its packs and snapshots. Checks: - rm4: unittest over test_super_tenant_profile, test_tenant_drafts, test_engagement_drafts, test_tenant_contract, test_tenant_administration, test_tenant_delete, test_tenancy_administration_store, test_data_maintenance, test_authz_surface, test_tenant_engagements: 313 tests OK. - host .venv pytest extensions/business/cybersec/red_mesh/tests: 6312 passed, 5 skipped, 3522 subtests passed. * fix(redmesh): generated-pack review fixes (RM-110 phase 1) What changed: - `release_tenant_draft_activation`, tenant-deleted branch: the generated baseline is dropped with the signed copy (`generated: null`, state `missing` when it was `generated`), since the tenant delete removed `contract_generated.ref`, the same CID the draft names. - `prepare_tenant(draft_id)`: the plugin re-reads and re-hashes `items.contract.generated.ref` as well (`resolve_draft_document`, slot `contract`; a gone or tampered file is `contract_invalid`), and the locked step includes it in the `draft_changed` comparison, as `activate_engagement_draft` does for its pack. - `drafts.normalize_snapshot`: a cheap character-length pre-check before the UTF-8 length. - `drafts.py` exports `LEGAL_MAX`, `nonempty_text`, `text` (private aliases kept); `super_tenant_profile.py` imports those. Why: Review of 3920034 (APPROVE_WITH_NITS): a released draft must not keep a block whose file is gone, and the tenant path should verify the generated pack the way the engagement path does. Checks: - rm4: unittest over test_super_tenant_profile, test_tenant_drafts, test_engagement_drafts, test_tenant_contract, test_tenant_administration, test_tenant_delete, test_tenancy_administration_store, test_data_maintenance, test_authz_surface, test_tenant_engagements: 315 tests OK. * test(redmesh): engagement draft ids never resolve on node assignment What changed: TestEngagementDraftIdsNeverResolve also probes set_tenant_node_assignment with a ted_ id. Why: pins the 404 not_found answer already confirmed by a read-only probe. Checks: test_engagement_drafts, 31 tests OK. * feat(redmesh): tenant activation without an admin, draft node plan and private hold (RM-112 phase 2) What changed: - Admin optional on a draft activation: None is canonical on the intent, the receipt and the tenant (draft "" normalised in _draft_terms); the receipt validator takes initial_admin_id and initial_admin_generation both or neither; prepare_tenant and activate_tenant read or check no account when there is none; root_admin_id stays None. Completeness no longer lists field:initial_admin_id. The one-step prepare_tenant still requires an admin. - Draft `nodes` plan ({node_address, mode}, validated with valid_node_address and NODE_ASSIGNMENT_MODES, sorted, one per node), written through update_tenant_draft, in the DTO and the list row; a row stored before it reads back with nodes []. - Private hold: _assign_node refuses a node held by a draft's private plan with 409 node_planned_private, except for the tenant whose receipt names that draft; update_tenant_draft refuses planning a node another draft holds privately, and a private plan while the node has a live assignment (node_private_assigned / node_shared_assigned). The holding draft's id and name are returned only to a full-portfolio Super-Tenant Admin. Delete and close drop the plan with the row. - begin_tenant_draft_delete refuses 409 draft_has_signed_documents while any draft item or child engagement pack is signed. - Tenant row (list and detail) gains nodeCount, the live assignments. Why: RM-112 owner decisions 3, 4, 14, 15, 17 and 20: a draft tenant activates without a user, plans its nodes on the record, and a draft with signed documents is kept. Checks: host .venv pytest, red_mesh tests (excluding e2e): 6334 passed, 5 skipped. rm4 unittest on the draft, node, administration and data-maintenance modules: 251 OK. Existing delete tests now use unsigned generated packs, since a signed draft is no longer deletable. * feat(redmesh): refuse deleting a signed engagement draft What changed: - begin_engagement_draft_delete refuses 409 draft_has_signed_documents when the draft's pack is signed, before any file is deleted; the finish path's race check is unchanged. - Engagement draft delete tests now delete unsigned drafts; new tests cover the signed refusal (and that unsigning first allows it) and a draft with no file. Why: - Owner decision 2026-10-08 (RM-112): a signed engagement pack keeps its draft, as the tenant draft rule does (decision 20). Checks: - pytest test_engagement_drafts/test_tenant_drafts/test_tenant_draft_setup: 121 passed - pytest red_mesh tests excluding e2e: 6322 passed, 5 skipped * feat(tenancy): a tenant draft activates only with its contract effective date What changed: - drafts.completeness lists field:contract.effective_from while the contract item has no effective date; an end date stays optional (none = an indefinite term). - Test helpers set the date on ready drafts; a test covers the refusal and the indefinite term. Why: - RM-112 decision 24: the contract term is set before the tenant pack is generated and printed in it, so a tenant is never activated on a contract without a start date. Checks: - docker exec rm4 python3 -m unittest (tenant contract, administration, delete, data maintenance, store, documents, drafts, engagement drafts, profile, authz, engagements, draft setup): 347 OK - full red_mesh unittest discover: 3576 run; the 30 errors are the existing pytest-import failures in the container * chore: inc ver
1 parent f48ae10 commit 76165fe

23 files changed

Lines changed: 5051 additions & 82 deletions

‎extensions/business/cybersec/red_mesh/AGENTS.md‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,3 +579,90 @@ Only append entries for critical or fundamental RedMesh backend changes, discove
579579
503, no storage" tests were narrowed to the namespace cases (`None`, blank, non-string) and
580580
renamed `test_missing_namespace_*`; stray `cfg_tenant_administration_enabled` fixture attributes
581581
dropped. Dev-node stream configs may still carry the key; the plugin ignores unknown config keys.
582+
583+
### 2026-10-06 — RM-109 phase 2: tenant drafts
584+
585+
- BUILDER: `tenant_draft` record kind (`tenancy/drafts.py`: vocabulary, validator, completeness, DTO)
586+
and seven `authorize_platform`-gated POST endpoints (create/update/get/list/delete, document
587+
upload/download). Contract: hub `docs/resources/redmesh/contracts/onboarding-drafts.md`. The
588+
service writes under the administration lock; every document read/write/delete is the plugin's,
589+
outside it. Draft uploads reuse the contract checks with a `schema_version` 1.1 envelope naming
590+
`draft_id` and `document_kind`; the stored ref keeps the `_valid_contract` key set.
591+
- CRITIC/response: a draft id is not a tenant id, so data maintenance classifies `tenant_draft`
592+
before its related-tenant check (current when the validator passes, else old/unrecognized; never
593+
orphan). A replaced or dropped file is deleted after the row stops naming it; a failed delete is
594+
logged with draft, slot and ref, the write stands (the file is then unreferenced). No draft
595+
answer reads the store: R1FS answers a timeout as no file, so a read failure must never be
596+
persisted as `missing`. An unreadable file is a 404 on download and is skipped on delete.
597+
Draft delete removes files first and refuses (`409 conflict`) a file attached meanwhile. No
598+
activation yet: `activation`/`last_release` stay null and a set marker locks the draft (phase 3).
599+
- Verification: `tests/test_tenant_drafts.py` (new), `test_data_maintenance.py` (draft current, files
600+
in the inventory, malformed draft old/unrecognized) and `test_authz_surface.py` (96 endpoints).
601+
602+
### 2026-10-06 — RM-109 phase 3: draft activation, release, close
603+
604+
- BUILDER: `prepare_tenant(draft_id)` takes every creation field from the draft (caller fields are
605+
ignored); the plugin re-resolves the draft's files bound to draft and slot. Locked order:
606+
`draft_changed`, marker (`activation_in_progress` with `holder`), `draft_incomplete` with
607+
`missing`, existing checks, then on first preparation the uploader rule (platform role through
608+
the identity port, both paths) and `contract_in_use` (raw tenant/receipt scan, 503 past the cap).
609+
The marker is written before the receipt. Receipt and tenant gain the draft terms, bound like
610+
`contract`; tenant delete and `download_tenant_document` cover all three documents.
611+
- CRITIC/response: release writes `last_release` while the marker is still set, then deletes tenant
612+
row, domain reservation and receipt, then clears the marker, so every crash point resumes. A
613+
marker without a receipt is told apart by `last_release` (resumed release), the domain
614+
reservation still bound to the marker (tenant activated then deleted: items set `missing`), or
615+
neither (preparation stopped before the receipt: files intact). No release deletes a file.
616+
- Verification: `tests/test_tenant_drafts.py`, `test_tenant_contract.py` (uploader relaxed,
617+
draft envelope refused, `contract_in_use`), `test_data_maintenance.py`, `test_authz_surface.py`.
618+
619+
### 2026-10-07 — RM-109 phase 4: engagement drafts inside the tenant draft
620+
621+
- BUILDER: `engagement_draft` record kind (`tenancy/engagement_drafts.py`, `ted_<uuid>`, `parent` =
622+
`{draft_id}` or `{tenant_id}`), eight `authorize_platform`-gated endpoints (create/update/get/list/
623+
delete, pack upload/download, `activate_engagement_draft`). Fields are checked with the
624+
`tenancy/engagements.py` normalizers on every write (emptiness allowed) and in full at activation.
625+
The pack envelope is `redmesh_tenant_contract` 1.1 with `document_kind: engagement_pack` and
626+
`engagement_draft_id`; `read_engagement_document` accepts it. Collapsed tenant checklist: the
627+
`contract` item covers both tenant records, applicability is `required` and no longer decides
628+
completeness; `items.*.generated` and `document.generated` are validated (written by RM-110).
629+
- CRITIC/response: activation is one locked call with no marker. An engagement `en_<uuid>` already
630+
under the parent tenant means a crash after `create_engagement`, so the row is deleted and the
631+
call answers `replayed`, whoever calls; the refs are re-resolved outside the lock
632+
(`resolve_engagement_pack`) and compared under it (`draft_changed`); pack uploaders need the
633+
platform role instead of "equals creator"; `contract_in_use` covers engagement `documents[]`.
634+
`close_tenant_draft` re-homes the children before deleting the row; `delete_tenant_draft`
635+
cascades (children's files, rows, then its own); `delete_tenant` does not cascade, so data
636+
maintenance classifies a child with no parent `orphan/parent_gone`. `ted_` ids answer 404 as
637+
tenant and as engagement ids. `_engagement_draft_children` validates every `engagement_draft`
638+
row of the namespace, so one malformed child row fails `close_tenant_draft`, `delete_tenant_draft`
639+
and `list_engagement_drafts` closed (503) until data maintenance removes it.
640+
- Verification: `tests/test_engagement_drafts.py` (new), `test_tenant_drafts.py` (collapsed
641+
checklist), `test_data_maintenance.py`, `test_authz_surface.py` (107 endpoints).
642+
643+
### 2026-10-07 — RM-110 phase 1: super-tenant profile, party block, generated-pack baseline
644+
645+
- BUILDER: `super_tenant_profile` record kind (`tenancy/super_tenant_profile.py`, one row per
646+
deployment, `ids = ["deployment"]`, eight 0-200 text fields) with `get_/update_super_tenant_profile`
647+
(`authorize_platform`-gated; the empty profile is answered without a write; `update` is partial
648+
and audited as `super_tenant_profile_updated` with the changed keys only). Tenant draft and tenant
649+
`legal` gain the optional party fields `address`, `vat_id`, `contact_name`, `contact_email`,
650+
`contact_phone` (`drafts.LEGAL_OPTIONAL_FIELDS`): `_legal` accepts a four-key block unchanged and
651+
writes no default, the draft validator completes an old block on a copy, DTOs answer absent
652+
optional fields as `""`. `store_generated_document` (one endpoint for `draft_id`/`contract` and
653+
`engagement_draft_id`/`engagement_pack`): the plugin stores bytes + `snapshot` in one
654+
`redmesh_tenant_contract` 1.1 envelope (`role: generated`) outside the lock, hashing both itself;
655+
the service writes the `generated` block and item state `generated` (new in `ITEM_STATES`; from
656+
`missing`, `generated`, `awaiting_signature`; `409 already_signed` otherwise). The draft's
657+
`contract` baseline is copied at activation as `contract_generated` on receipt and tenant.
658+
- CRITIC/response: `contract_generated` sits outside the receipt's all-or-none draft set and is
659+
compared only when present, with the extra-on-tenant guard, so pre-RM-110 rows stay valid;
660+
`_tenant_document_refs` includes its ref so tenant delete removes the baseline file. The snapshot
661+
is checked as one JSON object of at most 64 KB and hashed exactly as sent (a strict canonical
662+
re-serialization check would refuse valid JS output over number formatting). `already_signed` and
663+
the formats are refused in the authorize step, before any store write; a stored row with state
664+
`generated` and no block is unreadable. `items_done` and completeness still count `signed` only.
665+
- Verification: `tests/test_super_tenant_profile.py` (new), `test_tenant_drafts.py`,
666+
`test_engagement_drafts.py` (the generated pack now comes from the real endpoint),
667+
`test_tenant_administration.py`, `test_tenant_delete.py`, `test_tenancy_administration_store.py`,
668+
`test_data_maintenance.py`, `test_authz_surface.py` (110 endpoints).

0 commit comments

Comments
 (0)