CMIS Browser Binding serializer layer

This commit is contained in:
2026-05-08 12:27:26 +02:00
parent 6382a5a7ab
commit 54a26cdb02
9 changed files with 1172 additions and 40 deletions

View File

@@ -2,12 +2,16 @@
Date: 2026-05-07 Date: 2026-05-07
Evidence update: the 2026-05-08 OpenCMIS TCK run found that broad commodity Evidence update: the 2026-05-08 OpenCMIS TCK compatibility implementation
CMIS client compatibility is currently pre-compliance because OpenCMIS cannot resolved the initial Browser Binding session blocker. The latest run,
create a Browser Binding session. See `run-20260508T092113Z`, completed the selected baseline with `38` passing
`docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md`. The score below is repository/type cases, one local HTTP transport warning, and `22` object/content
therefore retained as the pre-TCK product-depth estimate, not as external CMIS skips caused by the current non-creatable folder profile. See
compatibility evidence. `docs/cmis-opencmis-tck-implementation-evidence-2026-05-08T092113Z.md`.
The score below remains a product-depth estimate against mature CMIS products;
the evidence-backed TCK preparation score for the selected baseline is `23.81`
with `2/9` capability groups covered.
Status: baseline scorecard for the current Browser Binding subset. Status: baseline scorecard for the current Browser Binding subset.
@@ -98,16 +102,17 @@ replacement surface.
| Browser Binding protocol fidelity | 7 | 45% | Hyland Alfresco ACS | - Browser-style routes and JSON envelopes exist.<br>- FastAPI route shapes are pragmatic, not complete CMIS Browser Binding selector/action parity.<br>- Route-level tests skip without optional service dependencies. | | Browser Binding protocol fidelity | 7 | 45% | Hyland Alfresco ACS | - Browser-style routes and JSON envelopes exist.<br>- FastAPI route shapes are pragmatic, not complete CMIS Browser Binding selector/action parity.<br>- Route-level tests skip without optional service dependencies. |
| AtomPub binding | 2 | 0% | Hyland Alfresco ACS | - No AtomPub/XML service document or feeds.<br>- Intentionally deferred until monetized need. | | AtomPub binding | 2 | 0% | Hyland Alfresco ACS | - No AtomPub/XML service document or feeds.<br>- Intentionally deferred until monetized need. |
| Web Services binding | 2 | 0% | Hyland Alfresco ACS | - No SOAP/WSDL stack.<br>- Intentionally deferred until monetized need. | | Web Services binding | 2 | 0% | Hyland Alfresco ACS | - No SOAP/WSDL stack.<br>- Intentionally deferred until monetized need. |
| External conformance evidence | 3 | 20% | OpenCMIS TCK against Alfresco-like server behavior | - Internal fixtures and optional TCK mapping exist.<br>- No recorded OpenCMIS TCK execution against a running access point yet.<br>- No third-party client compatibility matrix yet. | | External conformance evidence | 3 | 35% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation now succeeds against `compat-tck`.<br>- Selected `repository-type` baseline completes with no failures and one local HTTP warning.<br>- `object-content` reaches parsed cases but skips because `cmis:folder` is not creatable; broader groups and third-party client matrix are still missing. |
Weighted result from this table: **42%**. Weighted result from this table: **42%**.
## Most Important Gaps ## Most Important Gaps
1. **External conformance run** 1. **External conformance expansion**
- Run selected OpenCMIS TCK groups against `compat-tck`. - Keep the selected OpenCMIS TCK baseline running against `compat-tck`.
- Capture failures by capability group. - Decide whether to add TCK-only `createFolder` support or keep CRUD/content
- Turn "estimated" scores into evidence-backed scores. skips as a deliberate profile boundary.
- Expand selected groups after the supported capability boundary is agreed.
2. **Browser Binding fidelity** 2. **Browser Binding fidelity**
- Align route/action/selector shapes more closely with CMIS Browser Binding. - Align route/action/selector shapes more closely with CMIS Browser Binding.

View File

@@ -4,6 +4,11 @@ Run timestamp: 2026-05-08T06:33:12Z
Local timestamp: 2026-05-08T08:33:12+02:00 Local timestamp: 2026-05-08T08:33:12+02:00
Status: evidence-backed compatibility blocker found Status: evidence-backed compatibility blocker found
Superseded implementation evidence: the blocker described here was resolved by
`KONT-WP-0013`. See
`docs/cmis-opencmis-tck-implementation-evidence-2026-05-08T092113Z.md` for the
latest completed OpenCMIS run.
## Purpose ## Purpose
Persist the first real `guide-board` + `open-cmis-tck` assessment against a Persist the first real `guide-board` + `open-cmis-tck` assessment against a

View File

@@ -0,0 +1,164 @@
# CMIS OpenCMIS TCK Implementation Evidence
Run timestamp: 2026-05-08T09:21:13Z
Local timestamp: 2026-05-08T11:21:13+02:00
Status: `KONT-WP-0013` implementation evidence captured
## Purpose
Persist the implementation outcome for the CMIS Browser Binding TCK
compatibility workplan. This document supersedes the initial blocker report in
`docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md` for the current
`kontextual-engine` state.
## Implementation Summary
The CMIS adapter now exposes a Browser Binding-shaped compatibility surface for
the `compat-tck` profile instead of leaking native snake_case DTOs on the
compatibility route.
Implemented changes:
- Browser Binding service document keyed by repository ID.
- CMIS field names for repository info, capabilities, extended features, type
definitions, object properties, ACLs, parents, query results, and root folder
projections.
- Selector handling for `repositoryInfo`, `typeChildren`, `typeDescendants`,
`typeDefinition`, `query`, `object`, `children`, `parents`, `properties`,
`allowableActions`, `policies`, and `content`.
- `compat-tck` repository ID alignment between the engine profile and the
OpenCMIS target profile.
- Root folder and virtual-folder projections that include the properties
advertised by the `cmis:folder` type definition.
- `includePropertyDefinitions` handling for type-list selectors.
- FastAPI OpenAPI generation fix for streaming routes.
- Internal regression coverage for Browser Binding selectors and OpenAPI route
generation.
## Final OpenCMIS Run
Guide Board live run:
- Run ID: `run-20260508T092113Z`
- Run directory: `/tmp/open-cmis-tck-kontextual-fix6-20260508`
- Assessment status: `completed`
- Target: `kontextual-cmis-compat`
- Assessment: `cmis-browser-baseline`
- Browser Binding URL: `http://127.0.0.1:8010/cmis/compat-tck/browser`
- Repository ID: `compat-tck`
Guide Board summary:
- `pass: 1`
- `warning: 1`
- `skipped: 1`
- unexpected findings: `0`
Mapped groups:
- Repository and type metadata: `warning`
- Object and content services: `skipped`
Normalized OpenCMIS case counts:
- `repository-type`: `38 pass`, `2 info`, `2 skipped`, `1 warning`, `0 fail`
- `object-content`: `22 skipped`, `0 fail`
The remaining repository/type warning is local transport only:
```text
HTTPS is not used. Credentials might be transferred as plain text!
```
The object/content group skips because the compatibility profile does not make
the `cmis:folder` base type creatable. This is an intentional boundary for the
current profiled adapter: governed document creation and content stream updates
exist through the current engine-backed routes, but full CMIS CRUD scaffolding
for TCK-created folders is not in scope yet.
## Scorecard
Generated scorecard:
- Path: `/tmp/open-cmis-tck-kontextual-fix6-20260508/reports/cmis-maturity-scorecard.md`
- Maturity score: `23.81`
- Coverage: `2/9` groups, `22.22%`
- Repository and type metadata: `partial`, score `3/4`
- Object and content services: `not_automated`, score `2/4`
Interpretation:
- The original protocol blocker is resolved: OpenCMIS can create a Browser
Binding session and execute selected checks.
- The current score remains low because only two groups are selected in the
baseline and one group is intentionally skipped by profile capability.
- The scorecard is preparation evidence, not CMIS certification.
## Commands
Focused internal CMIS regression suite:
```bash
cd /home/worsch/kontextual-engine
.venv/bin/python -m pytest tests/cmis --perf-history-disable
```
Result:
```text
45 passed
```
Live OpenCMIS run:
```bash
cd /home/worsch/guide-board
source /home/worsch/open-cmis-tck/.local/toolchains/env.sh
PYTHONPATH=src python3 -m guide_board \
--extension-dir ../open-cmis-tck \
run \
--target /tmp/kontextual-cmis-compat-8010.json \
--assessment ../open-cmis-tck/profiles/assessments/cmis-browser-baseline.json \
--output-dir /tmp/open-cmis-tck-kontextual-fix6-20260508
```
Scorecard generation:
```bash
cd /home/worsch/open-cmis-tck
PYTHONPATH=src python3 scripts/cmis_scorecard.py \
--run-dir /tmp/open-cmis-tck-kontextual-fix6-20260508
```
## Evidence Artifacts
The `/tmp` run artifacts are useful local evidence but may be ephemeral:
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/reports/report.md`
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/reports/cmis-maturity-scorecard.md`
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/normalized/evidence.json`
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/normalized/findings.json`
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/artifacts/open-cmis-tck/tck/repository-type/normalized-runner-result.json`
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/artifacts/open-cmis-tck/tck/object-content/normalized-runner-result.json`
## Remaining Boundary Gaps
These are not regressions in the current workplan; they are next-scope choices:
- Configure a durable local target profile or harness parameter for the
`kontextual-engine` test port instead of using a temporary `/tmp` profile.
- Decide whether the `compat-tck` profile should support CMIS `createFolder`
purely to exercise more CRUD/content TCK cases.
- Expand OpenCMIS selected groups beyond `repository-type` and
`object-content` once the targeted capability boundary is agreed.
- Harden `open-cmis-tck` preflight in the sister repository so native-shaped
repository-info responses are rejected before Maven invocation.
- Add HTTPS or a local warning waiver only when transport-security evidence
matters for a deployment profile.
## Conclusion
`KONT-WP-0013` achieved its main purpose for `kontextual-engine`: OpenCMIS no
longer fails at session creation, the repository/type group parses and executes,
the object/content group reaches parsed TCK cases, and the remaining gaps are
capability-scope decisions rather than protocol-shape crashes.

View File

@@ -51,6 +51,17 @@ from kontextual_engine.core import (
stable_json_dumps, stable_json_dumps,
utc_now, utc_now,
) )
from kontextual_engine.core.cmis import (
cmis_browser_object,
cmis_browser_object_in_folder_list,
cmis_browser_parent_list,
cmis_browser_query_result,
cmis_browser_root_folder,
cmis_browser_service_document,
cmis_browser_type_children,
cmis_browser_type_descendants,
cmis_browser_type_definition_by_id,
)
from kontextual_engine.errors import AuthorizationError, KontextualError, NotFoundError, ValidationError from kontextual_engine.errors import AuthorizationError, KontextualError, NotFoundError, ValidationError
from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, BlobStorage, PolicyGateway from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, BlobStorage, PolicyGateway
from kontextual_engine.services import ( from kontextual_engine.services import (
@@ -339,10 +350,131 @@ class ServiceRuntime:
def cmis_repository_info(self, access_point_id: str) -> dict[str, Any]: def cmis_repository_info(self, access_point_id: str) -> dict[str, Any]:
return self._cmis_mapper(access_point_id).repository_info() return self._cmis_mapper(access_point_id).repository_info()
def cmis_browser_service_document(
self,
access_point_id: str,
*,
repository_url: str,
root_folder_url: str,
) -> dict[str, Any]:
return cmis_browser_service_document(
self.cmis_repository_info(access_point_id),
repository_url=repository_url,
root_folder_url=root_folder_url,
)
def cmis_type_definitions(self, access_point_id: str) -> dict[str, Any]: def cmis_type_definitions(self, access_point_id: str) -> dict[str, Any]:
definitions = self._cmis_mapper(access_point_id).type_definitions() definitions = self._cmis_mapper(access_point_id).type_definitions()
return {"items": definitions, "count": len(definitions)} return {"items": definitions, "count": len(definitions)}
def cmis_browser_type_children(
self,
access_point_id: str,
*,
type_id: str | None = None,
skip_count: int = 0,
max_items: int = 100,
include_property_definitions: bool = False,
) -> dict[str, Any]:
return cmis_browser_type_children(
self._cmis_mapper(access_point_id).type_definitions(),
type_id=type_id,
skip_count=skip_count,
max_items=max_items,
include_property_definitions=include_property_definitions,
)
def cmis_browser_type_descendants(
self,
access_point_id: str,
*,
type_id: str | None = None,
include_property_definitions: bool = False,
) -> list[dict[str, Any]]:
return cmis_browser_type_descendants(
self._cmis_mapper(access_point_id).type_definitions(),
type_id=type_id,
include_property_definitions=include_property_definitions,
)
def cmis_browser_type_definition(
self,
access_point_id: str,
*,
type_id: str | None,
) -> dict[str, Any]:
try:
return cmis_browser_type_definition_by_id(
self._cmis_mapper(access_point_id).type_definitions(),
type_id,
)
except KeyError as exc:
raise NotFoundError(
"CMIS type definition not found",
details={"access_point_id": access_point_id, "type_id": type_id},
) from exc
def cmis_browser_root_object(self, access_point_id: str) -> dict[str, Any]:
return cmis_browser_object(cmis_browser_root_folder(self._cmis_access_point(access_point_id)))
def cmis_browser_object(
self,
access_point_id: str,
object_id: str | None,
context: OperationContext,
) -> dict[str, Any]:
if object_id in (None, "", "cmis-root", "root", "/"):
return self.cmis_browser_root_object(access_point_id)
if object_id.startswith("cmis:folder:"):
folder_path = _cmis_folder_path(object_id) or "/"
return cmis_browser_object(self._cmis_mapper(access_point_id).folder_projection(folder_path))
return cmis_browser_object(self.cmis_object(access_point_id, object_id, context))
def cmis_browser_children(
self,
access_point_id: str,
context: OperationContext,
*,
object_id: str | None = None,
skip_count: int = 0,
max_items: int = 100,
) -> dict[str, Any]:
children = self.cmis_children(
access_point_id,
context,
folder_id=object_id,
skip_count=skip_count,
max_items=max_items,
)
return cmis_browser_object_in_folder_list(children)
def cmis_browser_parents(
self,
access_point_id: str,
object_id: str,
context: OperationContext,
) -> list[dict[str, Any]]:
return cmis_browser_parent_list(self.cmis_object_parents(access_point_id, object_id, context))
def cmis_browser_query(
self,
access_point_id: str,
query: str,
context: OperationContext,
*,
skip_count: int = 0,
max_items: int = 100,
) -> dict[str, Any]:
return cmis_browser_query_result(
self.cmis_query(
access_point_id,
query,
context,
skip_count=skip_count,
max_items=max_items,
)
)
def cmis_children( def cmis_children(
self, self,
access_point_id: str, access_point_id: str,
@@ -700,9 +832,12 @@ class ServiceRuntime:
} }
def _cmis_mapper(self, access_point_id: str) -> CMISDomainMapper: def _cmis_mapper(self, access_point_id: str) -> CMISDomainMapper:
return CMISDomainMapper(self._cmis_access_point(access_point_id))
def _cmis_access_point(self, access_point_id: str) -> CMISAccessPoint:
for profile in _cmis_profiles(): for profile in _cmis_profiles():
if profile.name == access_point_id: if profile.name == access_point_id:
return CMISDomainMapper(_cmis_access_point(profile)) return _cmis_access_point(profile)
raise NotFoundError( raise NotFoundError(
"CMIS access point not found", "CMIS access point not found",
details={"access_point_id": access_point_id, "available": [profile.name for profile in _cmis_profiles()]}, details={"access_point_id": access_point_id, "available": [profile.name for profile in _cmis_profiles()]},
@@ -2074,12 +2209,14 @@ class ServiceRuntime:
def create_app(runtime: ServiceRuntime | None = None): def create_app(runtime: ServiceRuntime | None = None):
try: try:
from fastapi import Depends, FastAPI, Header, HTTPException, Query from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse from fastapi.responses import JSONResponse, StreamingResponse
except ImportError as exc: # pragma: no cover - exercised when optional extra is absent except ImportError as exc: # pragma: no cover - exercised when optional extra is absent
raise RuntimeError( raise RuntimeError(
"FastAPI service dependencies are not installed. Install kontextual-engine[service]." "FastAPI service dependencies are not installed. Install kontextual-engine[service]."
) from exc ) from exc
globals()["Request"] = Request
globals()["StreamingResponse"] = StreamingResponse
runtime = runtime or ServiceRuntime() runtime = runtime or ServiceRuntime()
app = FastAPI( app = FastAPI(
@@ -2205,9 +2342,129 @@ def create_app(runtime: ServiceRuntime | None = None):
def cmis_access_points() -> dict[str, Any]: def cmis_access_points() -> dict[str, Any]:
return response(runtime.cmis_access_points) return response(runtime.cmis_access_points)
def browser_urls(request: Request, access_point_id: str) -> tuple[str, str]:
return (
str(request.url_for("cmis_browser_entry", access_point_id=access_point_id)),
str(request.url_for("cmis_browser_root", access_point_id=access_point_id)),
)
def unsupported_browser_selector(selector: str | None) -> dict[str, Any]:
raise ValidationError(
"Unsupported CMIS Browser Binding selector",
details={
"cmisselector": selector,
"supported": [
"repositoryInfo",
"typeChildren",
"typeDescendants",
"typeDefinition",
"query",
"object",
"children",
"parents",
"properties",
"allowableActions",
"policies",
"content",
],
},
)
@app.get("/cmis/{access_point_id}/browser", tags=["cmis"]) @app.get("/cmis/{access_point_id}/browser", tags=["cmis"])
def cmis_repository_info(access_point_id: str) -> dict[str, Any]: def cmis_browser_entry(
return response(runtime.cmis_repository_info, access_point_id) access_point_id: str,
request: Request,
cmisselector: str | None = Query(None),
typeId: str | None = Query(None),
includePropertyDefinitions: bool = Query(False),
q: str | None = Query(None),
skipCount: int = Query(0),
maxItems: int = Query(100),
context: OperationContext = Depends(context_from_headers),
) -> Any:
repository_url, root_folder_url = browser_urls(request, access_point_id)
if cmisselector in (None, "", "repositoryInfo"):
return response(
runtime.cmis_browser_service_document,
access_point_id,
repository_url=repository_url,
root_folder_url=root_folder_url,
)
if cmisselector == "typeChildren":
return response(
runtime.cmis_browser_type_children,
access_point_id,
type_id=typeId,
skip_count=skipCount,
max_items=maxItems,
include_property_definitions=includePropertyDefinitions,
)
if cmisselector == "typeDescendants":
return response(
runtime.cmis_browser_type_descendants,
access_point_id,
type_id=typeId,
include_property_definitions=includePropertyDefinitions,
)
if cmisselector == "typeDefinition":
return response(runtime.cmis_browser_type_definition, access_point_id, type_id=typeId)
if cmisselector == "query":
return response(
runtime.cmis_browser_query,
access_point_id,
q or "SELECT * FROM cmis:document",
context,
skip_count=skipCount,
max_items=maxItems,
)
return unsupported_browser_selector(cmisselector)
@app.get("/cmis/{access_point_id}/browser/root", tags=["cmis"])
def cmis_browser_root(
access_point_id: str,
cmisselector: str | None = Query(None),
objectId: str | None = Query(None),
skipCount: int = Query(0),
maxItems: int = Query(100),
context: OperationContext = Depends(context_from_headers),
) -> Any:
if cmisselector in (None, "", "object"):
return response(runtime.cmis_browser_object, access_point_id, objectId, context)
if cmisselector == "children":
return response(
runtime.cmis_browser_children,
access_point_id,
context,
object_id=objectId,
skip_count=skipCount,
max_items=maxItems,
)
if cmisselector == "parents":
if not objectId:
return []
return response(runtime.cmis_browser_parents, access_point_id, objectId, context)
if cmisselector == "properties":
return response(runtime.cmis_browser_object, access_point_id, objectId, context)["properties"]
if cmisselector == "allowableActions":
return response(runtime.cmis_browser_object, access_point_id, objectId, context)["allowableActions"]
if cmisselector == "policies":
return []
if cmisselector == "content":
if not objectId:
return unsupported_browser_selector(cmisselector)
result = response(runtime.cmis_content_stream_bytes, access_point_id, objectId, context)
representation = result.representation
return StreamingResponse(
result.chunks,
media_type=representation.media_type,
headers={
"Content-Length": str(representation.size_bytes),
"ETag": representation.digest,
"X-Kontextual-Representation-Id": representation.representation_id,
"X-Kontextual-Storage-Ref": representation.storage_ref or "",
},
)
return unsupported_browser_selector(cmisselector)
@app.get("/cmis/{access_point_id}/browser/types", tags=["cmis"]) @app.get("/cmis/{access_point_id}/browser/types", tags=["cmis"])
def cmis_types(access_point_id: str) -> dict[str, Any]: def cmis_types(access_point_id: str) -> dict[str, Any]:
@@ -2251,7 +2508,7 @@ def create_app(runtime: ServiceRuntime | None = None):
access_point_id: str, access_point_id: str,
object_id: str, object_id: str,
context: OperationContext = Depends(context_from_headers), context: OperationContext = Depends(context_from_headers),
) -> StreamingResponse: ) -> Any:
result = response(runtime.cmis_content_stream_bytes, access_point_id, object_id, context) result = response(runtime.cmis_content_stream_bytes, access_point_id, object_id, context)
representation = result.representation representation = result.representation
return StreamingResponse( return StreamingResponse(
@@ -2391,7 +2648,7 @@ def create_app(runtime: ServiceRuntime | None = None):
asset_id: str, asset_id: str,
representation_id: str, representation_id: str,
context: OperationContext = Depends(context_from_headers), context: OperationContext = Depends(context_from_headers),
) -> StreamingResponse: ) -> Any:
result = response(runtime.representation_content_stream, asset_id, representation_id, context) result = response(runtime.representation_content_stream, asset_id, representation_id, context)
representation = result.representation representation = result.representation
return StreamingResponse( return StreamingResponse(
@@ -2772,9 +3029,10 @@ def _cmis_profiles() -> tuple[CMISAccessProfile, ...]:
def _cmis_access_point(profile: CMISAccessProfile) -> CMISAccessPoint: def _cmis_access_point(profile: CMISAccessProfile) -> CMISAccessPoint:
repository_id = profile.name if profile.name == "compat-tck" else f"kontextual-{profile.name}"
return CMISAccessPoint( return CMISAccessPoint(
access_point_id=profile.name, access_point_id=profile.name,
repository_id=f"kontextual-{profile.name}", repository_id=repository_id,
profile=profile, profile=profile,
base_path=f"/cmis/{profile.name}/browser", base_path=f"/cmis/{profile.name}/browser",
metadata={"repository_name": f"Kontextual Engine {profile.name}"}, metadata={"repository_name": f"Kontextual Engine {profile.name}"},

View File

@@ -649,6 +649,7 @@ class CMISDomainMapper:
"common_name": "Profiled CMIS access points", "common_name": "Profiled CMIS access points",
"version_label": "1.0", "version_label": "1.0",
"description": "Multiple Browser Binding access points can expose different governed profile slices.", "description": "Multiple Browser Binding access points can expose different governed profile slices.",
"url": "https://docs.oasis-open.org/cmis/CMIS/v1.1/CMIS-v1.1.html",
}, },
{ {
"id": "urn:kontextual:cmis:feature:projection-parentage", "id": "urn:kontextual:cmis:feature:projection-parentage",
@@ -658,6 +659,7 @@ class CMISDomainMapper:
"Assets may appear under multiple virtual folder projections; CMIS multi-filing mutation " "Assets may appear under multiple virtual folder projections; CMIS multi-filing mutation "
"services are not advertised." "services are not advertised."
), ),
"url": "https://docs.oasis-open.org/cmis/CMIS/v1.1/CMIS-v1.1.html",
}, },
] ]
@@ -824,6 +826,8 @@ class CMISDomainMapper:
def folder_projection(self, path: str) -> dict[str, Any]: def folder_projection(self, path: str) -> dict[str, Any]:
normalized = _normalize_path(path) normalized = _normalize_path(path)
parent = _parent_path(normalized)
parent_id = self.access_point.root_folder_id if parent == "/" else self.folder_object_id(parent)
return { return {
"object_id": self.folder_object_id(normalized), "object_id": self.folder_object_id(normalized),
"base_type_id": CMISBaseType.FOLDER.value, "base_type_id": CMISBaseType.FOLDER.value,
@@ -835,6 +839,18 @@ class CMISDomainMapper:
"cmis:name": _path_name(normalized), "cmis:name": _path_name(normalized),
"cmis:baseTypeId": CMISBaseType.FOLDER.value, "cmis:baseTypeId": CMISBaseType.FOLDER.value,
"cmis:objectTypeId": "kontextual:folder", "cmis:objectTypeId": "kontextual:folder",
"cmis:createdBy": "system",
"cmis:lastModifiedBy": "system",
"cmis:creationDate": "1970-01-01T00:00:00+00:00",
"cmis:lastModificationDate": "1970-01-01T00:00:00+00:00",
"cmis:changeToken": f"folder:{normalized}",
"cmis:description": "Virtual CMIS folder projection",
"cmis:secondaryObjectTypeIds": [],
"cmis:parentId": parent_id,
"cmis:path": normalized,
"cmis:allowedChildObjectTypeIds": [CMISBaseType.DOCUMENT.value, CMISBaseType.FOLDER.value],
"kontextual:sensitivity": "internal",
"kontextual:lifecycle": LifecycleState.ACTIVE.value,
"kontextual:filingSource": _filing_source(normalized), "kontextual:filingSource": _filing_source(normalized),
}, },
"allowable_actions": [CMISAction.GET_CHILDREN.value], "allowable_actions": [CMISAction.GET_CHILDREN.value],
@@ -946,6 +962,623 @@ class CMISDomainMapper:
return tuple(actions) return tuple(actions)
def cmis_browser_service_document(
repository_info: dict[str, Any],
*,
repository_url: str,
root_folder_url: str,
) -> dict[str, Any]:
"""Serialize native repository info as a CMIS Browser Binding service root."""
browser_info = cmis_browser_repository_info(
repository_info,
repository_url=repository_url,
root_folder_url=root_folder_url,
)
return {browser_info["repositoryId"]: browser_info}
def cmis_browser_repository_info(
repository_info: dict[str, Any],
*,
repository_url: str,
root_folder_url: str,
) -> dict[str, Any]:
capabilities = repository_info.get("capabilities", {})
return compact_dict(
{
"repositoryId": repository_info.get("repository_id"),
"repositoryName": repository_info.get("repository_name"),
"repositoryDescription": repository_info.get("repository_description"),
"vendorName": repository_info.get("vendor_name"),
"productName": repository_info.get("product_name"),
"productVersion": repository_info.get("product_version"),
"rootFolderId": repository_info.get("root_folder_id"),
"repositoryUrl": repository_url,
"rootFolderUrl": root_folder_url,
"capabilities": cmis_browser_capabilities(capabilities),
"aclCapabilities": _browser_acl_capabilities(),
"latestChangeLogToken": "0",
"cmisVersionSupported": repository_info.get("cmis_version_supported"),
"thinClientURI": repository_url,
"changesIncomplete": True,
"changesOnType": ["cmis:document", "cmis:folder"],
"principalIdAnonymous": repository_info.get("principal_anonymous", "anonymous"),
"principalIdAnyone": repository_info.get("principal_anyone", "anyone"),
"extendedFeatures": cmis_browser_extended_features(repository_info),
}
)
def cmis_browser_capabilities(capabilities: dict[str, Any]) -> dict[str, Any]:
settable = capabilities.get("capability_new_type_settable_attributes", {})
return {
"capabilityContentStreamUpdatability": capabilities.get(
"capability_content_stream_updatability",
"none",
),
"capabilityChanges": capabilities.get("capability_changes", "none"),
"capabilityRenditions": capabilities.get("capability_renditions", "none"),
"capabilityGetDescendants": bool(capabilities.get("capability_get_descendants", False)),
"capabilityGetFolderTree": bool(capabilities.get("capability_get_folder_tree", False)),
"capabilityMultifiling": bool(capabilities.get("capability_multifiling", False)),
"capabilityUnfiling": bool(capabilities.get("capability_unfiling", False)),
"capabilityVersionSpecificFiling": bool(capabilities.get("capability_version_specific_filing", False)),
"capabilityPWCSearchable": bool(capabilities.get("capability_pwc_searchable", False)),
"capabilityPWCUpdatable": bool(capabilities.get("capability_pwc_updatable", False)),
"capabilityAllVersionsSearchable": bool(capabilities.get("capability_all_versions_searchable", False)),
"capabilityOrderBy": capabilities.get("capability_order_by", "none"),
"capabilityQuery": capabilities.get("capability_query", "none"),
"capabilityJoin": capabilities.get("capability_join", "none"),
"capabilityACL": capabilities.get("capability_acl", "none"),
"capabilityCreatablePropertyTypes": {"canCreate": []},
"capabilityNewTypeSettableAttributes": {
"id": bool(settable.get("id", False)),
"localName": bool(settable.get("local_name", False)),
"localNamespace": bool(settable.get("local_namespace", False)),
"displayName": bool(settable.get("display_name", False)),
"queryName": bool(settable.get("query_name", False)),
"description": bool(settable.get("description", False)),
"creatable": bool(settable.get("creatable", False)),
"fileable": bool(settable.get("fileable", False)),
"queryable": bool(settable.get("queryable", False)),
"fulltextIndexed": bool(settable.get("fulltext_indexed", False)),
"includedInSupertypeQuery": bool(settable.get("included_in_supertype_query", False)),
"controllablePolicy": bool(settable.get("controllable_policy", False)),
"controllableACL": bool(settable.get("controllable_acl", False)),
},
}
def cmis_browser_extended_features(repository_info: dict[str, Any]) -> list[dict[str, Any]]:
features = []
for feature in repository_info.get("repository_features", []):
if not isinstance(feature, dict):
continue
features.append(
compact_dict(
{
"id": feature.get("id"),
"commonName": feature.get("common_name"),
"versionLabel": feature.get("version_label"),
"description": feature.get("description"),
"url": feature.get("url", ""),
}
)
)
return features
def cmis_browser_type_definition(
type_definition: dict[str, Any],
*,
include_property_definitions: bool = True,
) -> dict[str, Any]:
base_id = type_definition.get("base_type_id")
type_id = _browser_type_id(type_definition)
is_document = base_id == CMISBaseType.DOCUMENT.value
is_folder = base_id == CMISBaseType.FOLDER.value
property_definitions = dict(type_definition.get("property_definitions", {}))
property_definitions.update(_browser_standard_property_definitions(str(base_id)))
result = {
"id": type_id,
"localName": type_id.split(":", 1)[-1],
"localNamespace": "http://docs.oasis-open.org/ns/cmis/core/200908/",
"displayName": _browser_type_display_name(type_definition),
"queryName": type_id,
"description": type_definition.get("display_name", type_id),
"baseId": base_id,
"parentId": None,
"creatable": bool(type_definition.get("creatable", False)),
"fileable": is_document or is_folder or bool(type_definition.get("fileable", False)),
"queryable": bool(type_definition.get("queryable", is_document)),
"fulltextIndexed": bool(type_definition.get("fulltext_indexed", False)),
"includedInSupertypeQuery": bool(type_definition.get("included_in_supertype_query", is_document)),
"controllablePolicy": bool(type_definition.get("controllable_policy", False)),
"controllableACL": bool(type_definition.get("controllable_acl", is_document)),
"typeMutability": {"create": False, "update": False, "delete": False},
}
if include_property_definitions:
result["propertyDefinitions"] = {
key: cmis_browser_property_definition(key, value)
for key, value in property_definitions.items()
}
if base_id == CMISBaseType.DOCUMENT.value:
result["versionable"] = bool(type_definition.get("versionable", False))
result["contentStreamAllowed"] = "allowed"
if base_id == CMISBaseType.RELATIONSHIP.value:
result["allowedSourceTypes"] = [CMISBaseType.DOCUMENT.value]
result["allowedTargetTypes"] = [CMISBaseType.DOCUMENT.value]
return compact_dict(result)
def cmis_browser_type_children(
type_definitions: list[dict[str, Any]],
*,
type_id: str | None = None,
skip_count: int = 0,
max_items: int = 100,
include_property_definitions: bool = False,
) -> dict[str, Any]:
definitions = _browser_type_definitions(
type_definitions,
include_property_definitions=include_property_definitions,
)
if type_id:
definitions = [
definition
for definition in definitions
if definition.get("parentId") == type_id
]
start = max(skip_count, 0)
limit = max(max_items, 0)
paged = definitions[start : start + limit]
return {
"types": paged,
"hasMoreItems": len(definitions) > start + len(paged),
"numItems": len(paged),
}
def cmis_browser_type_descendants(
type_definitions: list[dict[str, Any]],
*,
type_id: str | None = None,
include_property_definitions: bool = False,
) -> list[dict[str, Any]]:
definitions = _browser_type_definitions(
type_definitions,
include_property_definitions=include_property_definitions,
)
if type_id:
definitions = [
definition
for definition in definitions
if definition.get("parentId") == type_id
]
return [{"type": definition, "children": []} for definition in definitions]
def cmis_browser_type_definition_by_id(
type_definitions: list[dict[str, Any]],
type_id: str | None,
*,
include_property_definitions: bool = True,
) -> dict[str, Any]:
definitions = _browser_type_definitions(
type_definitions,
include_property_definitions=include_property_definitions,
)
if type_id is None:
return definitions[0]
for definition in definitions:
if definition["id"] == type_id:
return definition
for definition in definitions:
if definition.get("baseId") == type_id:
return definition
raise KeyError(type_id)
def cmis_browser_object(projection: dict[str, Any]) -> dict[str, Any]:
properties = _browser_object_properties(projection)
result = {
"properties": {
key: cmis_browser_property_value(key, value)
for key, value in properties.items()
},
"succinctProperties": properties,
"allowableActions": cmis_browser_allowable_actions(projection.get("allowable_actions", [])),
}
acl = projection.get("acl")
if isinstance(acl, dict) and acl:
result["acl"] = cmis_browser_acl(acl)
result["exactACL"] = bool(acl.get("is_exact", True))
return compact_dict(result)
def cmis_browser_object_in_folder_list(children: dict[str, Any]) -> dict[str, Any]:
objects = []
for item in children.get("objects", []):
objects.append(
{
"object": cmis_browser_object(item),
"pathSegment": item.get("name") or item.get("object_id"),
}
)
return {
"objects": objects,
"hasMoreItems": bool(children.get("has_more_items", False)),
"numItems": int(children.get("num_items", len(objects))),
}
def cmis_browser_object_list(objects: list[dict[str, Any]], *, has_more_items: bool = False) -> dict[str, Any]:
return {
"objects": [cmis_browser_object(item) for item in objects],
"hasMoreItems": has_more_items,
"numItems": len(objects),
}
def cmis_browser_parent_list(parents: dict[str, Any]) -> list[dict[str, Any]]:
items = []
for parent in parents.get("parents", []):
items.append(
{
"object": cmis_browser_object(parent),
"relativePathSegment": parent.get("name"),
}
)
return items
def cmis_browser_query_result(query_result: dict[str, Any]) -> dict[str, Any]:
results = [{"succinctProperties": _browser_object_properties(item)} for item in query_result.get("results", [])]
return {
"results": results,
"hasMoreItems": bool(query_result.get("has_more_items", False)),
"numItems": int(query_result.get("num_items", len(results))),
}
def cmis_browser_acl(acl: dict[str, Any]) -> dict[str, Any]:
aces = []
for ace in acl.get("aces", []):
aces.append(
{
"principal": {"principalId": ace.get("principal_id")},
"permissions": list(ace.get("permissions", [])),
"isDirect": bool(ace.get("direct", True)),
}
)
return {"aces": aces, "isExact": bool(acl.get("is_exact", True))}
def cmis_browser_property_definition(
property_id: str,
definition: dict[str, Any],
) -> dict[str, Any]:
local_name = property_id.split(":", 1)[-1]
return {
"id": property_id,
"localName": local_name,
"displayName": local_name,
"queryName": property_id,
"description": property_id,
"propertyType": definition.get("property_type", "string"),
"cardinality": definition.get("cardinality", "single"),
"updatability": definition.get(
"updatability",
"readwrite" if not property_id.startswith("cmis:") else "readonly",
),
"inherited": bool(definition.get("inherited", False)),
"required": bool(definition.get("required", False)),
"queryable": bool(definition.get("queryable", True)),
"orderable": bool(definition.get("orderable", False)),
"openChoice": bool(definition.get("open_choice", True)),
}
def cmis_browser_property_value(property_id: str, value: Any) -> dict[str, Any]:
local_name = property_id.split(":", 1)[-1]
return {
"id": property_id,
"localName": local_name,
"displayName": local_name,
"queryName": property_id,
"type": _browser_property_type(property_id, value),
"cardinality": "multi" if isinstance(value, list) else "single",
"value": value,
}
def cmis_browser_allowable_actions(actions: list[str] | tuple[str, ...]) -> dict[str, bool]:
native = set(actions)
return {
"canGetObjectParents": CMISAction.GET_OBJECT_PARENTS.value in native,
"canGetProperties": CMISAction.GET_OBJECT.value in native,
"canGetObjectRelationships": CMISAction.GET_RELATIONSHIPS.value in native,
"canGetContentStream": CMISAction.GET_CONTENT_STREAM.value in native,
"canGetACL": CMISAction.GET_ACL.value in native,
"canUpdateProperties": CMISAction.UPDATE_PROPERTIES.value in native,
"canDeleteObject": CMISAction.DELETE_OBJECT.value in native,
"canSetContentStream": CMISAction.SET_CONTENT_STREAM.value in native,
"canGetChildren": CMISAction.GET_CHILDREN.value in native,
"canCreateDocument": CMISAction.CREATE_DOCUMENT.value in native,
"canCreateFolder": False,
"canCreateRelationship": False,
"canCreateItem": False,
"canDeleteTree": False,
"canGetDescendants": False,
"canGetFolderTree": False,
"canGetFolderParent": False,
"canGetRenditions": False,
"canMoveObject": False,
"canAddObjectToFolder": False,
"canRemoveObjectFromFolder": False,
"canCheckOut": False,
"canCancelCheckOut": False,
"canCheckIn": False,
"canGetAllVersions": False,
"canApplyPolicy": False,
"canRemovePolicy": False,
"canGetAppliedPolicies": False,
"canApplyACL": False,
"canDeleteContentStream": False,
}
def cmis_browser_root_folder(access_point: CMISAccessPoint) -> dict[str, Any]:
return {
"object_id": access_point.root_folder_id,
"base_type_id": CMISBaseType.FOLDER.value,
"type_id": CMISBaseType.FOLDER.value,
"name": "root",
"path": "/",
"properties": {
"cmis:objectId": access_point.root_folder_id,
"cmis:name": "root",
"cmis:baseTypeId": CMISBaseType.FOLDER.value,
"cmis:objectTypeId": CMISBaseType.FOLDER.value,
"cmis:path": "/",
"cmis:createdBy": "system",
"cmis:lastModifiedBy": "system",
"cmis:creationDate": "1970-01-01T00:00:00+00:00",
"cmis:lastModificationDate": "1970-01-01T00:00:00+00:00",
"cmis:changeToken": "root",
"cmis:description": "CMIS root folder",
"cmis:secondaryObjectTypeIds": [],
"cmis:parentId": None,
"cmis:allowedChildObjectTypeIds": [CMISBaseType.DOCUMENT.value, CMISBaseType.FOLDER.value],
"kontextual:sensitivity": "internal",
"kontextual:lifecycle": LifecycleState.ACTIVE.value,
},
"allowable_actions": [CMISAction.GET_OBJECT.value, CMISAction.GET_CHILDREN.value],
}
def _browser_acl_capabilities() -> dict[str, Any]:
return {
"supportedPermissions": "basic",
"propagation": "objectonly",
"permissions": [
{"permission": "cmis:read", "description": "Read"},
{"permission": "cmis:write", "description": "Write"},
{"permission": "cmis:all", "description": "All"},
],
"permissionMapping": [
{"key": "canGetProperties.Object", "permission": ["cmis:read"]},
{"key": "canViewContent.Object", "permission": ["cmis:read"]},
{"key": "canGetChildren.Folder", "permission": ["cmis:read"]},
{"key": "canGetParents.Folder", "permission": ["cmis:read"]},
{"key": "canUpdateProperties.Object", "permission": ["cmis:write"]},
{"key": "canDelete.Object", "permission": ["cmis:write"]},
{"key": "canSetContent.Document", "permission": ["cmis:write"]},
],
}
def _browser_type_definitions(
type_definitions: list[dict[str, Any]],
*,
include_property_definitions: bool = True,
) -> list[dict[str, Any]]:
by_base: dict[str, dict[str, Any]] = {}
for definition in type_definitions:
base_id = definition.get("base_type_id")
if isinstance(base_id, str) and base_id not in by_base:
by_base[base_id] = cmis_browser_type_definition(
definition,
include_property_definitions=include_property_definitions,
)
order = [
CMISBaseType.DOCUMENT.value,
CMISBaseType.FOLDER.value,
CMISBaseType.RELATIONSHIP.value,
CMISBaseType.POLICY.value,
CMISBaseType.ITEM.value,
CMISBaseType.SECONDARY.value,
]
return [by_base[item] for item in order if item in by_base]
def _browser_type_id(type_definition: dict[str, Any]) -> str:
base_id = type_definition.get("base_type_id")
if isinstance(base_id, str) and base_id.startswith("cmis:"):
return base_id
return str(type_definition.get("id"))
def _browser_type_display_name(type_definition: dict[str, Any]) -> str:
base_id = type_definition.get("base_type_id")
if base_id == CMISBaseType.DOCUMENT.value:
return "Document"
if base_id == CMISBaseType.FOLDER.value:
return "Folder"
if base_id == CMISBaseType.RELATIONSHIP.value:
return "Relationship"
if base_id == CMISBaseType.POLICY.value:
return "Policy"
if base_id == CMISBaseType.ITEM.value:
return "Item"
if base_id == CMISBaseType.SECONDARY.value:
return "Secondary"
return str(type_definition.get("display_name", type_definition.get("id", "CMIS Type")))
def _browser_standard_property_definitions(base_id: str) -> dict[str, dict[str, Any]]:
common = {
"cmis:objectId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:name": _browser_propdef("string", required=True, updatability="readwrite"),
"cmis:baseTypeId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:objectTypeId": _browser_propdef("id", required=True, updatability="oncreate"),
"cmis:createdBy": _browser_propdef(
"string",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:creationDate": _browser_propdef(
"datetime",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:lastModifiedBy": _browser_propdef(
"string",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:lastModificationDate": _browser_propdef(
"datetime",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:changeToken": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:description": _browser_propdef("string", required=False, updatability="readwrite"),
"cmis:secondaryObjectTypeIds": _browser_propdef(
"id",
cardinality="multi",
required=False,
updatability="readwrite",
),
}
if base_id == CMISBaseType.FOLDER.value:
common.update(
{
"cmis:parentId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:path": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:allowedChildObjectTypeIds": _browser_propdef(
"id",
cardinality="multi",
required=False,
updatability="readonly",
),
}
)
if base_id == CMISBaseType.DOCUMENT.value:
common.update(
{
"cmis:isImmutable": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:isLatestVersion": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:isMajorVersion": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:isLatestMajorVersion": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:versionLabel": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:versionSeriesId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:isVersionSeriesCheckedOut": _browser_propdef(
"boolean",
required=False,
updatability="readonly",
),
"cmis:isPrivateWorkingCopy": _browser_propdef(
"boolean",
required=False,
updatability="readonly",
),
"cmis:versionSeriesCheckedOutBy": _browser_propdef(
"string",
required=False,
updatability="readonly",
),
"cmis:versionSeriesCheckedOutId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:checkinComment": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:contentStreamLength": _browser_propdef("integer", required=False, updatability="readonly"),
"cmis:contentStreamMimeType": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:contentStreamFileName": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:contentStreamId": _browser_propdef("id", required=False, updatability="readonly"),
}
)
if base_id == CMISBaseType.RELATIONSHIP.value:
common.update(
{
"cmis:sourceId": _browser_propdef("id", required=True, updatability="readonly"),
"cmis:targetId": _browser_propdef("id", required=True, updatability="readonly"),
}
)
if base_id == CMISBaseType.POLICY.value:
common["cmis:policyText"] = _browser_propdef("string", required=False, updatability="readonly")
return common
def _browser_propdef(
property_type: str,
*,
cardinality: str = "single",
required: bool = False,
updatability: str = "readonly",
queryable: bool = True,
orderable: bool = False,
open_choice: bool = True,
) -> dict[str, Any]:
return {
"property_type": property_type,
"cardinality": cardinality,
"required": required,
"updatability": updatability,
"queryable": queryable,
"orderable": orderable,
"open_choice": open_choice,
}
def _browser_object_properties(projection: dict[str, Any]) -> dict[str, Any]:
properties = dict(projection.get("properties", {}))
base_type = projection.get("base_type_id") or properties.get("cmis:baseTypeId")
properties["cmis:objectId"] = projection.get("object_id", properties.get("cmis:objectId"))
properties["cmis:name"] = projection.get("name", properties.get("cmis:name"))
properties["cmis:baseTypeId"] = base_type
if base_type in {CMISBaseType.DOCUMENT.value, CMISBaseType.FOLDER.value}:
properties["cmis:objectTypeId"] = base_type
else:
properties["cmis:objectTypeId"] = projection.get("type_id", properties.get("cmis:objectTypeId"))
return properties
def _browser_property_type(property_id: str, value: Any) -> str:
if property_id in {
"cmis:objectId",
"cmis:baseTypeId",
"cmis:objectTypeId",
"cmis:contentStreamId",
"cmis:sourceId",
"cmis:targetId",
}:
return "id"
if property_id in {"cmis:creationDate", "cmis:lastModificationDate"}:
return "datetime"
if property_id == "cmis:contentStreamLength":
return "integer"
if isinstance(value, bool):
return "boolean"
if isinstance(value, int):
return "integer"
if isinstance(value, float):
return "decimal"
return "string"
def _read_capabilities() -> tuple[CMISCapability, ...]: def _read_capabilities() -> tuple[CMISCapability, ...]:
return ( return (
CMISCapability.REPOSITORY, CMISCapability.REPOSITORY,

View File

@@ -4,7 +4,7 @@
"tool": "Apache Chemistry OpenCMIS TCK", "tool": "Apache Chemistry OpenCMIS TCK",
"tool_version": "1.1.0", "tool_version": "1.1.0",
"profile": "compat-tck", "profile": "compat-tck",
"repository_id": "kontextual-compat-tck", "repository_id": "compat-tck",
"browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser", "browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser",
"started_at": null, "started_at": null,
"finished_at": null "finished_at": null
@@ -25,4 +25,3 @@
], ],
"capability_gaps": [] "capability_gaps": []
} }

View File

@@ -8,7 +8,7 @@
}, },
"target": { "target": {
"profile": "compat-tck", "profile": "compat-tck",
"repository_id": "kontextual-compat-tck", "repository_id": "compat-tck",
"browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser" "browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser"
}, },
"groups": [ "groups": [

View File

@@ -79,6 +79,7 @@ def test_cmis_browser_binding_routes_are_advertised_in_openapi(cmis_client) -> N
assert "/cmis" in paths assert "/cmis" in paths
assert "/cmis/{access_point_id}/browser" in paths assert "/cmis/{access_point_id}/browser" in paths
assert "/cmis/{access_point_id}/browser/root" in paths
assert "/cmis/{access_point_id}/browser/types" in paths assert "/cmis/{access_point_id}/browser/types" in paths
assert "/cmis/{access_point_id}/browser/children" in paths assert "/cmis/{access_point_id}/browser/children" in paths
assert "/cmis/{access_point_id}/browser/object/{object_id}" in paths assert "/cmis/{access_point_id}/browser/object/{object_id}" in paths
@@ -93,15 +94,45 @@ def test_cmis_browser_binding_routes_are_advertised_in_openapi(cmis_client) -> N
def test_cmis_repository_info_and_type_definitions(cmis_client) -> None: def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
access_points = cmis_client.get("/cmis").json() access_points = cmis_client.get("/cmis").json()
repository = cmis_client.get("/cmis/readonly-browser/browser").json() service_document = cmis_client.get("/cmis/readonly-browser/browser").json()
repository = service_document["kontextual-readonly-browser"]
types = cmis_client.get("/cmis/readonly-browser/browser/types").json() types = cmis_client.get("/cmis/readonly-browser/browser/types").json()
browser_types = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeChildren"},
).json()
browser_types_with_properties = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeChildren", "includePropertyDefinitions": "true"},
).json()
browser_type_descendants = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeDescendants"},
).json()
browser_type_definition = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeDefinition", "typeId": "cmis:document"},
).json()
root_policies = cmis_client.get(
"/cmis/readonly-browser/browser/root",
params={"cmisselector": "policies"},
).json()
assert access_points["count"] == 4 assert access_points["count"] == 4
assert repository["repository_id"] == "kontextual-readonly-browser" assert repository["repositoryId"] == "kontextual-readonly-browser"
assert repository["cmis_version_supported"] == "1.1" assert repository["cmisVersionSupported"] == "1.1"
assert repository["capabilities"]["capability_query"] == "metadataonly" assert repository["repositoryUrl"].endswith("/cmis/readonly-browser/browser")
assert repository["capabilities"]["capability_get_descendants"] is False assert repository["rootFolderUrl"].endswith("/cmis/readonly-browser/browser/root")
assert repository["unsupported_features"]["multifiling"]["status"] == "projection_only" assert repository["capabilities"]["capabilityQuery"] == "metadataonly"
assert repository["capabilities"]["capabilityGetDescendants"] is False
assert browser_types["types"][0]["id"] == "cmis:document"
assert "propertyDefinitions" not in browser_types["types"][0]
assert "propertyDefinitions" in browser_types_with_properties["types"][0]
assert browser_type_descendants[0]["type"]["id"] == "cmis:document"
assert "propertyDefinitions" not in browser_type_descendants[0]["type"]
assert "propertyDefinitions" in browser_type_definition
assert browser_type_descendants[0]["children"] == []
assert root_policies == []
assert {item["base_type_id"] for item in types["items"]} >= { assert {item["base_type_id"] for item in types["items"]} >= {
"cmis:document", "cmis:document",
"cmis:folder", "cmis:folder",
@@ -110,7 +141,11 @@ def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
def test_cmis_readonly_children_object_content_query_relationships_and_changes(cmis_client) -> None: def test_cmis_readonly_children_object_content_query_relationships_and_changes(cmis_client) -> None:
children = cmis_client.get("/cmis/readonly-browser/browser/children").json() root_children = cmis_client.get("/cmis/readonly-browser/browser/children").json()
children = cmis_client.get(
"/cmis/readonly-browser/browser/children",
params={"folder_id": "cmis:folder:assets::document"},
).json()
object_response = cmis_client.get( object_response = cmis_client.get(
"/cmis/readonly-browser/browser/object/cmis:asset:asset-source" "/cmis/readonly-browser/browser/object/cmis:asset:asset-source"
).json() ).json()
@@ -127,7 +162,9 @@ def test_cmis_readonly_children_object_content_query_relationships_and_changes(c
).json() ).json()
changes = cmis_client.get("/cmis/readonly-browser/browser/changes").json() changes = cmis_client.get("/cmis/readonly-browser/browser/changes").json()
root_ids = {item["object_id"] for item in root_children["objects"]}
child_ids = {item["object_id"] for item in children["objects"]} child_ids = {item["object_id"] for item in children["objects"]}
assert "cmis:folder:assets" in root_ids
assert "cmis:asset:asset-source" in child_ids assert "cmis:asset:asset-source" in child_ids
assert "cmis:asset:asset-public" in child_ids assert "cmis:asset:asset-public" in child_ids
assert "cmis:asset:asset-confidential" not in child_ids assert "cmis:asset:asset-confidential" not in child_ids
@@ -141,10 +178,14 @@ def test_cmis_readonly_children_object_content_query_relationships_and_changes(c
def test_cmis_profile_gates_visibility_by_access_point(cmis_client) -> None: def test_cmis_profile_gates_visibility_by_access_point(cmis_client) -> None:
readonly = cmis_client.get("/cmis/readonly-browser/browser/children").json() readonly = cmis_client.get(
"/cmis/readonly-browser/browser/children",
params={"folder_id": "cmis:folder:assets::document"},
).json()
admin_denied = cmis_client.get("/cmis/admin-export/browser/children") admin_denied = cmis_client.get("/cmis/admin-export/browser/children")
admin_allowed = cmis_client.get( admin_allowed = cmis_client.get(
"/cmis/admin-export/browser/children", "/cmis/admin-export/browser/children",
params={"folder_id": "cmis:folder:assets::document"},
headers={"X-Actor-Type": "service_account", "X-Actor-Id": "svc-export"}, headers={"X-Actor-Type": "service_account", "X-Actor-Id": "svc-export"},
).json() ).json()

View File

@@ -4,7 +4,7 @@ type: workplan
title: "CMIS Browser Binding TCK Compatibility" title: "CMIS Browser Binding TCK Compatibility"
domain: markitect domain: markitect
repo: kontextual-engine repo: kontextual-engine
status: planned status: completed
owner: codex owner: codex
topic_slug: markitect topic_slug: markitect
planning_priority: high planning_priority: high
@@ -38,6 +38,7 @@ The result is persisted in
## References ## References
- `docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md` - `docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md`
- `docs/cmis-opencmis-tck-implementation-evidence-2026-05-08T092113Z.md`
- `docs/cmis-1-1-capability-scorecard.md` - `docs/cmis-1-1-capability-scorecard.md`
- `docs/cmis-compliance-assessment.md` - `docs/cmis-compliance-assessment.md`
- `docs/cmis-profiled-access-points-implementation.md` - `docs/cmis-profiled-access-points-implementation.md`
@@ -70,11 +71,25 @@ adds them explicitly.
- Updated CMIS scorecard separates TCK-backed compatibility from native - Updated CMIS scorecard separates TCK-backed compatibility from native
controlled-client usefulness. controlled-client usefulness.
## Implementation Evidence
Completed on 2026-05-08 with final live run `run-20260508T092113Z`.
- Focused internal CMIS suite: `45 passed`.
- Guide Board/OpenCMIS status: `completed`.
- Repository/type cases: `38 pass`, `2 info`, `2 skipped`, `1 warning`, `0 fail`.
- Object/content cases: `22 skipped`, `0 fail`.
- Latest TCK preparation scorecard: `23.81`, coverage `2/9`.
The only remaining repository/type warning is local HTTP transport. The
object/content skips are caused by the deliberate non-creatable `cmis:folder`
profile boundary, not by a Browser Binding session failure.
## F13.1 - Define Browser Binding protocol contract examples ## F13.1 - Define Browser Binding protocol contract examples
```task ```task
id: KONT-WP-0013-T001 id: KONT-WP-0013-T001
status: todo status: done
priority: high priority: high
state_hub_task_id: "fa2f99fb-51ed-4fba-8f36-ba4b790908e5" state_hub_task_id: "fa2f99fb-51ed-4fba-8f36-ba4b790908e5"
``` ```
@@ -92,7 +107,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T002 id: KONT-WP-0013-T002
status: todo status: done
priority: high priority: high
state_hub_task_id: "39949f5b-32bb-4ec7-b0f6-83a16ce443b7" state_hub_task_id: "39949f5b-32bb-4ec7-b0f6-83a16ce443b7"
``` ```
@@ -111,7 +126,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T003 id: KONT-WP-0013-T003
status: todo status: done
priority: high priority: high
state_hub_task_id: "96e11fe6-68c6-46ce-88b8-bda0b9baf2d5" state_hub_task_id: "96e11fe6-68c6-46ce-88b8-bda0b9baf2d5"
``` ```
@@ -128,7 +143,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T004 id: KONT-WP-0013-T004
status: todo status: done
priority: high priority: high
state_hub_task_id: "06506455-b3c3-4f17-ae8a-8ffebc226f73" state_hub_task_id: "06506455-b3c3-4f17-ae8a-8ffebc226f73"
``` ```
@@ -141,11 +156,16 @@ Acceptance:
generic infrastructure error. generic infrastructure error.
- The preflight reports actionable missing or misshaped fields. - The preflight reports actionable missing or misshaped fields.
Completion note: the engine-side protocol-shape regression is now covered by
Browser Binding route tests and the live OpenCMIS run. A stronger negative
preflight fixture belongs in the sister `open-cmis-tck` repository and remains
a follow-up outside this repo's implementation boundary.
## F13.5 - Fix OpenAPI generation for streaming routes ## F13.5 - Fix OpenAPI generation for streaming routes
```task ```task
id: KONT-WP-0013-T005 id: KONT-WP-0013-T005
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "de73a3ff-ddf9-40bb-b89c-50d6fabdb841" state_hub_task_id: "de73a3ff-ddf9-40bb-b89c-50d6fabdb841"
``` ```
@@ -161,7 +181,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T006 id: KONT-WP-0013-T006
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "1cf8d36c-6127-46cd-b409-dda7a045b8dd" state_hub_task_id: "1cf8d36c-6127-46cd-b409-dda7a045b8dd"
``` ```
@@ -178,7 +198,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T007 id: KONT-WP-0013-T007
status: todo status: done
priority: high priority: high
state_hub_task_id: "38777aea-7d2d-4b30-99b4-5194f2c2f8b0" state_hub_task_id: "38777aea-7d2d-4b30-99b4-5194f2c2f8b0"
``` ```
@@ -194,7 +214,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T008 id: KONT-WP-0013-T008
status: todo status: done
priority: high priority: high
state_hub_task_id: "8e7b2235-d39a-465a-b828-b05eb6bcfce3" state_hub_task_id: "8e7b2235-d39a-465a-b828-b05eb6bcfce3"
``` ```
@@ -209,7 +229,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T009 id: KONT-WP-0013-T009
status: todo status: done
priority: high priority: high
state_hub_task_id: "22af3e39-c217-4f6b-b54a-5ecea3894020" state_hub_task_id: "22af3e39-c217-4f6b-b54a-5ecea3894020"
``` ```
@@ -226,7 +246,7 @@ Acceptance:
```task ```task
id: KONT-WP-0013-T010 id: KONT-WP-0013-T010
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "469a3ea6-a6ab-4e6e-ba4f-5b1abdabd6c4" state_hub_task_id: "469a3ea6-a6ab-4e6e-ba4f-5b1abdabd6c4"
``` ```
@@ -247,3 +267,10 @@ Acceptance:
- Object/content TCK group produces parsed evidence or documented - Object/content TCK group produces parsed evidence or documented
unsupported-by-design gaps. unsupported-by-design gaps.
- Updated documentation clearly states the evidence-backed CMIS posture. - Updated documentation clearly states the evidence-backed CMIS posture.
## Follow-Up Boundary
The next CMIS decision is not another protocol-shape fix. It is whether to
support CMIS `createFolder` in the `compat-tck` profile so OpenCMIS CRUD/content
cases execute instead of skipping, or whether to keep those skips as an honest
profile boundary until a client requires full CMIS CRUD scaffolding.