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