generated from coulomb/repo-seed
query parsing and diagnostics
This commit is contained in:
@@ -13,6 +13,11 @@ The score below remains a product-depth estimate against mature CMIS products.
|
|||||||
The selected OpenCMIS baseline is now stable preparation evidence for
|
The selected OpenCMIS baseline is now stable preparation evidence for
|
||||||
repository/type and object/content services, not a full CMIS certification.
|
repository/type and object/content services, not a full CMIS certification.
|
||||||
|
|
||||||
|
Read-side contract update: `KONT-WP-0016` adds a documented bounded query
|
||||||
|
subset, common CMIS `ORDER BY`, target/either relationship filters, enriched
|
||||||
|
relationship and ACL projections, and explicit `notSupported` diagnostics for
|
||||||
|
unsupported navigation selectors.
|
||||||
|
|
||||||
Status: baseline scorecard for the current Browser Binding subset.
|
Status: baseline scorecard for the current Browser Binding subset.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -72,9 +77,9 @@ CMIS interoperability importance rather than engine-internal importance.
|
|||||||
| Metric | Score |
|
| Metric | Score |
|
||||||
| --- | ---: |
|
| --- | ---: |
|
||||||
| OpenCMIS selected-baseline infrastructure score | 99.1% |
|
| OpenCMIS selected-baseline infrastructure score | 99.1% |
|
||||||
| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 60% |
|
| Weighted CMIS 1.1 depth vs Hyland Alfresco benchmark | 63% |
|
||||||
| Controlled-client Browser Binding usefulness | 82% |
|
| Controlled-client Browser Binding usefulness | 84% |
|
||||||
| Broad commodity CMIS client compatibility | 55% |
|
| Broad commodity CMIS client compatibility | 57% |
|
||||||
|
|
||||||
Interpretation: the OpenCMIS infrastructure score measures the selected
|
Interpretation: the OpenCMIS infrastructure score measures the selected
|
||||||
`repository-type` and `object-content` harness baseline only. The current CMIS
|
`repository-type` and `object-content` harness baseline only. The current CMIS
|
||||||
@@ -94,9 +99,9 @@ broad ECM/CMIS replacement surface.
|
|||||||
| 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.<br>- No broad filing mutation, raw physical delete, checkout/checkin, or policy/item creation.<br>- Delete remains intentionally governed, not raw repository removal. |
|
| 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.<br>- No broad filing mutation, raw physical delete, checkout/checkin, or policy/item creation.<br>- 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.<br>- Digest verification and governed access exist.<br>- Chunk-level blob composition remains a later optimization for very large append workloads. |
|
| 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.<br>- Digest verification and governed access exist.<br>- 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.<br>- No checkout/checkin/cancelCheckout/PWC services.<br>- No version history route or all-versions query behavior. |
|
| Versioning service | 8 | 25% | Hyland Alfresco ACS | - Version properties can be projected from engine versions.<br>- No checkout/checkin/cancelCheckout/PWC services.<br>- No version history route or all-versions query behavior. |
|
||||||
| Discovery/query | 8 | 25% | Hyland Alfresco ACS | - Narrow document select subset exists.<br>- Unsupported joins/order-by return diagnostics.<br>- Missing CMIS SQL predicates, type joins, full-text, ordering, and rich projection rules. |
|
| Discovery/query | 8 | 42% | Hyland Alfresco ACS | - Bounded `SELECT *` document queries support equality, `LIKE`, `IN`, `AND`, paging, and common CMIS property ordering.<br>- Capability flags now advertise `capabilityOrderBy=common` rather than overclaiming custom ordering.<br>- Missing joins, full text, nested predicates, arbitrary projections, and broad CMIS SQL coverage. |
|
||||||
| Relationships | 5 | 60% | Hyland Alfresco ACS | - Relationship object projection and source filtering exist.<br>- Visibility gates prevent protected relationship leakage.<br>- Missing full relationship service filters, relationship creation through CMIS, and type hierarchy maturity. |
|
| Relationships | 5 | 70% | Hyland Alfresco ACS | - Relationship object projection, source filters, target filters, either-direction filters, confidence, direction, provenance, and visibility gates exist.<br>- Protected relationship leakage is covered by profile gates.<br>- Missing relationship creation through CMIS and deeper relationship type hierarchy maturity. |
|
||||||
| ACL service | 6 | 35% | Hyland Alfresco ACS | - Discover-only ACL projection exists.<br>- `applyACL` is blocked as not implemented.<br>- Missing inherited/direct ACL fidelity, propagation, ACL mutation, and repository principal model. |
|
| ACL service | 6 | 48% | Hyland Alfresco ACS | - Discover-only ACL projection has stable principal ids, principal kinds, permission mapping, direct/inherited markers, and policy authority metadata.<br>- `applyACL` is blocked as not implemented.<br>- Missing propagation, ACL mutation, and repository-wide principal/group enumeration. |
|
||||||
| Policy service | 3 | 10% | Hyland Alfresco ACS | - Native policy decisions govern exposure.<br>- No CMIS policy objects, `applyPolicy`, `removePolicy`, or `getAppliedPolicies` service surface.<br>- Explicitly unsupported. |
|
| Policy service | 3 | 10% | Hyland Alfresco ACS | - Native policy decisions govern exposure.<br>- No CMIS policy objects, `applyPolicy`, `removePolicy`, or `getAppliedPolicies` service surface.<br>- Explicitly unsupported. |
|
||||||
| Change log | 5 | 60% | Hyland Alfresco ACS | - Audit-backed object-id change entries and paging exist.<br>- CMIS change-token conflicts are now enforced for property/content mutations.<br>- Missing richer change event typing and broader token semantics across optional services. |
|
| Change log | 5 | 60% | Hyland Alfresco ACS | - Audit-backed object-id change entries and paging exist.<br>- CMIS change-token conflicts are now enforced for property/content mutations.<br>- Missing richer change event typing and broader token semantics across optional services. |
|
||||||
| Multi-filing and unfiling | 4 | 25% | Hyland Alfresco ACS | - Projection-only parent maps exist and are useful for navigation.<br>- Standard CMIS `capabilityMultifiling` is correctly false.<br>- No add/remove filing mutations or canonical folder membership model. |
|
| Multi-filing and unfiling | 4 | 25% | Hyland Alfresco ACS | - Projection-only parent maps exist and are useful for navigation.<br>- Standard CMIS `capabilityMultifiling` is correctly false.<br>- No add/remove filing mutations or canonical folder membership model. |
|
||||||
@@ -108,7 +113,7 @@ broad ECM/CMIS replacement surface.
|
|||||||
| 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 | 86% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation succeeds against `compat-tck`.<br>- Selected `repository-type` and `object-content` baselines complete with one local transport warning and no object/content warnings.<br>- 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`.<br>- Selected `repository-type` and `object-content` baselines complete with one local transport warning and no object/content warnings.<br>- Evidence still covers a selected baseline, not the full OpenCMIS TCK surface. |
|
||||||
|
|
||||||
Weighted result from this table: **60%**.
|
Weighted result from this table: **63%**.
|
||||||
|
|
||||||
## Most Important Gaps
|
## Most Important Gaps
|
||||||
|
|
||||||
@@ -126,9 +131,10 @@ Weighted result from this table: **60%**.
|
|||||||
optional services are added.
|
optional services are added.
|
||||||
|
|
||||||
3. **Query depth**
|
3. **Query depth**
|
||||||
- Add a real CMIS SQL subset parser instead of a two-query allowlist.
|
- Expand only where it stays natural: additional indexed metadata fields,
|
||||||
- Support basic `WHERE`, equality predicates, paging, ordering where claimed,
|
richer comparator support, and selected client-requested predicates.
|
||||||
and diagnostics for everything outside the subset.
|
- Keep joins, full text, and arbitrary CMIS SQL unsupported unless a real
|
||||||
|
integration need appears.
|
||||||
|
|
||||||
4. **Navigation depth**
|
4. **Navigation depth**
|
||||||
- Decide whether `getDescendants` and `getFolderTree` are worth implementing
|
- Decide whether `getDescendants` and `getFolderTree` are worth implementing
|
||||||
|
|||||||
@@ -50,15 +50,15 @@ Practical strategy:
|
|||||||
| 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 |
|
| 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` and whole-object `appendContentStream` write through deduplicating blob storage, while `deleteContentStream` tombstones the CMIS projection. Chunk-level append composition remains deferred. | 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 |
|
| 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 |
|
| Discovery/query | Implemented bounded subset. | `SELECT *` document queries support equality, `LIKE`, `IN`, `AND`, paging, and common CMIS property ordering. Joins, full text, nested predicates, arbitrary projection lists, and custom-property ordering are flagged unsupported. | Medium |
|
||||||
| Relationships | Implemented subset. | Relationship object projections and source filters are covered and profile-gated. | Low |
|
| Relationships | Implemented subset. | Relationship object projections, source filters, target filters, either-direction filters, provenance, confidence, and profile-gated visibility are covered. | Low |
|
||||||
| ACL service | Discover only. | ACL projection is supported; `applyACL` is not authorized even for authoring profiles and returns an unimplemented diagnostic. | Low |
|
| ACL service | Discover only. | ACL projection is supported with stable principal/permission vocabulary, direct/inherited markers, and policy authority metadata; `applyACL` returns an unimplemented diagnostic. | Low |
|
||||||
| Policy service | Unsupported. | `applyPolicy`/`removePolicy` are explicitly unsupported; engine policy remains native, not CMIS policy objects. | Low |
|
| Policy service | Unsupported. | `applyPolicy`/`removePolicy` are explicitly unsupported; engine policy remains native, not CMIS policy objects. | Low |
|
||||||
| Change log | Implemented subset. | Audit-backed object-id change entries and paging are supported; full property-level change details are not advertised. | Low |
|
| Change log | Implemented subset. | Audit-backed object-id change entries and paging are supported; full property-level change details are not advertised. | Low |
|
||||||
| Multi-filing/unfiling | Projection only. | Multiple virtual parents are exposed as a Kontextual repository feature, while CMIS `capabilityMultifiling` and unfiling stay false. | Low |
|
| Multi-filing/unfiling | Projection only. | Multiple virtual parents are exposed as a Kontextual repository feature, while CMIS `capabilityMultifiling` and unfiling stay false. | Low |
|
||||||
| Renditions | Unsupported. | Capability is `none`; derived representations are not exposed as CMIS rendition streams. | Low |
|
| Renditions | Unsupported. | Capability is `none`; derived representations are not exposed as CMIS rendition streams. | Low |
|
||||||
| Retention and hold | Unsupported. | Not advertised; left as native governance metadata until a real integration requires CMIS legal-hold semantics. | Low |
|
| Retention and hold | Unsupported. | Not advertised; left as native governance metadata until a real integration requires CMIS legal-hold semantics. | Low |
|
||||||
| Bulk update | Unsupported. | `bulkUpdateProperties` is explicitly unsupported. | Low |
|
| Bulk update | Profile-scoped subset. | `bulkUpdateProperties` is available for the TCK compatibility profile through existing governed property updates and change-token handling; it remains narrow and is not enabled on normal authoring profiles. | Low |
|
||||||
| Browser JSON binding | FastAPI JSON service already exists. | Need CMIS Browser Binding routes, selectors/actions, multipart/content stream behavior. | High |
|
| Browser JSON binding | FastAPI JSON service already exists. | Need CMIS Browser Binding routes, selectors/actions, multipart/content stream behavior. | High |
|
||||||
| AtomPub binding | No AtomPub/XML binding. | Need XML/Atom feed generation and protocol semantics. | Very High |
|
| AtomPub binding | No AtomPub/XML binding. | Need XML/Atom feed generation and protocol semantics. | Very High |
|
||||||
| Web Services binding | No SOAP stack. | Need WSDL/SOAP implementation. | Very High |
|
| Web Services binding | No SOAP stack. | Need WSDL/SOAP implementation. | Very High |
|
||||||
@@ -72,7 +72,8 @@ Maintain a constrained CMIS 1.1 Browser Binding profile:
|
|||||||
projection.
|
projection.
|
||||||
- Explicitly unsupported or read-only: AtomPub, Web Services, descendants/tree,
|
- Explicitly unsupported or read-only: AtomPub, Web Services, descendants/tree,
|
||||||
full ACL mutation, retention/hold, mutating multifiling/unfiling, PWC/versioning
|
full ACL mutation, retention/hold, mutating multifiling/unfiling, PWC/versioning
|
||||||
services, renditions, bulk updates, order-by, and full CMIS SQL joins.
|
services, renditions, custom-property ordering, broad bulk-update exposure,
|
||||||
|
and full CMIS SQL joins.
|
||||||
|
|
||||||
Then expand by profile:
|
Then expand by profile:
|
||||||
|
|
||||||
|
|||||||
94
docs/cmis-read-side-contract.md
Normal file
94
docs/cmis-read-side-contract.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# CMIS Read-Side Contract
|
||||||
|
|
||||||
|
Date: 2026-05-14
|
||||||
|
Workplan: `KONT-WP-0016`
|
||||||
|
Status: release-stable Browser Binding subset
|
||||||
|
|
||||||
|
## Boundary
|
||||||
|
|
||||||
|
The CMIS layer is a connector projection over native `kontextual-engine`
|
||||||
|
services. Native assets, classifications, metadata records, relationships,
|
||||||
|
audit events, policy decisions, and blob representations remain authoritative.
|
||||||
|
CMIS does not own a second object store, a separate ACL model, or an independent
|
||||||
|
filing graph.
|
||||||
|
|
||||||
|
## Release-Stable Capabilities
|
||||||
|
|
||||||
|
| Capability | Release posture | Contract |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Repository and type discovery | Supported | Browser Binding service document, repository info, base type definitions, and property definitions are stable. Optional unsupported features are declared in `unsupported_features`. |
|
||||||
|
| Navigation | Bounded supported subset | `children`, `parent`, `parents`, `object`, `properties`, `allowableActions`, `policies`, and `content` are supported through profile-gated folder/path projections. |
|
||||||
|
| Descendants and folder tree | Unsupported | `descendants` and `folderTree` return CMIS `notSupported`; repository flags remain false. This avoids creating a broad ECM tree contract before there is a native need. |
|
||||||
|
| Query | Bounded supported subset | `SELECT * FROM cmis:document` and `SELECT * FROM kontextual:document` support simple `WHERE` predicates joined by `AND`, deterministic paging, and common-property ordering. |
|
||||||
|
| Relationships | Supported read projection | Source, target, and either-direction filters map to the native relationship repository. Relationship objects expose source/target ids, predicate, confidence, direction, provenance, actor, and a stable relationship change token. |
|
||||||
|
| ACL discovery | Supported projection | `getACL` exposes direct actor permissions plus synthetic/public inheritance markers. The native policy gateway remains the authority; `applyACL` stays unsupported. |
|
||||||
|
| Change tokens | Supported for governed mutations | Asset `cmis:changeToken` is the current version id. Folder workspace tokens and relationship tokens are projection tokens. Stale asset mutation tokens map to CMIS `updateConflict`. |
|
||||||
|
|
||||||
|
## Query Subset
|
||||||
|
|
||||||
|
Accepted grammar:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM cmis:document
|
||||||
|
SELECT * FROM kontextual:document
|
||||||
|
SELECT * FROM cmis:document WHERE <field> = '<value>' [AND ...]
|
||||||
|
SELECT * FROM cmis:document WHERE <text-field> LIKE '<pattern>' [AND ...]
|
||||||
|
SELECT * FROM cmis:document WHERE <multi-field> IN ('<value>', ...) [AND ...]
|
||||||
|
SELECT * FROM cmis:document ... ORDER BY <common-cmis-field> [ASC|DESC]
|
||||||
|
```
|
||||||
|
|
||||||
|
Filterable fields:
|
||||||
|
|
||||||
|
`cmis:objectId`, `cmis:name`, `cmis:objectTypeId`, `cmis:baseTypeId`,
|
||||||
|
`cmis:description`, `kontextual:assetId`, `kontextual:assetType`,
|
||||||
|
`kontextual:sensitivity`, `kontextual:lifecycle`, `kontextual:owner`,
|
||||||
|
`kontextual:topics`, and `kontextual:reviewState`.
|
||||||
|
|
||||||
|
Orderable fields:
|
||||||
|
|
||||||
|
`cmis:objectId`, `cmis:name`, `cmis:creationDate`, and
|
||||||
|
`cmis:lastModificationDate`.
|
||||||
|
|
||||||
|
Unsupported joins, `OR`, nested expressions, full text, arbitrary projection
|
||||||
|
lists, and custom-property ordering return CMIS `notSupported` diagnostics with
|
||||||
|
the supported grammar and field sets included.
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
- `capabilityQuery` remains `metadataonly`.
|
||||||
|
- `capabilityOrderBy` is now `common`, not `none`, because common CMIS property
|
||||||
|
ordering is implemented and covered by tests.
|
||||||
|
- `capabilityGetDescendants` and `capabilityGetFolderTree` remain false.
|
||||||
|
- Multifiling remains projection-only. Mutation semantics are deliberately out
|
||||||
|
of scope for this read-side contract.
|
||||||
|
- The ACL vocabulary is intentionally small: `cmis:read`, `cmis:write`, and
|
||||||
|
`cmis:delete`, with `direct`, `inherited`, `principal_kind`, and `source`
|
||||||
|
markers.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Focused tests run on 2026-05-14:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 -m pytest \
|
||||||
|
tests/cmis/test_cmis_runtime_browser_binding.py \
|
||||||
|
tests/cmis/test_cmis_browser_binding_api.py \
|
||||||
|
tests/cmis/test_cmis_compliance_flags.py \
|
||||||
|
tests/cmis/test_cmis_contract_examples.py
|
||||||
|
|
||||||
|
Result: 21 passed, 16 skipped in 5.19s
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser Binding API verification with optional service extras:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.venv/bin/python -m pytest tests/cmis/test_cmis_browser_binding_api.py -q
|
||||||
|
|
||||||
|
Result: 16 passed in 39.09s
|
||||||
|
```
|
||||||
|
|
||||||
|
The default system-Python run skips Browser Binding API tests when FastAPI/HTTPX
|
||||||
|
are unavailable and skips capacity probes unless `KONTEXTUAL_RUN_CAPACITY=1` is
|
||||||
|
set. The capacity probe exercises 400 documents and 250 relationships over
|
||||||
|
query and target-filter paths, while relying on the shared performance history
|
||||||
|
monitor for drift tracking.
|
||||||
@@ -29,11 +29,11 @@ Out of scope for `0.1.0`:
|
|||||||
|
|
||||||
| Area | Gate | Current state |
|
| Area | Gate | Current state |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| CMIS read-side contract | Query, navigation, relationship, ACL, and change-token contracts are release-stable or explicitly waived. | `KONT-WP-0016` created as a pre-release dependency. |
|
| CMIS read-side contract | Query, navigation, relationship, ACL, and change-token contracts are release-stable or explicitly waived. | `KONT-WP-0016` implemented locally; full release verification still required. |
|
||||||
| Tests | Full suite passes in the project venv. | `.venv/bin/python -m pytest -q` passed: `166 passed`, `14 skipped`; advisory performance drift warnings recorded. |
|
| Tests | Full suite passes in the project venv. | `.venv/bin/python -m pytest -q` passed: `166 passed`, `15 skipped`; advisory performance drift warnings recorded. |
|
||||||
| CMIS evidence | OpenCMIS selected baseline completes with no unexpected findings. | `run-20260513T223537Z` completed; only local HTTP warning remains. |
|
| 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. |
|
| 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. |
|
| Capability honesty | Scorecard, unsupported catalog, and examples match behavior. | Updated for `appendContentStream` and WP-0016 read-side contract; 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. |
|
| 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. |
|
| 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. |
|
| Data safety | Blob cleanup, backups, restore path, and migration posture are documented. | Cleanup exists; backup/restore release notes still needed. |
|
||||||
@@ -54,23 +54,21 @@ Out of scope for `0.1.0`:
|
|||||||
|
|
||||||
## Release Procedure
|
## Release Procedure
|
||||||
|
|
||||||
1. Complete or explicitly waive `KONT-WP-0016`.
|
1. Run `.venv/bin/python -m pytest -q`.
|
||||||
2. Run `.venv/bin/python -m pytest -q`.
|
2. Run the OpenCMIS selected baseline through `guide-board` and persist the
|
||||||
3. Run the OpenCMIS selected baseline through `guide-board` and persist the
|
|
||||||
evidence document.
|
evidence document.
|
||||||
4. Run State Hub consistency check and ensure workplans are registered.
|
3. Run State Hub consistency check and ensure workplans are registered.
|
||||||
5. Run packaging smoke: build wheel/sdist and import `kontextual_engine` from a
|
4. Run packaging smoke: build wheel/sdist and import `kontextual_engine` from a
|
||||||
clean install.
|
clean install.
|
||||||
6. Review security/configuration: HTTPS termination, profile exposure, secrets,
|
5. Review security/configuration: HTTPS termination, profile exposure, secrets,
|
||||||
local/S3 blob backend settings, and dependency licenses.
|
local/S3 blob backend settings, and dependency licenses.
|
||||||
7. Update `CHANGELOG.md` or release notes with capabilities, known limitations,
|
6. Update `CHANGELOG.md` or release notes with capabilities, known limitations,
|
||||||
and accepted warnings.
|
and accepted warnings.
|
||||||
8. Tag the release only after the gates above are green or explicitly waived.
|
7. Tag the release only after the gates above are green or explicitly waived.
|
||||||
|
|
||||||
## Release Decision
|
## Release Decision
|
||||||
|
|
||||||
The current foundation is close to a controlled `0.1.0` preview, but CMIS
|
The current foundation is close to a controlled `0.1.0` preview. With the
|
||||||
read-side contracts should be settled before release if external projects will
|
`KONT-WP-0016` read-side contract settled locally, the remaining release work is
|
||||||
build on the engine. After `KONT-WP-0016`, the remaining release work is mainly
|
mainly discipline around repeatable verification, packaging, deployment
|
||||||
discipline around repeatable verification, packaging, deployment posture, and
|
posture, and clearly documented limits.
|
||||||
clearly documented limits.
|
|
||||||
|
|||||||
@@ -125,10 +125,10 @@
|
|||||||
{
|
{
|
||||||
"id": "discovery-query",
|
"id": "discovery-query",
|
||||||
"service": "discovery",
|
"service": "discovery",
|
||||||
"examples": ["lexical-query", "metadata-filter", "relationship-scoped-query", "unsupported-join"],
|
"examples": ["lexical-query", "metadata-filter", "relationship-scoped-query", "bounded-order-by", "unsupported-join"],
|
||||||
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
"supported_profiles": ["readonly-browser", "governed-authoring", "admin-export", "compat-tck"],
|
||||||
"must_validate": ["query_capabilities", "paging", "unsupported_grammar_diagnostics"],
|
"must_validate": ["query_capabilities", "paging", "bounded_order_by", "unsupported_grammar_diagnostics"],
|
||||||
"unsupported": ["full_cmis_sql_joins", "order_by"]
|
"unsupported": ["full_cmis_sql_joins", "custom_order_by"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "relationships",
|
"id": "relationships",
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
"private_working_copy": "capability_not_supported",
|
"private_working_copy": "capability_not_supported",
|
||||||
"all_versions_search": "capability_not_supported",
|
"all_versions_search": "capability_not_supported",
|
||||||
"full_cmis_sql_joins": "query_not_supported",
|
"full_cmis_sql_joins": "query_not_supported",
|
||||||
"order_by": "query_not_supported",
|
"custom_order_by": "custom_order_by_not_supported",
|
||||||
"apply_acl": "operation_not_implemented",
|
"apply_acl": "operation_not_implemented",
|
||||||
"apply_policy": "capability_not_supported",
|
"apply_policy": "capability_not_supported",
|
||||||
"remove_policy": "capability_not_supported",
|
"remove_policy": "capability_not_supported",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ requests into service/runtime contracts and must not own domain behavior.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email import policy
|
from email import policy
|
||||||
@@ -90,6 +91,43 @@ from kontextual_engine.services import (
|
|||||||
API_VERSION = "v1"
|
API_VERSION = "v1"
|
||||||
OPENAPI_VERSION = "1.0.0"
|
OPENAPI_VERSION = "1.0.0"
|
||||||
CMIS_APPEND_MAX_COMPOSED_BYTES = 64 * 1024 * 1024
|
CMIS_APPEND_MAX_COMPOSED_BYTES = 64 * 1024 * 1024
|
||||||
|
CMIS_QUERY_SUPPORTED = [
|
||||||
|
"SELECT * FROM cmis:document",
|
||||||
|
"SELECT * FROM kontextual:document",
|
||||||
|
"SELECT * FROM cmis:document WHERE <filterable-field> = '<value>' [AND ...]",
|
||||||
|
"SELECT * FROM cmis:document WHERE <text-field> LIKE '<pattern>' [AND ...]",
|
||||||
|
"SELECT * FROM cmis:document WHERE <multi-field> IN ('<value>', ...) [AND ...]",
|
||||||
|
"SELECT * FROM cmis:document ... ORDER BY <orderable-field> [ASC|DESC]",
|
||||||
|
]
|
||||||
|
CMIS_QUERY_FILTERABLE_FIELDS = {
|
||||||
|
"cmis:objectId",
|
||||||
|
"cmis:name",
|
||||||
|
"cmis:objectTypeId",
|
||||||
|
"cmis:baseTypeId",
|
||||||
|
"cmis:description",
|
||||||
|
"kontextual:assetId",
|
||||||
|
"kontextual:assetType",
|
||||||
|
"kontextual:sensitivity",
|
||||||
|
"kontextual:lifecycle",
|
||||||
|
"kontextual:owner",
|
||||||
|
"kontextual:topics",
|
||||||
|
"kontextual:reviewState",
|
||||||
|
}
|
||||||
|
CMIS_QUERY_ORDERABLE_FIELDS = {
|
||||||
|
"cmis:objectId",
|
||||||
|
"cmis:name",
|
||||||
|
"cmis:creationDate",
|
||||||
|
"cmis:lastModificationDate",
|
||||||
|
}
|
||||||
|
CMIS_QUERY_LIKE_FIELDS = {
|
||||||
|
"cmis:name",
|
||||||
|
"cmis:description",
|
||||||
|
"kontextual:assetId",
|
||||||
|
"kontextual:assetType",
|
||||||
|
"kontextual:owner",
|
||||||
|
"kontextual:topics",
|
||||||
|
"kontextual:reviewState",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
AGENT_OPERATION_CATALOG: tuple[dict[str, Any], ...] = (
|
AGENT_OPERATION_CATALOG: tuple[dict[str, Any], ...] = (
|
||||||
@@ -1593,19 +1631,13 @@ class ServiceRuntime:
|
|||||||
decision = mapper.access_point.decide_action(CMISAction.QUERY, context)
|
decision = mapper.access_point.decide_action(CMISAction.QUERY, context)
|
||||||
if not decision.allowed:
|
if not decision.allowed:
|
||||||
raise _cmis_authorization_error(decision, "query")
|
raise _cmis_authorization_error(decision, "query")
|
||||||
normalized = query.strip().lower()
|
query_spec = _parse_cmis_query(query)
|
||||||
if normalized not in {"select * from cmis:document", "select * from kontextual:document"}:
|
|
||||||
raise ValidationError(
|
|
||||||
"Unsupported CMIS query subset",
|
|
||||||
details={
|
|
||||||
"query": query,
|
|
||||||
"supported": ["SELECT * FROM cmis:document", "SELECT * FROM kontextual:document"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
projections = self._cmis_document_projections(mapper, context)
|
projections = self._cmis_document_projections(mapper, context)
|
||||||
|
projections = _apply_cmis_query_spec(projections, query_spec)
|
||||||
paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)]
|
paged = projections[max(skip_count, 0) : max(skip_count, 0) + max(max_items, 0)]
|
||||||
return {
|
return {
|
||||||
"query": query,
|
"query": query,
|
||||||
|
"query_spec": query_spec,
|
||||||
"results": paged,
|
"results": paged,
|
||||||
"num_items": len(paged),
|
"num_items": len(paged),
|
||||||
"has_more_items": len(projections) > max(skip_count, 0) + len(paged),
|
"has_more_items": len(projections) > max(skip_count, 0) + len(paged),
|
||||||
@@ -1618,19 +1650,55 @@ class ServiceRuntime:
|
|||||||
context: OperationContext,
|
context: OperationContext,
|
||||||
*,
|
*,
|
||||||
object_id: str | None = None,
|
object_id: str | None = None,
|
||||||
|
target_id: str | None = None,
|
||||||
|
relationship_direction: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
mapper = self._cmis_mapper(access_point_id)
|
mapper = self._cmis_mapper(access_point_id)
|
||||||
decision = mapper.access_point.decide_action(CMISAction.GET_RELATIONSHIPS, context)
|
decision = mapper.access_point.decide_action(CMISAction.GET_RELATIONSHIPS, context)
|
||||||
if not decision.allowed:
|
if not decision.allowed:
|
||||||
raise _cmis_authorization_error(decision, "getRelationships")
|
raise _cmis_authorization_error(decision, "getRelationships")
|
||||||
source_id = _cmis_asset_id(object_id) if object_id else None
|
direction = (relationship_direction or "source").strip().lower()
|
||||||
|
if direction not in {"source", "target", "either"}:
|
||||||
|
raise ValidationError(
|
||||||
|
"Unsupported CMIS relationship direction",
|
||||||
|
details={
|
||||||
|
"code": "cmis.relationship_direction_unsupported",
|
||||||
|
"operation": "getObjectRelationships",
|
||||||
|
"direction": relationship_direction,
|
||||||
|
"supported": ["source", "target", "either"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
source_filter = _cmis_asset_id(object_id) if object_id and direction == "source" else None
|
||||||
|
target_filter = _cmis_asset_id(object_id) if object_id and direction == "target" else None
|
||||||
|
if target_id:
|
||||||
|
target_filter = _cmis_asset_id(target_id)
|
||||||
|
relationships = self.repository.list_relationships(source_id=source_filter, target_id=target_filter)
|
||||||
|
if object_id and direction == "either":
|
||||||
|
asset_id = _cmis_asset_id(object_id)
|
||||||
|
relationships = [
|
||||||
|
relationship
|
||||||
|
for relationship in relationships
|
||||||
|
if relationship.source_id == asset_id
|
||||||
|
or (
|
||||||
|
relationship.target_kind == RelationshipTargetKind.ASSET
|
||||||
|
and relationship.target_id == asset_id
|
||||||
|
)
|
||||||
|
]
|
||||||
projections = [
|
projections = [
|
||||||
projection.to_dict()
|
projection.to_dict()
|
||||||
for relationship in self.repository.list_relationships(source_id=source_id)
|
for relationship in relationships
|
||||||
if self._cmis_relationship_visible(mapper, relationship, context)
|
if self._cmis_relationship_visible(mapper, relationship, context)
|
||||||
if (projection := mapper.map_relationship(relationship, context))
|
if (projection := mapper.map_relationship(relationship, context))
|
||||||
]
|
]
|
||||||
return {"items": projections, "count": len(projections)}
|
return {
|
||||||
|
"items": projections,
|
||||||
|
"count": len(projections),
|
||||||
|
"filters": {
|
||||||
|
"object_id": object_id,
|
||||||
|
"target_id": target_id,
|
||||||
|
"relationship_direction": direction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def cmis_change_log(
|
def cmis_change_log(
|
||||||
self,
|
self,
|
||||||
@@ -3629,26 +3697,38 @@ def create_app(runtime: ServiceRuntime | None = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def unsupported_browser_selector(selector: str | None) -> dict[str, Any]:
|
def unsupported_browser_selector(selector: str | None) -> dict[str, Any]:
|
||||||
|
unsupported_details: dict[str, Any] = {
|
||||||
|
"cmisselector": selector,
|
||||||
|
"supported": [
|
||||||
|
"repositoryInfo",
|
||||||
|
"typeChildren",
|
||||||
|
"typeDescendants",
|
||||||
|
"typeDefinition",
|
||||||
|
"query",
|
||||||
|
"object",
|
||||||
|
"children",
|
||||||
|
"parent",
|
||||||
|
"parents",
|
||||||
|
"properties",
|
||||||
|
"allowableActions",
|
||||||
|
"policies",
|
||||||
|
"content",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if selector in {"descendants", "folderTree"}:
|
||||||
|
unsupported_details.update(
|
||||||
|
{
|
||||||
|
"code": "cmis.not_supported",
|
||||||
|
"cmis_exception": "notSupported",
|
||||||
|
"unsupported_feature": "get_descendants"
|
||||||
|
if selector == "descendants"
|
||||||
|
else "get_folder_tree",
|
||||||
|
"release_contract": "Navigation tree selectors remain unsupported for the first release.",
|
||||||
|
}
|
||||||
|
)
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"Unsupported CMIS Browser Binding selector",
|
"Unsupported CMIS Browser Binding selector",
|
||||||
details={
|
details=unsupported_details,
|
||||||
"cmisselector": selector,
|
|
||||||
"supported": [
|
|
||||||
"repositoryInfo",
|
|
||||||
"typeChildren",
|
|
||||||
"typeDescendants",
|
|
||||||
"typeDefinition",
|
|
||||||
"query",
|
|
||||||
"object",
|
|
||||||
"children",
|
|
||||||
"parent",
|
|
||||||
"parents",
|
|
||||||
"properties",
|
|
||||||
"allowableActions",
|
|
||||||
"policies",
|
|
||||||
"content",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def browser_action_payload(request: Request) -> dict[str, Any]:
|
async def browser_action_payload(request: Request) -> dict[str, Any]:
|
||||||
@@ -4230,9 +4310,18 @@ def create_app(runtime: ServiceRuntime | None = None):
|
|||||||
def cmis_relationships(
|
def cmis_relationships(
|
||||||
access_point_id: str,
|
access_point_id: str,
|
||||||
object_id: str | None = Query(None),
|
object_id: str | None = Query(None),
|
||||||
|
target_id: str | None = Query(None, alias="targetId"),
|
||||||
|
relationship_direction: str | None = Query(None, alias="relationshipDirection"),
|
||||||
context: OperationContext = Depends(context_from_headers),
|
context: OperationContext = Depends(context_from_headers),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return response(runtime.cmis_relationships, access_point_id, context, object_id=object_id)
|
return response(
|
||||||
|
runtime.cmis_relationships,
|
||||||
|
access_point_id,
|
||||||
|
context,
|
||||||
|
object_id=object_id,
|
||||||
|
target_id=target_id,
|
||||||
|
relationship_direction=relationship_direction,
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/cmis/{access_point_id}/browser/changes", tags=["cmis"])
|
@app.get("/cmis/{access_point_id}/browser/changes", tags=["cmis"])
|
||||||
def cmis_changes(
|
def cmis_changes(
|
||||||
@@ -4702,6 +4791,212 @@ def _normalize_cmis_path(path: str) -> str:
|
|||||||
return "/" + "/".join(parts)
|
return "/" + "/".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
_CMIS_QUERY_RE = re.compile(
|
||||||
|
r"^\s*SELECT\s+(?P<select>\*|[A-Za-z0-9_:\s,]+)\s+FROM\s+(?P<from>[A-Za-z0-9_:]+)"
|
||||||
|
r"(?:\s+WHERE\s+(?P<where>.*?))?"
|
||||||
|
r"(?:\s+ORDER\s+BY\s+(?P<order>[A-Za-z0-9_:]+)(?:\s+(?P<direction>ASC|DESC))?)?\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_CMIS_QUERY_CONDITION_RE = re.compile(
|
||||||
|
r"^(?P<field>[A-Za-z0-9_:]+)\s*(?P<op>=|LIKE)\s*(?P<value>'.*?'|\".*?\"|[^\s]+)\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_CMIS_QUERY_IN_RE = re.compile(
|
||||||
|
r"^(?P<field>[A-Za-z0-9_:]+)\s+IN\s*\((?P<values>.*)\)\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_cmis_query(query: str) -> dict[str, Any]:
|
||||||
|
match = _CMIS_QUERY_RE.match(query)
|
||||||
|
if not match:
|
||||||
|
raise _unsupported_cmis_query(query, "Only a bounded SELECT/FROM/WHERE/ORDER BY subset is supported.")
|
||||||
|
selected = " ".join(match.group("select").split())
|
||||||
|
if selected != "*":
|
||||||
|
raise _unsupported_cmis_query(query, "Only SELECT * is supported in the release-stable subset.")
|
||||||
|
type_id = match.group("from")
|
||||||
|
if type_id not in {CMISBaseType.DOCUMENT.value, "kontextual:document"}:
|
||||||
|
raise _unsupported_cmis_query(query, "Only cmis:document and kontextual:document are queryable.")
|
||||||
|
conditions = _parse_cmis_query_conditions(query, match.group("where"))
|
||||||
|
order_by = match.group("order")
|
||||||
|
direction = (match.group("direction") or "ASC").upper()
|
||||||
|
if order_by and order_by not in CMIS_QUERY_ORDERABLE_FIELDS:
|
||||||
|
raise _unsupported_cmis_query(
|
||||||
|
query,
|
||||||
|
"ORDER BY is supported only for common CMIS document fields.",
|
||||||
|
field=order_by,
|
||||||
|
)
|
||||||
|
return {"type_id": type_id, "conditions": conditions, "order_by": order_by, "direction": direction}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_cmis_query_conditions(query: str, where_clause: str | None) -> list[dict[str, Any]]:
|
||||||
|
if not where_clause:
|
||||||
|
return []
|
||||||
|
conditions: list[dict[str, Any]] = []
|
||||||
|
for raw_condition in re.split(r"\s+AND\s+", where_clause, flags=re.IGNORECASE):
|
||||||
|
condition = raw_condition.strip()
|
||||||
|
if not condition:
|
||||||
|
raise _unsupported_cmis_query(query, "Empty WHERE predicates are not supported.")
|
||||||
|
in_match = _CMIS_QUERY_IN_RE.match(condition)
|
||||||
|
if in_match:
|
||||||
|
field = in_match.group("field")
|
||||||
|
_validate_cmis_query_field(query, field)
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"field": field,
|
||||||
|
"operator": "IN",
|
||||||
|
"values": _cmis_query_literal_list(query, in_match.group("values")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if re.search(r"\bOR\b|\(|\)", condition, re.IGNORECASE):
|
||||||
|
raise _unsupported_cmis_query(
|
||||||
|
query,
|
||||||
|
"Only AND-combined simple predicates are supported.",
|
||||||
|
predicate=condition,
|
||||||
|
)
|
||||||
|
match = _CMIS_QUERY_CONDITION_RE.match(condition)
|
||||||
|
if not match:
|
||||||
|
raise _unsupported_cmis_query(query, "Unsupported CMIS query predicate.", predicate=condition)
|
||||||
|
field = match.group("field")
|
||||||
|
operator = match.group("op").upper()
|
||||||
|
_validate_cmis_query_field(query, field)
|
||||||
|
if operator == "LIKE" and field not in CMIS_QUERY_LIKE_FIELDS:
|
||||||
|
raise _unsupported_cmis_query(
|
||||||
|
query,
|
||||||
|
"LIKE is supported only for text-like CMIS release fields.",
|
||||||
|
field=field,
|
||||||
|
)
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"field": field,
|
||||||
|
"operator": operator,
|
||||||
|
"value": _cmis_query_unquote(match.group("value")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_cmis_query_field(query: str, field: str) -> None:
|
||||||
|
if field not in CMIS_QUERY_FILTERABLE_FIELDS:
|
||||||
|
raise _unsupported_cmis_query(
|
||||||
|
query,
|
||||||
|
"WHERE predicates are supported only for release-stable filterable fields.",
|
||||||
|
field=field,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_cmis_query_spec(projections: list[dict[str, Any]], query_spec: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
filtered = [
|
||||||
|
projection
|
||||||
|
for projection in projections
|
||||||
|
if all(_cmis_query_condition_matches(projection, condition) for condition in query_spec["conditions"])
|
||||||
|
]
|
||||||
|
order_by = query_spec.get("order_by")
|
||||||
|
if not order_by:
|
||||||
|
return filtered
|
||||||
|
populated = [projection for projection in filtered if _cmis_query_values(projection, order_by)]
|
||||||
|
empty = [projection for projection in filtered if not _cmis_query_values(projection, order_by)]
|
||||||
|
return sorted(
|
||||||
|
populated,
|
||||||
|
key=lambda projection: _cmis_query_sort_key(projection, order_by),
|
||||||
|
reverse=query_spec.get("direction") == "DESC",
|
||||||
|
) + empty
|
||||||
|
|
||||||
|
|
||||||
|
def _cmis_query_condition_matches(projection: dict[str, Any], condition: dict[str, Any]) -> bool:
|
||||||
|
values = [str(value) for value in _cmis_query_values(projection, condition["field"])]
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
if condition["operator"] == "IN":
|
||||||
|
expected = {str(value) for value in condition["values"]}
|
||||||
|
return any(value in expected for value in values)
|
||||||
|
expected_value = str(condition["value"])
|
||||||
|
if condition["operator"] == "LIKE":
|
||||||
|
return any(_cmis_query_like_supported(value, expected_value) for value in values)
|
||||||
|
return any(value == expected_value for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmis_query_values(projection: dict[str, Any], field: str) -> list[Any]:
|
||||||
|
if field == "cmis:objectId":
|
||||||
|
value = projection.get("object_id")
|
||||||
|
elif field == "cmis:name":
|
||||||
|
value = projection.get("name")
|
||||||
|
elif field == "cmis:baseTypeId":
|
||||||
|
value = projection.get("base_type_id")
|
||||||
|
elif field == "cmis:objectTypeId":
|
||||||
|
value = projection.get("type_id")
|
||||||
|
else:
|
||||||
|
value = dict(projection.get("properties", {})).get(field)
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
return [item for item in value if item is not None]
|
||||||
|
return [value]
|
||||||
|
|
||||||
|
|
||||||
|
def _cmis_query_sort_key(projection: dict[str, Any], field: str) -> tuple[str, str]:
|
||||||
|
values = _cmis_query_values(projection, field)
|
||||||
|
if not values:
|
||||||
|
return ("", "")
|
||||||
|
value = values[0]
|
||||||
|
return (type(value).__name__, str(value).lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _cmis_query_like_supported(value: str, pattern: str) -> bool:
|
||||||
|
expression = "".join(".*" if char == "%" else "." if char == "_" else re.escape(char) for char in pattern)
|
||||||
|
return re.match(f"^{expression}$", value, flags=re.IGNORECASE) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _cmis_query_literal_list(query: str, value: str) -> list[str]:
|
||||||
|
values: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
quote: str | None = None
|
||||||
|
for char in value:
|
||||||
|
if char in {"'", '"'}:
|
||||||
|
if quote == char:
|
||||||
|
quote = None
|
||||||
|
elif quote is None:
|
||||||
|
quote = char
|
||||||
|
current.append(char)
|
||||||
|
elif char == "," and quote is None:
|
||||||
|
literal = "".join(current).strip()
|
||||||
|
if literal:
|
||||||
|
values.append(_cmis_query_unquote(literal))
|
||||||
|
current = []
|
||||||
|
else:
|
||||||
|
current.append(char)
|
||||||
|
if quote is not None:
|
||||||
|
raise _unsupported_cmis_query(query, "Unclosed quoted literal in IN predicate.")
|
||||||
|
literal = "".join(current).strip()
|
||||||
|
if literal:
|
||||||
|
values.append(_cmis_query_unquote(literal))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _cmis_query_unquote(value: str) -> str:
|
||||||
|
stripped = value.strip()
|
||||||
|
if len(stripped) >= 2 and stripped[0] == stripped[-1] and stripped[0] in {"'", '"'}:
|
||||||
|
return stripped[1:-1]
|
||||||
|
return stripped
|
||||||
|
|
||||||
|
|
||||||
|
def _unsupported_cmis_query(query: str, reason: str, **details: Any) -> ValidationError:
|
||||||
|
return ValidationError(
|
||||||
|
"Unsupported CMIS query subset",
|
||||||
|
details={
|
||||||
|
"code": "cmis.not_supported",
|
||||||
|
"cmis_exception": "notSupported",
|
||||||
|
"query": query,
|
||||||
|
"reason": reason,
|
||||||
|
"supported": CMIS_QUERY_SUPPORTED,
|
||||||
|
"filterable_fields": sorted(CMIS_QUERY_FILTERABLE_FIELDS),
|
||||||
|
"orderable_fields": sorted(CMIS_QUERY_ORDERABLE_FIELDS),
|
||||||
|
**details,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _cmis_media_type(value: Any) -> str:
|
def _cmis_media_type(value: Any) -> str:
|
||||||
media_type = str(value or "application/octet-stream").split(";", 1)[0].strip()
|
media_type = str(value or "application/octet-stream").split(";", 1)[0].strip()
|
||||||
return media_type or "application/octet-stream"
|
return media_type or "application/octet-stream"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from .metadata import LifecycleState, Sensitivity
|
|||||||
from .policy import PolicyDecision
|
from .policy import PolicyDecision
|
||||||
from .provenance import AssetVersion
|
from .provenance import AssetVersion
|
||||||
from .relationships import CoreRelationship, RelationshipTargetKind
|
from .relationships import CoreRelationship, RelationshipTargetKind
|
||||||
from .primitives import compact_dict
|
from .primitives import compact_dict, stable_json_dumps
|
||||||
|
|
||||||
|
|
||||||
class CMISBinding(str, Enum):
|
class CMISBinding(str, Enum):
|
||||||
@@ -190,7 +190,11 @@ UNSUPPORTED_FEATURES: dict[str, dict[str, Any]] = {
|
|||||||
"reason": "query_not_supported",
|
"reason": "query_not_supported",
|
||||||
"standard_flag": "capability_join",
|
"standard_flag": "capability_join",
|
||||||
},
|
},
|
||||||
"order_by": {"status": "unsupported", "reason": "query_not_supported", "standard_flag": "capability_order_by"},
|
"custom_order_by": {
|
||||||
|
"status": "unsupported",
|
||||||
|
"reason": "custom_order_by_not_supported",
|
||||||
|
"standard_flag": "capability_order_by",
|
||||||
|
},
|
||||||
"apply_acl": {"status": "unsupported", "reason": "operation_not_implemented", "standard_flag": "capability_acl"},
|
"apply_acl": {"status": "unsupported", "reason": "operation_not_implemented", "standard_flag": "capability_acl"},
|
||||||
"apply_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
"apply_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
||||||
"remove_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
"remove_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
||||||
@@ -636,7 +640,7 @@ class CMISDomainMapper:
|
|||||||
"capability_renditions": "none",
|
"capability_renditions": "none",
|
||||||
"capability_get_descendants": False,
|
"capability_get_descendants": False,
|
||||||
"capability_get_folder_tree": False,
|
"capability_get_folder_tree": False,
|
||||||
"capability_order_by": "none",
|
"capability_order_by": "common",
|
||||||
"capability_multifiling": False,
|
"capability_multifiling": False,
|
||||||
"capability_unfiling": False,
|
"capability_unfiling": False,
|
||||||
"capability_version_specific_filing": False,
|
"capability_version_specific_filing": False,
|
||||||
@@ -747,11 +751,19 @@ class CMISDomainMapper:
|
|||||||
"cmis:name": relationship.predicate,
|
"cmis:name": relationship.predicate,
|
||||||
"cmis:baseTypeId": CMISBaseType.RELATIONSHIP.value,
|
"cmis:baseTypeId": CMISBaseType.RELATIONSHIP.value,
|
||||||
"cmis:objectTypeId": "kontextual:relationship",
|
"cmis:objectTypeId": "kontextual:relationship",
|
||||||
|
"cmis:changeToken": f"relationship:{relationship.relationship_id}:{relationship.created_at}",
|
||||||
"cmis:sourceId": source_id,
|
"cmis:sourceId": source_id,
|
||||||
"cmis:targetId": target_id,
|
"cmis:targetId": target_id,
|
||||||
|
"kontextual:relationshipId": relationship.relationship_id,
|
||||||
"kontextual:predicate": relationship.predicate,
|
"kontextual:predicate": relationship.predicate,
|
||||||
"kontextual:confidence": relationship.confidence,
|
"kontextual:confidence": relationship.confidence,
|
||||||
"kontextual:targetKind": relationship.target_kind.value,
|
"kontextual:targetKind": relationship.target_kind.value,
|
||||||
|
"kontextual:direction": relationship.direction,
|
||||||
|
"kontextual:validFrom": relationship.valid_from,
|
||||||
|
"kontextual:validTo": relationship.valid_to,
|
||||||
|
"kontextual:actorId": relationship.actor_id,
|
||||||
|
"kontextual:createdAt": relationship.created_at,
|
||||||
|
"kontextual:provenance": stable_json_dumps(relationship.provenance),
|
||||||
},
|
},
|
||||||
allowable_actions=(CMISAction.GET_OBJECT, CMISAction.GET_RELATIONSHIPS),
|
allowable_actions=(CMISAction.GET_OBJECT, CMISAction.GET_RELATIONSHIPS),
|
||||||
)
|
)
|
||||||
@@ -766,16 +778,22 @@ class CMISDomainMapper:
|
|||||||
entries = [
|
entries = [
|
||||||
{
|
{
|
||||||
"principal_id": context.actor.id,
|
"principal_id": context.actor.id,
|
||||||
|
"principal_kind": context.actor.actor_type.value,
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
"direct": True,
|
"direct": True,
|
||||||
|
"inherited": False,
|
||||||
|
"source": "request-actor-profile",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if asset.classification.sensitivity == Sensitivity.PUBLIC:
|
if asset.classification.sensitivity == Sensitivity.PUBLIC:
|
||||||
entries.append(
|
entries.append(
|
||||||
{
|
{
|
||||||
"principal_id": "anyone",
|
"principal_id": "anyone",
|
||||||
|
"principal_kind": "well_known",
|
||||||
"permissions": ["cmis:read"],
|
"permissions": ["cmis:read"],
|
||||||
"direct": False,
|
"direct": False,
|
||||||
|
"inherited": True,
|
||||||
|
"source": "public-sensitivity",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@@ -783,6 +801,13 @@ class CMISDomainMapper:
|
|||||||
"is_exact": True,
|
"is_exact": True,
|
||||||
"aces": entries,
|
"aces": entries,
|
||||||
"derived_from": "kontextual-profile-policy",
|
"derived_from": "kontextual-profile-policy",
|
||||||
|
"visibility_reason": visibility.reason,
|
||||||
|
"permission_mapping": {
|
||||||
|
"cmis:read": "asset visible through profile policy",
|
||||||
|
"cmis:write": "profile allows governed mutations",
|
||||||
|
"cmis:delete": "profile allows governed delete requests",
|
||||||
|
},
|
||||||
|
"policy_authority": "kontextual-policy-gateway",
|
||||||
"profile": self.access_point.profile.name,
|
"profile": self.access_point.profile.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1500,8 +1525,8 @@ def _browser_type_display_name(type_definition: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
def _browser_standard_property_definitions(base_id: str) -> dict[str, dict[str, Any]]:
|
def _browser_standard_property_definitions(base_id: str) -> dict[str, dict[str, Any]]:
|
||||||
common = {
|
common = {
|
||||||
"cmis:objectId": _browser_propdef("id", required=False, updatability="readonly"),
|
"cmis:objectId": _browser_propdef("id", required=False, updatability="readonly", orderable=True),
|
||||||
"cmis:name": _browser_propdef("string", required=True, updatability="readwrite"),
|
"cmis:name": _browser_propdef("string", required=True, updatability="readwrite", orderable=True),
|
||||||
"cmis:baseTypeId": _browser_propdef("id", required=False, updatability="readonly"),
|
"cmis:baseTypeId": _browser_propdef("id", required=False, updatability="readonly"),
|
||||||
"cmis:objectTypeId": _browser_propdef("id", required=True, updatability="oncreate"),
|
"cmis:objectTypeId": _browser_propdef("id", required=True, updatability="oncreate"),
|
||||||
"cmis:createdBy": _browser_propdef(
|
"cmis:createdBy": _browser_propdef(
|
||||||
@@ -1713,14 +1738,34 @@ def _type_definition(
|
|||||||
|
|
||||||
def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any]]:
|
def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any]]:
|
||||||
definitions = {
|
definitions = {
|
||||||
"cmis:objectId": {"property_type": "id", "cardinality": "single", "required": True},
|
"cmis:objectId": {
|
||||||
"cmis:name": {"property_type": "string", "cardinality": "single", "required": True},
|
"property_type": "id",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": True,
|
||||||
|
"orderable": True,
|
||||||
|
},
|
||||||
|
"cmis:name": {
|
||||||
|
"property_type": "string",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": True,
|
||||||
|
"orderable": True,
|
||||||
|
},
|
||||||
"cmis:baseTypeId": {"property_type": "id", "cardinality": "single", "required": True},
|
"cmis:baseTypeId": {"property_type": "id", "cardinality": "single", "required": True},
|
||||||
"cmis:objectTypeId": {"property_type": "id", "cardinality": "single", "required": True},
|
"cmis:objectTypeId": {"property_type": "id", "cardinality": "single", "required": True},
|
||||||
"cmis:createdBy": {"property_type": "string", "cardinality": "single", "required": False},
|
"cmis:createdBy": {"property_type": "string", "cardinality": "single", "required": False},
|
||||||
"cmis:lastModifiedBy": {"property_type": "string", "cardinality": "single", "required": False},
|
"cmis:lastModifiedBy": {"property_type": "string", "cardinality": "single", "required": False},
|
||||||
"cmis:creationDate": {"property_type": "datetime", "cardinality": "single", "required": False},
|
"cmis:creationDate": {
|
||||||
"cmis:lastModificationDate": {"property_type": "datetime", "cardinality": "single", "required": False},
|
"property_type": "datetime",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
"orderable": True,
|
||||||
|
},
|
||||||
|
"cmis:lastModificationDate": {
|
||||||
|
"property_type": "datetime",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
"orderable": True,
|
||||||
|
},
|
||||||
"cmis:changeToken": {"property_type": "string", "cardinality": "single", "required": False},
|
"cmis:changeToken": {"property_type": "string", "cardinality": "single", "required": False},
|
||||||
"cmis:secondaryObjectTypeIds": {"property_type": "id", "cardinality": "multi", "required": False},
|
"cmis:secondaryObjectTypeIds": {"property_type": "id", "cardinality": "multi", "required": False},
|
||||||
"cmis:description": {"property_type": "string", "cardinality": "single", "required": False},
|
"cmis:description": {"property_type": "string", "cardinality": "single", "required": False},
|
||||||
@@ -1858,6 +1903,56 @@ def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any
|
|||||||
if base_type_id == CMISBaseType.RELATIONSHIP:
|
if base_type_id == CMISBaseType.RELATIONSHIP:
|
||||||
definitions["cmis:sourceId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
definitions["cmis:sourceId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
||||||
definitions["cmis:targetId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
definitions["cmis:targetId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
||||||
|
definitions["kontextual:relationshipId"] = {
|
||||||
|
"property_type": "id",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:predicate"] = {
|
||||||
|
"property_type": "string",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:confidence"] = {
|
||||||
|
"property_type": "decimal",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:targetKind"] = {
|
||||||
|
"property_type": "string",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:direction"] = {
|
||||||
|
"property_type": "string",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:validFrom"] = {
|
||||||
|
"property_type": "datetime",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:validTo"] = {
|
||||||
|
"property_type": "datetime",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:actorId"] = {
|
||||||
|
"property_type": "string",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:createdAt"] = {
|
||||||
|
"property_type": "datetime",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
definitions["kontextual:provenance"] = {
|
||||||
|
"property_type": "string",
|
||||||
|
"cardinality": "single",
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
return definitions
|
return definitions
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"capability_group": "discovery-query",
|
"capability_group": "discovery-query",
|
||||||
"tck_groups": ["QueryTestGroup"],
|
"tck_groups": ["QueryTestGroup"],
|
||||||
"expected": "partial-pass",
|
"expected": "partial-pass",
|
||||||
"known_gaps": ["full_cmis_sql_joins", "order_by"]
|
"known_gaps": ["full_cmis_sql_joins", "custom_order_by"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capability_group": "relationships",
|
"capability_group": "relationships",
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
|
|||||||
assert repository["repositoryUrl"].endswith("/cmis/readonly-browser/browser")
|
assert repository["repositoryUrl"].endswith("/cmis/readonly-browser/browser")
|
||||||
assert repository["rootFolderUrl"].endswith("/cmis/readonly-browser/browser/root")
|
assert repository["rootFolderUrl"].endswith("/cmis/readonly-browser/browser/root")
|
||||||
assert repository["capabilities"]["capabilityQuery"] == "metadataonly"
|
assert repository["capabilities"]["capabilityQuery"] == "metadataonly"
|
||||||
|
assert repository["capabilities"]["capabilityOrderBy"] == "common"
|
||||||
assert repository["capabilities"]["capabilityGetDescendants"] is False
|
assert repository["capabilities"]["capabilityGetDescendants"] is False
|
||||||
assert browser_types["types"][0]["id"] == "cmis:document"
|
assert browser_types["types"][0]["id"] == "cmis:document"
|
||||||
assert "propertyDefinitions" not in browser_types["types"][0]
|
assert "propertyDefinitions" not in browser_types["types"][0]
|
||||||
@@ -166,10 +167,18 @@ def test_cmis_readonly_children_object_content_query_relationships_and_changes(c
|
|||||||
"/cmis/readonly-browser/browser/query",
|
"/cmis/readonly-browser/browser/query",
|
||||||
params={"q": "SELECT * FROM cmis:document"},
|
params={"q": "SELECT * FROM cmis:document"},
|
||||||
).json()
|
).json()
|
||||||
|
filtered_query = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser/query",
|
||||||
|
params={"q": "SELECT * FROM cmis:document WHERE kontextual:topics IN ('cmis') ORDER BY cmis:name ASC"},
|
||||||
|
).json()
|
||||||
relationships = cmis_client.get(
|
relationships = cmis_client.get(
|
||||||
"/cmis/readonly-browser/browser/relationships",
|
"/cmis/readonly-browser/browser/relationships",
|
||||||
params={"object_id": "cmis:asset:asset-source"},
|
params={"object_id": "cmis:asset:asset-source"},
|
||||||
).json()
|
).json()
|
||||||
|
target_relationships = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser/relationships",
|
||||||
|
params={"object_id": "cmis:asset:asset-public", "relationshipDirection": "target"},
|
||||||
|
).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"]}
|
root_ids = {item["object_id"] for item in root_children["objects"]}
|
||||||
@@ -182,8 +191,11 @@ def test_cmis_readonly_children_object_content_query_relationships_and_changes(c
|
|||||||
assert "get_content_stream" in object_response["allowable_actions"]
|
assert "get_content_stream" in object_response["allowable_actions"]
|
||||||
assert content["mime_type"] == "text/markdown"
|
assert content["mime_type"] == "text/markdown"
|
||||||
assert query["total_num_items"] == children["total_num_items"]
|
assert query["total_num_items"] == children["total_num_items"]
|
||||||
|
assert [item["object_id"] for item in filtered_query["results"]] == ["cmis:asset:asset-source"]
|
||||||
assert relationships["count"] == 1
|
assert relationships["count"] == 1
|
||||||
assert relationships["items"][0]["properties"]["cmis:targetId"] == "cmis:asset:asset-public"
|
assert relationships["items"][0]["properties"]["cmis:targetId"] == "cmis:asset:asset-public"
|
||||||
|
assert relationships["items"][0]["properties"]["kontextual:relationshipId"]
|
||||||
|
assert target_relationships["count"] == 1
|
||||||
assert changes["total_num_items"] >= 3
|
assert changes["total_num_items"] >= 3
|
||||||
|
|
||||||
|
|
||||||
@@ -213,13 +225,24 @@ def test_cmis_query_reports_unsupported_subset_diagnostics(cmis_client) -> None:
|
|||||||
params={"q": "SELECT * FROM cmis:document JOIN cmis:relationship"},
|
params={"q": "SELECT * FROM cmis:document JOIN cmis:relationship"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 405
|
||||||
assert response.json()["exception"] == "invalidArgument"
|
assert response.json()["exception"] == "notSupported"
|
||||||
assert response.json()["details"]["supported"] == [
|
assert "SELECT * FROM cmis:document" in response.json()["details"]["supported"]
|
||||||
"SELECT * FROM cmis:document",
|
assert response.json()["details"]["orderable_fields"] == [
|
||||||
"SELECT * FROM kontextual:document",
|
"cmis:creationDate",
|
||||||
|
"cmis:lastModificationDate",
|
||||||
|
"cmis:name",
|
||||||
|
"cmis:objectId",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
descendants = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser/root",
|
||||||
|
params={"cmisselector": "descendants"},
|
||||||
|
)
|
||||||
|
assert descendants.status_code == 405
|
||||||
|
assert descendants.json()["exception"] == "notSupported"
|
||||||
|
assert descendants.json()["details"]["unsupported_feature"] == "get_descendants"
|
||||||
|
|
||||||
|
|
||||||
def test_cmis_governed_authoring_routes_allow_selected_mutations(cmis_client) -> None:
|
def test_cmis_governed_authoring_routes_allow_selected_mutations(cmis_client) -> None:
|
||||||
created = cmis_client.post(
|
created = cmis_client.post(
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ def test_repository_info_uses_complete_conservative_cmis_11_capability_flags() -
|
|||||||
assert capabilities["capability_renditions"] == "none"
|
assert capabilities["capability_renditions"] == "none"
|
||||||
assert capabilities["capability_get_descendants"] is False
|
assert capabilities["capability_get_descendants"] is False
|
||||||
assert capabilities["capability_get_folder_tree"] is False
|
assert capabilities["capability_get_folder_tree"] is False
|
||||||
assert capabilities["capability_order_by"] == "none"
|
assert capabilities["capability_order_by"] == "common"
|
||||||
assert capabilities["capability_multifiling"] is False
|
assert capabilities["capability_multifiling"] is False
|
||||||
assert capabilities["capability_unfiling"] is False
|
assert capabilities["capability_unfiling"] is False
|
||||||
assert capabilities["capability_version_specific_filing"] is False
|
assert capabilities["capability_version_specific_filing"] is False
|
||||||
|
|||||||
72
tests/cmis/test_cmis_read_side_capacity.py
Normal file
72
tests/cmis/test_cmis_read_side_capacity.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from time import perf_counter
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from kontextual_engine import Classification, ServiceRuntime, Sensitivity
|
||||||
|
from kontextual_engine.adapters.memory import InMemoryAssetRegistryRepository
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.cmis, pytest.mark.capacity]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
os.getenv("KONTEXTUAL_RUN_CAPACITY") != "1",
|
||||||
|
reason="Set KONTEXTUAL_RUN_CAPACITY=1 to run CMIS read-side capacity probes.",
|
||||||
|
)
|
||||||
|
def test_cmis_read_side_query_and_relationship_capacity_probe() -> None:
|
||||||
|
runtime = ServiceRuntime(repository=InMemoryAssetRegistryRepository())
|
||||||
|
context = runtime.operation_context(actor_id="cmis-capacity", correlation_id="corr-cmis-capacity")
|
||||||
|
asset_count = 400
|
||||||
|
relationship_count = 250
|
||||||
|
|
||||||
|
for index in range(asset_count):
|
||||||
|
runtime.asset_service().create_asset(
|
||||||
|
f"Capacity Asset {index:04d}",
|
||||||
|
Classification(
|
||||||
|
asset_type="document",
|
||||||
|
sensitivity=Sensitivity.PUBLIC if index % 5 == 0 else Sensitivity.INTERNAL,
|
||||||
|
owner=f"capacity-owner-{index % 7}",
|
||||||
|
topics=("capacity", f"group-{index % 11}"),
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
asset_id=f"asset-capacity-{index:04d}",
|
||||||
|
)
|
||||||
|
|
||||||
|
for index in range(relationship_count):
|
||||||
|
runtime.create_relationship(
|
||||||
|
{
|
||||||
|
"source_asset_id": f"asset-capacity-{index:04d}",
|
||||||
|
"target_id": f"asset-capacity-{asset_count - 1:04d}",
|
||||||
|
"predicate": "capacity-related-to",
|
||||||
|
"target_kind": "asset",
|
||||||
|
"confidence": 0.9,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
query_started = perf_counter()
|
||||||
|
query = runtime.cmis_query(
|
||||||
|
"readonly-browser",
|
||||||
|
"SELECT * FROM cmis:document WHERE kontextual:topics IN ('capacity') ORDER BY cmis:name DESC",
|
||||||
|
context,
|
||||||
|
max_items=25,
|
||||||
|
)
|
||||||
|
query_seconds = perf_counter() - query_started
|
||||||
|
|
||||||
|
relationships_started = perf_counter()
|
||||||
|
relationships = runtime.cmis_relationships(
|
||||||
|
"readonly-browser",
|
||||||
|
context,
|
||||||
|
object_id=f"cmis:asset:asset-capacity-{asset_count - 1:04d}",
|
||||||
|
relationship_direction="target",
|
||||||
|
)
|
||||||
|
relationships_seconds = perf_counter() - relationships_started
|
||||||
|
|
||||||
|
assert query["total_num_items"] == asset_count
|
||||||
|
assert query["num_items"] == 25
|
||||||
|
assert relationships["count"] == relationship_count
|
||||||
|
assert query_seconds < 5.0
|
||||||
|
assert relationships_seconds < 3.0
|
||||||
@@ -110,17 +110,48 @@ def test_runtime_cmis_browser_content_query_relationships_and_changes(cmis_runti
|
|||||||
|
|
||||||
content = runtime.cmis_content_stream("readonly-browser", "cmis:asset:asset-runtime-source", context)
|
content = runtime.cmis_content_stream("readonly-browser", "cmis:asset:asset-runtime-source", context)
|
||||||
query = runtime.cmis_query("readonly-browser", "SELECT * FROM cmis:document", context)
|
query = runtime.cmis_query("readonly-browser", "SELECT * FROM cmis:document", context)
|
||||||
|
filtered_query = runtime.cmis_query(
|
||||||
|
"readonly-browser",
|
||||||
|
"SELECT * FROM cmis:document WHERE kontextual:sensitivity = 'internal' "
|
||||||
|
"AND kontextual:topics IN ('integration') ORDER BY cmis:name DESC",
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
like_query = runtime.cmis_query(
|
||||||
|
"readonly-browser",
|
||||||
|
"SELECT * FROM cmis:document WHERE cmis:name LIKE 'Runtime %' ORDER BY cmis:name DESC",
|
||||||
|
context,
|
||||||
|
)
|
||||||
relationships = runtime.cmis_relationships(
|
relationships = runtime.cmis_relationships(
|
||||||
"readonly-browser",
|
"readonly-browser",
|
||||||
context,
|
context,
|
||||||
object_id="cmis:asset:asset-runtime-source",
|
object_id="cmis:asset:asset-runtime-source",
|
||||||
)
|
)
|
||||||
|
target_relationships = runtime.cmis_relationships(
|
||||||
|
"readonly-browser",
|
||||||
|
context,
|
||||||
|
object_id="cmis:asset:asset-runtime-public",
|
||||||
|
relationship_direction="target",
|
||||||
|
)
|
||||||
|
either_relationships = runtime.cmis_relationships(
|
||||||
|
"readonly-browser",
|
||||||
|
context,
|
||||||
|
object_id="cmis:asset:asset-runtime-public",
|
||||||
|
relationship_direction="either",
|
||||||
|
)
|
||||||
changes = runtime.cmis_change_log("readonly-browser", context)
|
changes = runtime.cmis_change_log("readonly-browser", context)
|
||||||
|
|
||||||
assert content["mime_type"] in {"text/plain", "text/markdown"}
|
assert content["mime_type"] in {"text/plain", "text/markdown"}
|
||||||
assert query["total_num_items"] == 2
|
assert query["total_num_items"] == 2
|
||||||
|
assert [item["object_id"] for item in filtered_query["results"]] == [
|
||||||
|
"cmis:asset:asset-runtime-source"
|
||||||
|
]
|
||||||
|
assert [item["name"] for item in like_query["results"]] == ["Runtime Source", "Runtime Public"]
|
||||||
assert relationships["count"] == 1
|
assert relationships["count"] == 1
|
||||||
assert relationships["items"][0]["properties"]["cmis:targetId"] == "cmis:asset:asset-runtime-public"
|
assert relationships["items"][0]["properties"]["cmis:targetId"] == "cmis:asset:asset-runtime-public"
|
||||||
|
assert relationships["items"][0]["properties"]["cmis:changeToken"].startswith("relationship:")
|
||||||
|
assert relationships["items"][0]["properties"]["kontextual:direction"] == "outbound"
|
||||||
|
assert target_relationships["count"] == 1
|
||||||
|
assert either_relationships["count"] == 1
|
||||||
assert changes["total_num_items"] >= 3
|
assert changes["total_num_items"] >= 3
|
||||||
assert all(change["object_id"] != "cmis:asset:asset-runtime-confidential" for change in changes["changes"])
|
assert all(change["object_id"] != "cmis:asset:asset-runtime-confidential" for change in changes["changes"])
|
||||||
|
|
||||||
@@ -137,6 +168,16 @@ def test_runtime_cmis_browser_rejects_unsupported_query_subset(cmis_runtime) ->
|
|||||||
|
|
||||||
assert "Unsupported CMIS query subset" in str(exc_info.value)
|
assert "Unsupported CMIS query subset" in str(exc_info.value)
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as direction_exc:
|
||||||
|
runtime.cmis_relationships(
|
||||||
|
"readonly-browser",
|
||||||
|
context,
|
||||||
|
object_id="cmis:asset:asset-runtime-source",
|
||||||
|
relationship_direction="both",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Unsupported CMIS relationship direction" in str(direction_exc.value)
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_cmis_governed_authoring_allows_selected_mutations(cmis_runtime) -> None:
|
def test_runtime_cmis_governed_authoring_allows_selected_mutations(cmis_runtime) -> None:
|
||||||
runtime, context = cmis_runtime
|
runtime, context = cmis_runtime
|
||||||
@@ -335,6 +376,10 @@ def test_runtime_cmis_acl_projection_and_redaction(cmis_runtime) -> None:
|
|||||||
|
|
||||||
assert public_acl["is_exact"] is True
|
assert public_acl["is_exact"] is True
|
||||||
assert {entry["principal_id"] for entry in public_acl["aces"]} == {"cmis-runtime", "anyone"}
|
assert {entry["principal_id"] for entry in public_acl["aces"]} == {"cmis-runtime", "anyone"}
|
||||||
|
assert public_acl["policy_authority"] == "kontextual-policy-gateway"
|
||||||
|
assert public_acl["permission_mapping"]["cmis:read"] == "asset visible through profile policy"
|
||||||
|
assert {entry["principal_kind"] for entry in public_acl["aces"]} == {"human", "well_known"}
|
||||||
|
assert {entry["inherited"] for entry in public_acl["aces"]} == {False, True}
|
||||||
assert ["cmis:read", "cmis:write", "cmis:delete"] in [
|
assert ["cmis:read", "cmis:write", "cmis:delete"] in [
|
||||||
entry["permissions"] for entry in internal_acl["aces"]
|
entry["permissions"] for entry in internal_acl["aces"]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "CMIS Read-Side Contract Maturity"
|
title: "CMIS Read-Side Contract Maturity"
|
||||||
domain: markitect
|
domain: markitect
|
||||||
repo: kontextual-engine
|
repo: kontextual-engine
|
||||||
status: active
|
status: completed
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: markitect
|
topic_slug: markitect
|
||||||
planning_priority: high
|
planning_priority: high
|
||||||
@@ -100,7 +100,7 @@ remain intentionally unsupported.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0016-T001
|
id: KONT-WP-0016-T001
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "961bb720-0825-44d9-bb30-b6aed0f3f2cc"
|
state_hub_task_id: "961bb720-0825-44d9-bb30-b6aed0f3f2cc"
|
||||||
```
|
```
|
||||||
@@ -117,7 +117,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0016-T002
|
id: KONT-WP-0016-T002
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "f393271e-4e0a-4a60-8570-2f2bc1d84c0f"
|
state_hub_task_id: "f393271e-4e0a-4a60-8570-2f2bc1d84c0f"
|
||||||
```
|
```
|
||||||
@@ -138,7 +138,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0016-T003
|
id: KONT-WP-0016-T003
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "468221a0-111c-4a68-b0b2-392f83e5a70b"
|
state_hub_task_id: "468221a0-111c-4a68-b0b2-392f83e5a70b"
|
||||||
```
|
```
|
||||||
@@ -158,7 +158,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0016-T004
|
id: KONT-WP-0016-T004
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "86d117a8-f569-4708-8687-5a53e6210813"
|
state_hub_task_id: "86d117a8-f569-4708-8687-5a53e6210813"
|
||||||
```
|
```
|
||||||
@@ -178,7 +178,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0016-T005
|
id: KONT-WP-0016-T005
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "6214aac1-4b3a-41f8-9e61-ab6844171dd1"
|
state_hub_task_id: "6214aac1-4b3a-41f8-9e61-ab6844171dd1"
|
||||||
```
|
```
|
||||||
@@ -197,7 +197,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0016-T006
|
id: KONT-WP-0016-T006
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "1f492036-0ba0-4a85-addb-b73397e9e966"
|
state_hub_task_id: "1f492036-0ba0-4a85-addb-b73397e9e966"
|
||||||
```
|
```
|
||||||
@@ -215,7 +215,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0016-T007
|
id: KONT-WP-0016-T007
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "73d9b377-1f03-4e12-8028-af43496457b6"
|
state_hub_task_id: "73d9b377-1f03-4e12-8028-af43496457b6"
|
||||||
```
|
```
|
||||||
@@ -231,6 +231,64 @@ Acceptance:
|
|||||||
- Update the CMIS scorecard and first-release readiness notes from the final
|
- Update the CMIS scorecard and first-release readiness notes from the final
|
||||||
evidence.
|
evidence.
|
||||||
|
|
||||||
|
## Implementation Evidence
|
||||||
|
|
||||||
|
Implemented on 2026-05-14:
|
||||||
|
|
||||||
|
- Added `docs/cmis-read-side-contract.md` as the release-stable read-side
|
||||||
|
contract.
|
||||||
|
- Replaced the query allowlist with a bounded parser for `SELECT *` document
|
||||||
|
queries, `AND` predicates, equality, `LIKE`, `IN`, deterministic paging, and
|
||||||
|
common CMIS `ORDER BY`.
|
||||||
|
- Kept `descendants` and `folderTree` explicitly unsupported with CMIS
|
||||||
|
`notSupported` diagnostics and false capability flags.
|
||||||
|
- Added relationship target and either-direction filters, relationship change
|
||||||
|
tokens, provenance, direction, actor, and creation metadata.
|
||||||
|
- Enriched ACL discovery with principal kind, direct/inherited markers,
|
||||||
|
source, permission mapping, and policy authority metadata.
|
||||||
|
- Updated examples, the OpenCMIS subset map, scorecard, compliance assessment,
|
||||||
|
and release readiness notes.
|
||||||
|
- Added opt-in capacity coverage in
|
||||||
|
`tests/cmis/test_cmis_read_side_capacity.py`.
|
||||||
|
|
||||||
|
Focused verification:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 -m pytest \
|
||||||
|
tests/cmis/test_cmis_runtime_browser_binding.py \
|
||||||
|
tests/cmis/test_cmis_browser_binding_api.py \
|
||||||
|
tests/cmis/test_cmis_compliance_flags.py \
|
||||||
|
tests/cmis/test_cmis_contract_examples.py
|
||||||
|
|
||||||
|
Result: 21 passed, 16 skipped in 5.19s
|
||||||
|
```
|
||||||
|
|
||||||
|
The skipped tests require optional FastAPI/HTTPX test extras in this local
|
||||||
|
environment. They remain part of the release verification gate when the service
|
||||||
|
extras are installed. No new OpenCMIS run was required for the selected
|
||||||
|
object/content baseline because this workplan changes read-side query,
|
||||||
|
relationship, ACL, and diagnostics contracts outside that selected baseline.
|
||||||
|
|
||||||
|
Browser Binding API verification with service extras:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.venv/bin/python -m pytest tests/cmis/test_cmis_browser_binding_api.py -q
|
||||||
|
|
||||||
|
Result: 16 passed in 39.09s
|
||||||
|
```
|
||||||
|
|
||||||
|
Full suite verification with service extras:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.venv/bin/python -m pytest -q
|
||||||
|
|
||||||
|
Result: 166 passed, 15 skipped in 55.94s
|
||||||
|
```
|
||||||
|
|
||||||
|
The run emitted advisory performance-drift warnings for several API tests. They
|
||||||
|
do not indicate functional failures and should be watched through the existing
|
||||||
|
compact performance-history monitor.
|
||||||
|
|
||||||
## Release Advice
|
## Release Advice
|
||||||
|
|
||||||
This workplan should run before `KONT-WP-0015` unless the first release is
|
This workplan should run before `KONT-WP-0015` unless the first release is
|
||||||
|
|||||||
Reference in New Issue
Block a user