From 41bd6626414e4761f64467ef944c07627103ddd3 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 14 May 2026 01:02:25 +0200 Subject: [PATCH] Warnings cleanup --- ...blob-storage-content-streaming-workplan.md | 2 + docs/cmis-1-1-capability-scorecard.md | 44 ++++----- docs/cmis-compliance-assessment.md | 2 +- ...e-readiness-evidence-2026-05-13T223537Z.md | 70 ++++++++++++++ docs/first-release-readiness.md | 72 ++++++++++++++ examples/cmis/capability-fixtures.json | 13 ++- src/kontextual_engine/api/app.py | 93 +++++++++++++++++++ src/kontextual_engine/core/cmis.py | 5 - tests/cmis/test_cmis_browser_binding_api.py | 48 ++++++++++ tests/cmis/test_cmis_contract_examples.py | 3 +- tests/cmis/test_cmis_fixture_integration.py | 2 +- ...NT-WP-0014-cmis-object-content-maturity.md | 18 +++- 12 files changed, 331 insertions(+), 41 deletions(-) create mode 100644 docs/cmis-opencmis-tck-release-readiness-evidence-2026-05-13T223537Z.md create mode 100644 docs/first-release-readiness.md diff --git a/docs/blob-storage-content-streaming-workplan.md b/docs/blob-storage-content-streaming-workplan.md index d7a9391..ce39824 100644 --- a/docs/blob-storage-content-streaming-workplan.md +++ b/docs/blob-storage-content-streaming-workplan.md @@ -78,6 +78,8 @@ CMIS integration: - `getContentStream` returns actual bytes/stream with content headers, - `setContentStream` stores through deduplicating representation service, +- `appendContentStream` composes the current stream plus appended bytes and + stores the resulting representation through the same deduplicating service, - content stream changes produce versions and audit events, - descriptors remain available for clients that only need metadata. diff --git a/docs/cmis-1-1-capability-scorecard.md b/docs/cmis-1-1-capability-scorecard.md index 2e3944f..094941b 100644 --- a/docs/cmis-1-1-capability-scorecard.md +++ b/docs/cmis-1-1-capability-scorecard.md @@ -1,14 +1,13 @@ # CMIS 1.1 Capability Scorecard -Date: 2026-05-13 +Date: 2026-05-14 -Evidence update: the 2026-05-13 WP-0014 OpenCMIS pass completed the selected +Evidence update: the 2026-05-14 release-warning pass completed the selected Browser Binding `repository-type` and `object-content` baseline. The latest -run, `run-20260513T210255Z`, reports `0` unexpected findings: -`repository-type` is warning-only because the local harness uses HTTP, and -`object-content` is warning-only because `appendContentStream()` is not -supported. See -`docs/cmis-opencmis-tck-wp0014-evidence-2026-05-13T210255Z.md`. +run, `run-20260513T223537Z`, reports `0` unexpected findings. The +`object-content` group now passes without warnings; the only remaining +OpenCMIS warning is the local harness using HTTP rather than HTTPS. See +`docs/cmis-opencmis-tck-release-readiness-evidence-2026-05-13T223537Z.md`. The score below remains a product-depth estimate against mature CMIS products. The selected OpenCMIS baseline is now stable preparation evidence for @@ -72,10 +71,10 @@ CMIS interoperability importance rather than engine-internal importance. | Metric | Score | | --- | ---: | -| OpenCMIS selected-baseline infrastructure score | 98.3% | -| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 59% | -| Controlled-client Browser Binding usefulness | 80% | -| Broad commodity CMIS client compatibility | 54% | +| OpenCMIS selected-baseline infrastructure score | 99.1% | +| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 60% | +| Controlled-client Browser Binding usefulness | 82% | +| Broad commodity CMIS client compatibility | 55% | Interpretation: the OpenCMIS infrastructure score measures the selected `repository-type` and `object-content` harness baseline only. The current CMIS @@ -92,8 +91,8 @@ broad ECM/CMIS replacement surface. | Type definitions | 6 | 55% | Hyland Alfresco ACS | - Base types, Browser Binding type definitions, secondary type projection, and nullable content stream properties exist.
- No mutable types or custom schema/type management.
- Property definition depth remains intentionally narrow. | | Navigation service | 8 | 62% | Hyland Alfresco ACS | - Root and folder-scoped children, path lookup, folder parent lookup, parent path segments, move, and delete-tree work in the selected baseline.
- Projection-only parents exist.
- Missing `getDescendants`, `getFolderTree`, and real filing mutations. | | Object read service | 10 | 84% | Hyland Alfresco ACS | - Object envelopes, properties, content descriptors, ACL projection, relationships, allowable actions, property filters, and path-addressed Browser Binding reads exist.
- Deleted/hidden objects are correctly not exposed.
- OpenCMIS object/content read-side baseline now completes with warnings only. | -| Object write service | 8 | 70% | Hyland Alfresco ACS | - `createDocument`, `createFolder`, scoped `moveObject`, folder rename, selected standard property updates, custom metadata updates, content stream set/delete, `bulkUpdateProperties`, and `createDocumentFromSource` exist.
- No broad filing mutation, raw physical delete, checkout/checkin, or policy/item creation.
- Delete remains intentionally governed, not raw repository removal. | -| Content stream read/write | 8 | 82% | Hyland Alfresco ACS | - Byte streaming, explicit content headers, multipart Browser Binding create, deduplicating `setContentStream`, no-content compatibility streams, content tombstones, partial body slicing, and offset-zero full-stream classification exist.
- Digest verification and governed access exist.
- Remaining warning is unsupported append semantics. | +| Object write service | 8 | 72% | Hyland Alfresco ACS | - `createDocument`, `createFolder`, scoped `moveObject`, folder rename, selected standard property updates, custom metadata updates, content stream set/append/delete, `bulkUpdateProperties`, and `createDocumentFromSource` exist.
- No broad filing mutation, raw physical delete, checkout/checkin, or policy/item creation.
- Delete remains intentionally governed, not raw repository removal. | +| Content stream read/write | 8 | 86% | Hyland Alfresco ACS | - Byte streaming, explicit content headers, multipart Browser Binding create, deduplicating `setContentStream`, whole-object `appendContentStream`, no-content compatibility streams, content tombstones, partial body slicing, and offset-zero full-stream classification exist.
- Digest verification and governed access exist.
- Chunk-level blob composition remains a later optimization for very large append workloads. | | Versioning service | 8 | 25% | Hyland Alfresco ACS | - Version properties can be projected from engine versions.
- No checkout/checkin/cancelCheckout/PWC services.
- No version history route or all-versions query behavior. | | Discovery/query | 8 | 25% | Hyland Alfresco ACS | - Narrow document select subset exists.
- Unsupported joins/order-by return diagnostics.
- Missing CMIS SQL predicates, type joins, full-text, ordering, and rich projection rules. | | Relationships | 5 | 60% | Hyland Alfresco ACS | - Relationship object projection and source filtering exist.
- Visibility gates prevent protected relationship leakage.
- Missing full relationship service filters, relationship creation through CMIS, and type hierarchy maturity. | @@ -104,19 +103,19 @@ broad ECM/CMIS replacement surface. | Renditions | 3 | 15% | Hyland Alfresco ACS | - Native representations could become rendition candidates later.
- CMIS rendition capability is currently `none`.
- No rendition taxonomy or rendition stream routes. | | Retention and hold | 2 | 5% | OpenText / Hyland governance stacks | - Native governance metadata can represent intent later.
- No CMIS retention/hold model or mutation services.
- Explicitly unsupported. | | Bulk update | 2 | 65% | Hyland Alfresco ACS | - `bulkUpdateProperties` works for the `compat-tck` profile by batching existing property updates with change-token handling.
- It is intentionally narrow and not enabled on the normal governed-authoring profile yet.
- No advanced partial-success envelope beyond the Browser Binding response list. | -| Browser Binding protocol fidelity | 7 | 78% | Hyland Alfresco ACS | - Browser-style routes, JSON envelopes, CMIS exception names, action aliases, multipart forms, path-addressed root routes, property filters, path segments, and range responses exist.
- Selected OpenCMIS Browser Binding repository/type and object/content baseline completes with warnings only.
- Optional services and broader CMIS SQL/versioning protocol surfaces remain incomplete. | +| Browser Binding protocol fidelity | 7 | 80% | Hyland Alfresco ACS | - Browser-style routes, JSON envelopes, CMIS exception names, action aliases, multipart forms, path-addressed root routes, property filters, path segments, and range responses exist.
- Selected OpenCMIS Browser Binding repository/type and object/content baseline completes with only the local HTTP warning.
- Optional services and broader CMIS SQL/versioning protocol surfaces remain incomplete. | | AtomPub binding | 2 | 0% | Hyland Alfresco ACS | - No AtomPub/XML service document or feeds.
- Intentionally deferred until monetized need. | | Web Services binding | 2 | 0% | Hyland Alfresco ACS | - No SOAP/WSDL stack.
- Intentionally deferred until monetized need. | -| External conformance evidence | 3 | 82% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation succeeds against `compat-tck`.
- Selected `repository-type` and `object-content` baselines complete with warnings only.
- Evidence still covers a selected baseline, not the full OpenCMIS TCK surface. | +| External conformance evidence | 3 | 86% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation succeeds against `compat-tck`.
- Selected `repository-type` and `object-content` baselines complete with one local transport warning and no object/content warnings.
- Evidence still covers a selected baseline, not the full OpenCMIS TCK surface. | -Weighted result from this table: **59%**. +Weighted result from this table: **60%**. ## Most Important Gaps 1. **External conformance expansion** - Keep the selected OpenCMIS TCK baseline running against `compat-tck`. - - Treat the current repository/type and object/content warnings as known: - local HTTP and unsupported append. + - Treat the remaining local HTTP warning as a harness/deployment topology + issue, not an adapter behavior failure. - Expand selected groups after the supported baseline remains stable. 2. **Browser Binding fidelity** @@ -145,10 +144,11 @@ Weighted result from this table: **59%**. - Map selected derived representations to CMIS renditions only after we have stable representation taxonomy and real preview/thumbnail use cases. -7. **Append streams** - - Keep `appendContentStream()` unsupported unless a real client requires it. - - If implemented later, design it through blob composition/deduplication - rather than byte concatenation in the CMIS adapter. +7. **Release transport and operational posture** + - Terminate CMIS access over HTTPS in deployable environments. + - Keep the local HTTP warning accepted only for loopback TCK runs. + - Revisit blob composition if large append workloads become a real usage + pattern. ## Product Positioning Takeaway diff --git a/docs/cmis-compliance-assessment.md b/docs/cmis-compliance-assessment.md index 81d132b..b36d520 100644 --- a/docs/cmis-compliance-assessment.md +++ b/docs/cmis-compliance-assessment.md @@ -48,7 +48,7 @@ Practical strategy: | Navigation service | Implemented subset. | `getChildren` and projected parents are supported. `getDescendants`, `getFolderTree`, mutating multifiling, and unfiling are explicitly flagged unsupported. | Low unless full folder tree is required | | Object service read | Implemented subset. | Object envelopes, allowable actions, content stream descriptors, content stream properties, visibility redaction, and relationship IDs are covered. | Low | | Object service write | Governed subset. | `createDocument`, custom metadata updates, `setContentStream`, and delete-request lifecycle transition are supported by authoring profiles. Unsupported standard property updates now fail with diagnostics. | Medium | -| Content streams | Implemented subset. | Descriptor and byte-stream routes exist; `setContentStream` writes through deduplicating blob storage. Append/delete content stream are unsupported. | Low | +| Content streams | Implemented subset. | Descriptor and byte-stream routes exist; `setContentStream` and whole-object `appendContentStream` write through deduplicating blob storage, while `deleteContentStream` tombstones the CMIS projection. Chunk-level append composition remains deferred. | Low | | Versioning | Projection only. | Latest-version properties can be projected from engine versions, but CMIS checkout/PWC/all-versions services are not advertised. | Low if unsupported remains acceptable | | Discovery/query | Implemented narrow subset. | `SELECT * FROM cmis:document` and `SELECT * FROM kontextual:document` are supported. Joins, order-by, full CMIS SQL predicates, and full-text are flagged unsupported. | Medium | | Relationships | Implemented subset. | Relationship object projections and source filters are covered and profile-gated. | Low | diff --git a/docs/cmis-opencmis-tck-release-readiness-evidence-2026-05-13T223537Z.md b/docs/cmis-opencmis-tck-release-readiness-evidence-2026-05-13T223537Z.md new file mode 100644 index 0000000..06db6cc --- /dev/null +++ b/docs/cmis-opencmis-tck-release-readiness-evidence-2026-05-13T223537Z.md @@ -0,0 +1,70 @@ +# CMIS OpenCMIS TCK Evidence - Release Readiness - 2026-05-13T22:35:37Z + +## Run Summary + +- Run ID: `run-20260513T223537Z` +- Local date: 2026-05-14 Europe/Berlin +- Harness: `guide-board` with `open-cmis-tck` extension +- Assessment: `cmis-browser-baseline` +- Target: `kontextual-cmis-compat` +- Endpoint: `http://127.0.0.1:8010/cmis/compat-tck/browser` +- OpenCMIS TCK: CMIS 1.1.0, revision `1789681` +- Result: `completed` +- Policy: `0` unexpected findings, `0` applied waivers +- Run directory: `/tmp/kontextual-cmis-release-20260514-toolchain` +- Internal verification: `.venv/bin/python -m pytest -q` -> `166 passed`, + `14 skipped` + +This run was executed after adding Browser Binding `appendContent` / +`appendContentStream` support through the existing content representation +service. + +## Normalized Results + +| Group | Result | Counts | Remaining non-green findings | +| --- | --- | --- | --- | +| `repository-type` | `warning` | `38 pass`, `2 info`, `1 skipped`, `1 warning` | Local loopback endpoint uses HTTP rather than HTTPS. | +| `object-content` | `pass` | `10 info`, `5 skipped` | None. | + +Guide Board summary: + +- `pass`: 2 +- `warning`: 1 + +## Score + +This is a compatibility-infrastructure score for the selected Browser Binding +baseline, not a CMIS certification score. + +| Metric | Score | Basis | +| --- | ---: | --- | +| Selected baseline completion | 100.0% | Guide Board result `completed`; both selected groups returned `0`. | +| Unexpected finding clearance | 100.0% | `0` unexpected findings, `0` fail, `0` infrastructure_error. | +| Warning-adjusted normalized case score | 99.1% | `(56 accepted + 0.5 * 1 warning) / 57 normalized cases`. | +| Strict no-warning normalized case score | 98.2% | `56 accepted / 57 normalized cases`. | + +Digest versus `run-20260513T210255Z`: + +- OpenCMIS selected-baseline infrastructure score improved from `98.3%` to + `99.1%`. +- `object-content` improved from warning to pass. +- The previous `appendContentStream()` warning is closed. +- The only remaining warning is local HTTP transport in the loopback harness. + +## Interpretation + +The selected Browser Binding repository/type and object/content baseline is now +release-ready for a controlled preview. The remaining HTTP warning should be +handled as a deployment gate: released access points need HTTPS termination, +while loopback TCK runs may keep the warning as an accepted local harness +condition. + +Skipped object/content cases remain aligned with declared capability +boundaries: + +- Relationship, policy, and item creation are not advertised as creatable. +- Folder-name change-token subcases are skipped by the TCK. + +This evidence still does not cover full CMIS 1.1 certification, AtomPub, Web +Services, PWC/checkin/checkout, full CMIS SQL, renditions, retention, or policy +mutation depth. diff --git a/docs/first-release-readiness.md b/docs/first-release-readiness.md new file mode 100644 index 0000000..2aa6373 --- /dev/null +++ b/docs/first-release-readiness.md @@ -0,0 +1,72 @@ +# First Release Readiness + +Date: 2026-05-14 + +Target posture: `0.1.0` controlled preview. This release can be good to go +when it is honest, reproducible, observable, and recoverable. It does not need +to pretend to be a full ECM or certified CMIS repository. + +## Release Scope + +In scope: + +- native asset registry, metadata, relationship, retrieval, transformation, + workflow, service API, blob, S3-backend, and observability foundations, +- profiled CMIS Browser Binding subset for controlled integrations, +- OpenCMIS selected baseline for `repository-type` and `object-content`, +- explicit unsupported flags for out-of-scope CMIS capabilities, +- State Hub registered workplans and evidence documents. + +Out of scope for `0.1.0`: + +- AtomPub and Web Services bindings, +- full CMIS SQL, checkout/checkin/PWC, policy mutation, retention/hold, + renditions, and broad filing mutation, +- full ECM/records-management semantics, +- formal CMIS certification claim. + +## Go / No-Go Gates + +| Area | Gate | Current state | +| --- | --- | --- | +| Tests | Full suite passes in the project venv. | `.venv/bin/python -m pytest -q` passed: `166 passed`, `14 skipped`; advisory performance drift warnings recorded. | +| CMIS evidence | OpenCMIS selected baseline completes with no unexpected findings. | `run-20260513T223537Z` completed; only local HTTP warning remains. | +| Transport | Released CMIS access points are served behind HTTPS. | Required deployment gate; local loopback warning is accepted only for harness runs. | +| Capability honesty | Scorecard, unsupported catalog, and examples match behavior. | Updated for `appendContentStream`; final doc review required. | +| Packaging | Version, dependencies, optional extras, and install smoke are checked. | `pyproject.toml` is already `0.1.0`; build/install smoke still required. | +| Configuration | Storage backend, S3 settings, local blob path, and environment defaults are documented. | Existing docs cover backends; release runbook should point to them. | +| Data safety | Blob cleanup, backups, restore path, and migration posture are documented. | Cleanup exists; backup/restore release notes still needed. | +| Security | Actor headers, access-point profiles, secrets, and dependency/license review are done. | Profile model exists; release security review still required. | +| Operations | Health checks, logs, performance history, and known warnings are documented. | Health and performance monitor exist; release runbook still needed. | +| State | State Hub consistency check passes after release docs/workplan updates. | Passed after WP-0015 registration. | + +## Known Accepted Limitations + +- CMIS Browser Binding is the supported external CMIS protocol surface. +- CMIS multifiling remains projection-only. +- CMIS append is implemented as whole-object append through deduplicating + representation storage; chunk-level blob composition is deferred. +- Local OpenCMIS loopback runs warn about HTTP. This must not be carried into + production-facing access points. +- Performance drift warnings from pytest are advisory unless repeated under a + quiet system baseline; the current run flagged several CMIS API tests. + +## Release Procedure + +1. Run `.venv/bin/python -m pytest -q`. +2. Run the OpenCMIS selected baseline through `guide-board` and persist the + evidence document. +3. Run State Hub consistency check and ensure workplans are registered. +4. Run packaging smoke: build wheel/sdist and import `kontextual_engine` from a + clean install. +5. Review security/configuration: HTTPS termination, profile exposure, secrets, + local/S3 blob backend settings, and dependency licenses. +6. Update `CHANGELOG.md` or release notes with capabilities, known limitations, + and accepted warnings. +7. Tag the release only after the gates above are green or explicitly waived. + +## Release Decision + +The current foundation is close to a controlled `0.1.0` preview. The main +remaining release work is not feature depth; it is discipline around repeatable +verification, packaging, deployment posture, and clearly documented limits. diff --git a/examples/cmis/capability-fixtures.json b/examples/cmis/capability-fixtures.json index 6e14281..4def144 100644 --- a/examples/cmis/capability-fixtures.json +++ b/examples/cmis/capability-fixtures.json @@ -111,8 +111,8 @@ "service": "object", "examples": ["source-document", "normalized-representation", "metadata-rich-document", "streamless-document"], "supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"], - "must_validate": ["get_object", "get_properties", "get_content_stream", "allowable_actions"], - "unsupported": ["append_content_stream"] + "must_validate": ["get_object", "get_properties", "get_content_stream", "append_content_stream", "allowable_actions"], + "unsupported": [] }, { "id": "versioning", @@ -170,7 +170,6 @@ "get_folder_tree": "capability_not_supported", "multifiling": "projection_only", "unfiling": "capability_not_supported", - "append_content_stream": "capability_not_supported", "versioning_services": "capability_not_supported", "private_working_copy": "capability_not_supported", "all_versions_search": "capability_not_supported", @@ -188,22 +187,22 @@ "readonly-browser": { "must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log"], "must_not_expose_objects": ["doc-confidential-risk"], - "must_reject_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"] + "must_reject_actions": ["create_document", "update_properties", "delete_object", "set_content_stream", "append_content_stream"] }, "governed-authoring": { "must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log"], "must_not_expose_objects": ["doc-confidential-risk"], - "must_authorize_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"] + "must_authorize_actions": ["create_document", "update_properties", "delete_object", "set_content_stream", "append_content_stream"] }, "admin-export": { "must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log", "retention-renditions-bulk"], "must_not_expose_objects": [], - "must_reject_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"] + "must_reject_actions": ["create_document", "update_properties", "delete_object", "set_content_stream", "append_content_stream"] }, "compat-tck": { "must_expose": ["repository-type", "navigation", "object-content", "versioning", "discovery-query", "relationships", "acl-policy", "change-log"], "must_not_expose_objects": ["doc-confidential-risk"], - "must_authorize_actions": ["create_document", "update_properties", "delete_object", "set_content_stream"] + "must_authorize_actions": ["create_document", "update_properties", "delete_object", "set_content_stream", "append_content_stream"] } } } diff --git a/src/kontextual_engine/api/app.py b/src/kontextual_engine/api/app.py index eb2ab63..0cca7b4 100644 --- a/src/kontextual_engine/api/app.py +++ b/src/kontextual_engine/api/app.py @@ -89,6 +89,7 @@ from kontextual_engine.services import ( API_VERSION = "v1" OPENAPI_VERSION = "1.0.0" +CMIS_APPEND_MAX_COMPOSED_BYTES = 64 * 1024 * 1024 AGENT_OPERATION_CATALOG: tuple[dict[str, Any], ...] = ( @@ -1305,6 +1306,73 @@ class ServiceRuntime: ) return self.cmis_object(access_point_id, object_id, context) + def cmis_append_content_stream( + self, + access_point_id: str, + object_id: str, + payload: dict[str, Any], + context: OperationContext, + ) -> dict[str, Any]: + mapper = self._cmis_mapper(access_point_id) + decision = mapper.access_point.decide_action(CMISAction.SET_CONTENT_STREAM, context, resource=object_id) + if not decision.allowed: + raise _cmis_authorization_error(decision, "appendContentStream") + asset_id = _cmis_asset_id(object_id) + asset = self.repository.get_asset(asset_id) + if not mapper.access_point.exposes_asset(asset, context): + raise NotFoundError( + "CMIS object not found", + details={"object_id": object_id, "access_point_id": access_point_id}, + ) + expected = ( + payload.get("expected_current_version_id") + or payload.get("changeToken") + or payload.get("change_token") + ) + self._cmis_assert_change_token(asset, expected, operation="appendContentStream") + appended = _cmis_payload_bytes(payload.get("content", b"")) + is_last_chunk = _cmis_form_bool(payload.get("isLastChunk") or payload.get("is_last_chunk"), default=True) + base = b"" + kind = payload.get("kind", RepresentationKind.SOURCE.value) + media_type = _cmis_media_type(payload.get("media_type", "application/octet-stream")) + if self._cmis_asset_representations(asset): + stream = self.content_service().get_content_stream(asset_id, context) + base = stream.content + kind = stream.representation.kind.value + media_type = _cmis_media_type(payload.get("media_type") or stream.representation.media_type) + elif asset.metadata.get("cmis_content_deleted"): + metadata = dict(asset.metadata) + metadata.pop("cmis_content_deleted", None) + self.repository.save_asset(replace(asset, metadata=metadata)) + composed_size = len(base) + len(appended) + if composed_size > CMIS_APPEND_MAX_COMPOSED_BYTES: + raise ValidationError( + "CMIS append content stream exceeds the composed append limit", + details={ + "code": "cmis.append_content_limit_exceeded", + "cmis_exception": "constraint", + "operation": "appendContentStream", + "max_size_bytes": CMIS_APPEND_MAX_COMPOSED_BYTES, + "composed_size_bytes": composed_size, + }, + ) + self.content_service().add_representation_from_bytes( + asset_id, + kind, + media_type, + base + appended, + context, + expected_current_version_id=expected, + metadata={ + "cmis": { + "operation": "appendContentStream", + "is_last_chunk": is_last_chunk, + "appended_bytes": len(appended), + } + }, + ) + return self.cmis_object(access_point_id, object_id, context) + def cmis_delete_content_stream( self, access_point_id: str, @@ -3676,6 +3744,11 @@ def create_app(runtime: ServiceRuntime | None = None): raise ValidationError("CMIS object id is required", details={"operation": "setContentStream"}) response(runtime.cmis_set_content_stream, access_point_id, object_id, payload, context) return response(runtime.cmis_browser_object, access_point_id, object_id, context) + if action in {"appendContentStream", "appendContent"}: + if not object_id: + raise ValidationError("CMIS object id is required", details={"operation": "appendContentStream"}) + response(runtime.cmis_append_content_stream, access_point_id, object_id, payload, context) + return response(runtime.cmis_browser_object, access_point_id, object_id, context) if action in {"deleteContent", "deleteContentStream"}: if not object_id: raise ValidationError("CMIS object id is required", details={"operation": "deleteContentStream"}) @@ -3699,6 +3772,8 @@ def create_app(runtime: ServiceRuntime | None = None): "move", "setContentStream", "setContent", + "appendContentStream", + "appendContent", "deleteContent", "deleteContentStream", ], @@ -4632,6 +4707,24 @@ def _cmis_media_type(value: Any) -> str: return media_type or "application/octet-stream" +def _cmis_payload_bytes(value: Any) -> bytes: + if value is None: + return b"" + if isinstance(value, bytes): + return value + if isinstance(value, bytearray): + return bytes(value) + return str(value).encode("utf-8") + + +def _cmis_form_bool(value: Any, *, default: bool = False) -> bool: + if value in (None, ""): + return default + if isinstance(value, bool): + return value + return str(value).strip().lower() in {"1", "true", "yes", "on"} + + def _cmis_value_list(value: Any) -> list[str]: if value is None or value == "": return [] diff --git a/src/kontextual_engine/core/cmis.py b/src/kontextual_engine/core/cmis.py index ff27a9a..6d372e3 100644 --- a/src/kontextual_engine/core/cmis.py +++ b/src/kontextual_engine/core/cmis.py @@ -191,11 +191,6 @@ UNSUPPORTED_FEATURES: dict[str, dict[str, Any]] = { "standard_flag": "capability_join", }, "order_by": {"status": "unsupported", "reason": "query_not_supported", "standard_flag": "capability_order_by"}, - "append_content_stream": { - "status": "unsupported", - "reason": "capability_not_supported", - "standard_flag": "capability_content_stream_updatability", - }, "apply_acl": {"status": "unsupported", "reason": "operation_not_implemented", "standard_flag": "capability_acl"}, "apply_policy": {"status": "unsupported", "reason": "capability_not_supported"}, "remove_policy": {"status": "unsupported", "reason": "capability_not_supported"}, diff --git a/tests/cmis/test_cmis_browser_binding_api.py b/tests/cmis/test_cmis_browser_binding_api.py index 8071db1..c604d7e 100644 --- a/tests/cmis/test_cmis_browser_binding_api.py +++ b/tests/cmis/test_cmis_browser_binding_api.py @@ -366,6 +366,54 @@ def test_cmis_browser_binding_delete_content_stream_tombstones_content(cmis_clie assert content_after_delete.json()["exception"] == "constraint" +def test_cmis_browser_binding_append_content_stream_adds_versioned_content(cmis_client) -> None: + document = cmis_client.post( + "/cmis/compat-tck/browser/root", + data={ + "cmisaction": "createDocument", + "propertyId[0]": "cmis:objectTypeId", + "propertyValue[0]": "cmis:document", + "propertyId[1]": "cmis:name", + "propertyValue[1]": "Append Content Document", + }, + files={"content": ("append-content.txt", b"one", "text/plain")}, + ).json() + object_id = document["properties"]["cmis:objectId"]["value"] + token = document["properties"]["cmis:changeToken"]["value"] + + appended = cmis_client.post( + "/cmis/compat-tck/browser/root", + data={ + "cmisaction": "appendContent", + "objectId": object_id, + "changeToken": token, + "isLastChunk": "true", + }, + files={"content": ("append-content.txt", b" two", "text/plain")}, + ) + content = cmis_client.get( + "/cmis/compat-tck/browser/root", + params={"cmisselector": "content", "objectId": object_id}, + ) + stale_append = cmis_client.post( + "/cmis/compat-tck/browser/root", + data={ + "cmisaction": "appendContentStream", + "objectId": object_id, + "changeToken": token, + }, + files={"content": ("append-content.txt", b" stale", "text/plain")}, + ) + + assert appended.status_code == 200 + assert appended.json()["properties"]["cmis:contentStreamLength"]["value"] == 7 + assert appended.json()["properties"]["cmis:contentStreamMimeType"]["value"] == "text/plain" + assert appended.json()["properties"]["cmis:changeToken"]["value"] != token + assert content.content == b"one two" + assert stale_append.status_code == 409 + assert stale_append.json()["exception"] == "updateConflict" + + def test_cmis_browser_binding_change_tokens_conflict_on_stale_updates(cmis_client) -> None: document = cmis_client.post( "/cmis/compat-tck/browser/root", diff --git a/tests/cmis/test_cmis_contract_examples.py b/tests/cmis/test_cmis_contract_examples.py index 72806ad..a943629 100644 --- a/tests/cmis/test_cmis_contract_examples.py +++ b/tests/cmis/test_cmis_contract_examples.py @@ -80,6 +80,7 @@ def test_profile_visibility_and_mutation_expectations_are_explicit() -> None: "update_properties", "delete_object", "set_content_stream", + "append_content_stream", ] else: assert expectations["must_reject_actions"] == [ @@ -87,6 +88,7 @@ def test_profile_visibility_and_mutation_expectations_are_explicit() -> None: "update_properties", "delete_object", "set_content_stream", + "append_content_stream", ] @@ -117,4 +119,3 @@ def test_admin_export_is_the_only_profile_for_deferred_extension_group() -> None for profile_name, expectations in catalog["profile_expectations"].items(): exposes_extension = "retention-renditions-bulk" in expectations["must_expose"] assert exposes_extension is (profile_name == "admin-export") - diff --git a/tests/cmis/test_cmis_fixture_integration.py b/tests/cmis/test_cmis_fixture_integration.py index 27704d8..f3f335b 100644 --- a/tests/cmis/test_cmis_fixture_integration.py +++ b/tests/cmis/test_cmis_fixture_integration.py @@ -27,6 +27,7 @@ ACTION_MAP = { "update_properties": CMISAction.UPDATE_PROPERTIES, "delete_object": CMISAction.DELETE_OBJECT, "set_content_stream": CMISAction.SET_CONTENT_STREAM, + "append_content_stream": CMISAction.SET_CONTENT_STREAM, } @@ -119,4 +120,3 @@ def test_optional_tck_result_template_can_capture_gap_mapping() -> None: assert template["summary"] == {"passed": 0, "failed": 0, "skipped": 0, "known_gap": 0} assert "capability_gaps" in template assert template["groups"][0]["status"] == "not_run" - diff --git a/workplans/KONT-WP-0014-cmis-object-content-maturity.md b/workplans/KONT-WP-0014-cmis-object-content-maturity.md index 40fc384..3f5fe22 100644 --- a/workplans/KONT-WP-0014-cmis-object-content-maturity.md +++ b/workplans/KONT-WP-0014-cmis-object-content-maturity.md @@ -10,7 +10,7 @@ topic_slug: markitect planning_priority: high planning_order: 14 created: "2026-05-08" -updated: "2026-05-13" +updated: "2026-05-14" state_hub_workstream_id: "ccfa90ee-be23-499b-a727-451a0d289df7" --- @@ -226,12 +226,20 @@ Latest verification: - `repository-type`: `38 pass`, `2 info`, `1 skipped`, `1 warning`. - `object-content`: `10 info`, `5 skipped`, `1 warning`. +Release-warning pass: + +- OpenCMIS: `run-20260513T223537Z` in + `/tmp/kontextual-cmis-release-20260514-toolchain`. +- Guide Board status: `completed`, with `0` unexpected findings. +- `repository-type`: `38 pass`, `2 info`, `1 skipped`, `1 warning`. +- `object-content`: `10 info`, `5 skipped`, `0 warning`. +- Warning-adjusted selected-baseline score improved from `98.3%` to `99.1%`. + Current external frontier: - The selected OpenCMIS Browser Binding repository/type and object/content baseline is completed with warnings only. -- Remaining warnings are local HTTP instead of HTTPS and unsupported - `appendContentStream()`. +- The only remaining warning is local HTTP instead of HTTPS. - Further maturity work should move to deliberately selected optional CMIS areas: query depth, descendants/folder-tree read navigation, version-history reads, relationship service depth, ACL detail, and rendition projection. @@ -338,7 +346,9 @@ Progress: `Content-Length`, `Content-Type`, `ETag`, and `Content-Range`. - Done for `deleteContentStream` tombstone semantics and offset-zero range classification. -- `appendContentStream()` remains explicitly unsupported by design. +- Done for Browser Binding `appendContent` / `appendContentStream` as + whole-object append through the deduplicating representation service, with a + composed-size guard. ## D14.6 - Add natural navigation and query depth