chore(consistency): sync task status from DB [auto]

Updated by fix-consistency on 2026-05-07:
  - update .custodian-brief.md for open-cmis-tck
This commit is contained in:
2026-05-07 22:58:28 +02:00
parent a9e1f7130c
commit cc43881565
13 changed files with 1301 additions and 39 deletions

View File

@@ -2,12 +2,23 @@
# Custodian Brief — open-cmis-tck # Custodian Brief — open-cmis-tck
**Domain:** markitect **Domain:** markitect
**Last synced:** 2026-05-07 20:34 UTC **Last synced:** 2026-05-07 20:58 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams ## Active Workstreams
*(none — repo may need first-session setup)* ### Live OpenCMIS TCK Execution And Capability Maturity
Progress: 0/9 done | workstream_id: `da3f0d16-ba8e-4147-b0fc-ab3462e0b7b0`
**Open tasks:**
- · D2.1 - Resolve TCK Runtime And Access Model `f3144edb`
- · D2.2 - Local Environment Bootstrap Command `f993c1ef`
- · D2.3 - OpenCMIS TCK Adapter Invocation `a446a80f`
- · D2.4 - Target Profiles And Credential References `c33a4d9a`
- · D2.5 - Real Result Normalization `03ba9506`
- · D2.6 - Live Pilot Run `d9eb9384`
- · D2.7 - CMIS Capability Maturity Scorecard `7365052f`
- … and 2 more open tasks
--- ---
## MCP Orientation (when available) ## MCP Orientation (when available)

View File

@@ -53,8 +53,14 @@ Expected current behavior:
- CMIS Browser Binding preflight runs first. - CMIS Browser Binding preflight runs first.
- Failed preflight blocks downstream TCK groups. - Failed preflight blocks downstream TCK groups.
- The Java/Maven OpenCMIS wrapper reports structured blockers until the final - The Java/Maven OpenCMIS wrapper reports structured blockers until a local
Apache Chemistry TCK invocation is configured. Apache Chemistry TCK command is configured.
- If a command is configured, raw stdout/stderr and normalized runner output are
captured under the guide-board run directory.
Runner command configuration lives in
`runtime_policy.opencmis_tck.command`. See
[docs/OPENCMIS-TCK-RUNNER.md](docs/OPENCMIS-TCK-RUNNER.md).
## Tests ## Tests
@@ -64,6 +70,12 @@ Run extension tests with the guide-board core on `PYTHONPATH`:
PYTHONPATH=../guide-board/src python3 -m unittest discover -s tests PYTHONPATH=../guide-board/src python3 -m unittest discover -s tests
``` ```
## Docs
- [docs/CMIS-PROFILES.md](docs/CMIS-PROFILES.md)
- [docs/OPENCMIS-TCK-RUNNER.md](docs/OPENCMIS-TCK-RUNNER.md)
- [docs/SERVICE-AND-RETENTION.md](docs/SERVICE-AND-RETENTION.md)
## Boundary ## Boundary
Apache Chemistry/OpenCMIS TCK dependencies may be restricted by upstream Apache Chemistry/OpenCMIS TCK dependencies may be restricted by upstream

67
docs/CMIS-PROFILES.md Normal file
View File

@@ -0,0 +1,67 @@
# CMIS Profiles
Status: draft
Created: 2026-05-07
## Purpose
`open-cmis-tck` uses guide-board target and assessment profiles without adding a
separate persisted profile format. This keeps the extension compatible with the
guide-board planner while still giving CMIS-specific diagnostics through
`open_cmis_tck.profile.validate_cmis_profile_config`.
## Target Profile Fields
The CMIS target profile uses the guide-board `target-profile` schema:
- `subject_type`: use `cmis-browser-binding-endpoint`.
- `endpoints`: include one endpoint with `binding` set to `cmis-browser` and
`url` set to the Browser Binding service document URL.
- `credentials_ref`: use `null` for anonymous/local development targets, or a
secret reference for authenticated repositories.
- `declared_capabilities`: list the CMIS requirement refs the target claims to
support, such as `cmis.repository-info`, `cmis.type-definitions`,
`cmis.object-services`, `cmis.content-streams`, `cmis.query`, `cmis.acl`, and
`cmis.versioning`.
- `known_gaps`: list unsupported optional requirements with a stable gap ID,
requirement refs, reason, and status such as `unsupported_by_design`.
## Assessment Runtime Fields
Repository selection and harness execution settings live in the assessment
profile because they are run policy, not target identity:
```json
{
"runtime_policy": {
"offline": false,
"timeout_seconds": 300,
"opencmis_tck": {
"repository_id": "compat-tck",
"requires_java_maven": true,
"command": ["java", "-jar", "/assets/opencmis-tck-runner.jar"]
}
}
}
```
`repository_id` is optional for preflight. If omitted, preflight selects the
first repository from the Browser Binding service document. A real TCK command
usually needs it.
`command` is optional. When absent, the wrapper reports
`tck_invocation_not_configured` as a structured, expected bootstrap blocker.
## Diagnostics
Use the extension helper from tests or local scripts:
```python
from open_cmis_tck.profile import validate_cmis_profile_config
diagnostics = validate_cmis_profile_config(target_profile, assessment_profile)
```
The result contains `status`, `diagnostics`, and the interpreted `cmis_config`.
Diagnostics are intentionally actionable: they point to the field that should be
changed and explain what the extension expects.

View File

@@ -0,0 +1,97 @@
# OpenCMIS TCK Runner
Status: draft
Created: 2026-05-07
## Purpose
The runner wrapper at `runners/opencmis_tck.py` is the boundary between
guide-board and Apache Chemistry OpenCMIS TCK execution. It keeps Java/Maven
setup, harness command lines, raw logs, and result normalization inside this
extension.
## Dependency Checks
By default, the wrapper checks:
- `java -version`
- `mvn -version`
If either dependency is unavailable, the runner returns `blocked` evidence with
`blocked_reason: missing_dependency`.
Set `runtime_policy.opencmis_tck.requires_java_maven` to `false` only for tests
or custom harness commands that do not use the local Java/Maven toolchain.
## Command Configuration
Configure a TCK command as an argv list:
```json
{
"runtime_policy": {
"opencmis_tck": {
"repository_id": "compat-tck",
"command": [
"java",
"-jar",
"/assets/opencmis-tck-runner.jar",
"--url",
"{browser_url}",
"--repository",
"{repository_id}",
"--group",
"{check_group}",
"--output",
"{artifact_dir}"
]
}
}
}
```
Supported placeholders:
- `{browser_url}`
- `{repository_id}`
- `{check_group}`
- `{target_id}`
- `{run_dir}`
- `{artifact_dir}`
The wrapper also accepts `OPENCMIS_TCK_COMMAND_JSON` as a JSON string array, or
`OPENCMIS_TCK_COMMAND` as a shell-like string that is split into argv. The final
command still runs without shell expansion.
## Raw Artifacts
For each selected check group, artifacts are written under:
```text
artifacts/open-cmis-tck/tck/<check-group>/
```
Current artifacts:
- `invocation.json`
- `stdout.log`
- `stderr.log`
- `normalized-runner-result.json`
The guide-board core fingerprints these files in the assessment package artifact
manifest when they are referenced by the runner result.
## Normalization
The wrapper normalizes, in order:
1. JSON written to stdout with a `tests`, `cases`, or `results` array.
2. JUnit-style XML files written directly into `{artifact_dir}`.
3. Exit code only, when no structured output is found.
Case statuses normalize to guide-board result vocabulary: `pass`, `fail`,
`skipped`, `expected_gap`, `unsupported_by_design`, `infrastructure_error`, and
related core statuses.
This is enough to run a real local TCK adapter while preserving raw logs for
future Apache Chemistry-specific parsing refinements.

View File

@@ -0,0 +1,56 @@
# Service And Retention Integration
Status: draft
Created: 2026-05-07
## Local Service
`open-cmis-tck` does not run its own service. It plugs into the guide-board
local API as an external extension:
```sh
cd ../guide-board
PYTHONPATH=src python3 -m guide_board \
--extension-dir ../open-cmis-tck \
serve --host 127.0.0.1 --port 8080
```
The guide-board service can then:
- list `open-cmis-tck` from `GET /extensions`,
- build CMIS run plans with `POST /assessments/plan`,
- start CMIS runs with `POST /runs`,
- inspect jobs with `GET /runs/{job_id}`,
- fetch reports with `GET /runs/{job_id}/reports`.
CLI execution remains the primary and most transparent path. The service is a
transport and job-tracking layer over the same runner contracts.
## Retention
CMIS runs use the guide-board run directory contract. Each run writes:
- `run.json`
- `retention-summary.json`
- `plan.json`
- `normalized/evidence.json`
- `normalized/findings.json`
- `normalized/mappings.json`
- `reports/assessment-package.json`
- `reports/report.md`
The sample assessment profile keeps summaries for 365 days and raw artifacts for
30 days:
```json
{
"retention_policy": {
"summary_days": 365,
"raw_artifact_days": 30
}
}
```
Compact `retention-summary.json` files are suitable for guide-board trend
summaries and downstream CMIS capability scorecards without retaining unbounded
raw TCK logs.

View File

@@ -55,6 +55,33 @@
"cmis.versioning" "cmis.versioning"
], ],
"runner_ref": "opencmis-tck" "runner_ref": "opencmis-tck"
},
{
"id": "relationships",
"name": "Relationship Checks",
"check_type": "executable_harness",
"requirement_refs": [
"cmis.relationships"
],
"runner_ref": "opencmis-tck"
},
{
"id": "change-log",
"name": "Change Log Checks",
"check_type": "executable_harness",
"requirement_refs": [
"cmis.change-log"
],
"runner_ref": "opencmis-tck"
},
{
"id": "extension-gaps",
"name": "Extension And Known Gap Checks",
"check_type": "executable_harness",
"requirement_refs": [
"cmis.extensions"
],
"runner_ref": "opencmis-tck"
} }
], ],
"preflight_runner": "cmis-browser-preflight", "preflight_runner": "cmis-browser-preflight",
@@ -78,7 +105,7 @@
"--context", "--context",
"{context_json}" "{context_json}"
], ],
"description": "Checks Java/Maven availability and prepares the future Apache Chemistry OpenCMIS TCK invocation." "description": "Checks Java/Maven availability, invokes a configured Apache Chemistry OpenCMIS TCK command, captures raw artifacts, and returns normalized evidence."
} }
], ],
"normalizers": [ "normalizers": [

View File

@@ -54,12 +54,40 @@
"label": "ACL And Policy", "label": "ACL And Policy",
"description": "Access-control list and policy behavior where supported by the target profile." "description": "Access-control list and policy behavior where supported by the target profile."
}, },
{
"requirement_ref": "cmis.policies",
"target_type": "capability_group",
"target_id": "acl-policy",
"label": "ACL And Policy",
"description": "Policy object behavior where supported by the target profile."
},
{ {
"requirement_ref": "cmis.versioning", "requirement_ref": "cmis.versioning",
"target_type": "capability_group", "target_type": "capability_group",
"target_id": "versioning", "target_id": "versioning",
"label": "Versioning", "label": "Versioning",
"description": "Checkout, checkin, version series, and version-specific object behavior." "description": "Checkout, checkin, version series, and version-specific object behavior."
},
{
"requirement_ref": "cmis.relationships",
"target_type": "capability_group",
"target_id": "relationships",
"label": "Relationships",
"description": "Relationship object and relationship navigation behavior where supported by the repository."
},
{
"requirement_ref": "cmis.change-log",
"target_type": "capability_group",
"target_id": "change-log",
"label": "Change Log",
"description": "Change log capability, token behavior, and change event retrieval."
},
{
"requirement_ref": "cmis.extensions",
"target_type": "capability_group",
"target_id": "extension-gaps",
"label": "Extensions And Known Gaps",
"description": "Explicitly scoped CMIS extensions, unsupported optional services, and compatibility gaps."
} }
] ]
} }

View File

@@ -28,6 +28,10 @@
}, },
"runtime_policy": { "runtime_policy": {
"offline": false, "offline": false,
"timeout_seconds": 300 "timeout_seconds": 300,
"opencmis_tck": {
"repository_id": "compat-tck",
"requires_java_maven": true
}
} }
} }

View File

@@ -1,17 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""OpenCMIS TCK wrapper boundary. """OpenCMIS TCK wrapper boundary.
This wrapper intentionally stops before invoking Apache Chemistry. Its current The wrapper owns extension-local orchestration only: dependency checks, optional
job is to prove the command-runner contract, verify local Java/Maven posture, and user-supplied TCK command execution, raw artifact capture, and normalization into
return structured evidence that the actual TCK execution remains pending. the guide-board runner result contract.
""" """
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import json import json
import os
import shutil import shutil
import shlex
import subprocess import subprocess
import xml.etree.ElementTree as ET
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -23,6 +26,7 @@ def main() -> int:
context = _load_context(Path(args.context)) context = _load_context(Path(args.context))
selected_group = context["step"].get("check_group") selected_group = context["step"].get("check_group")
config = _opencmis_policy(context)
dependency_results = { dependency_results = {
"java": _probe_command(["java", "-version"]), "java": _probe_command(["java", "-version"]),
"maven": _probe_command(["mvn", "-version"]), "maven": _probe_command(["mvn", "-version"]),
@@ -33,7 +37,7 @@ def main() -> int:
if not result["available"] if not result["available"]
] ]
if missing: if config.get("requires_java_maven", True) and missing:
_emit( _emit(
{ {
"result": "blocked", "result": "blocked",
@@ -52,6 +56,11 @@ def main() -> int:
) )
return 0 return 0
command_template = _configured_command(config)
if command_template is not None:
_emit(_run_configured_tck(context, selected_group, dependency_results, command_template))
return 0
_emit( _emit(
{ {
"result": "blocked", "result": "blocked",
@@ -62,7 +71,7 @@ def main() -> int:
"blocked_reason": "tck_invocation_not_configured", "blocked_reason": "tck_invocation_not_configured",
"selected_check_group": selected_group, "selected_check_group": selected_group,
"dependencies": dependency_results, "dependencies": dependency_results,
"next_step": "Resolve the Maven artifact, classpath, TCK group mapping, and raw artifact capture contract.", "next_step": "Configure runtime_policy.opencmis_tck.command or OPENCMIS_TCK_COMMAND_JSON with an argv list for the local Apache Chemistry TCK runner.",
}, },
"artifact_refs": [], "artifact_refs": [],
} }
@@ -108,6 +117,358 @@ def _probe_command(command: list[str]) -> dict[str, Any]:
} }
def _run_configured_tck(
context: dict[str, Any],
selected_group: str | None,
dependency_results: dict[str, dict[str, Any]],
command_template: list[str],
) -> dict[str, Any]:
run_dir = Path(context["run_dir"])
artifact_dir = run_dir / "artifacts" / "open-cmis-tck" / "tck" / _safe_id(selected_group or "unknown")
artifact_dir.mkdir(parents=True, exist_ok=True)
command = [_expand_arg(arg, context, selected_group, artifact_dir) for arg in command_template]
invocation_ref = _write_json_artifact(
run_dir,
artifact_dir,
"invocation.json",
{
"selected_check_group": selected_group,
"command": command,
"target_profile_id": context["target_profile"]["id"],
"repository_id": _repository_id(context),
"browser_binding_url": _browser_url(context),
},
)
try:
completed = subprocess.run(
command,
cwd=Path(context["extension_path"]),
capture_output=True,
text=True,
timeout=_timeout_seconds(context),
check=False,
)
except FileNotFoundError as exc:
return {
"result": "blocked",
"observations": [
f"OpenCMIS TCK command could not start because {exc.filename!r} was not found."
],
"facts": {
"blocked_reason": "missing_command",
"selected_check_group": selected_group,
"dependencies": dependency_results,
"command": command,
},
"artifact_refs": [invocation_ref],
}
except subprocess.TimeoutExpired:
return {
"result": "infrastructure_error",
"observations": [
f"OpenCMIS TCK command timed out after {_timeout_seconds(context)} seconds."
],
"facts": {
"selected_check_group": selected_group,
"dependencies": dependency_results,
"command": command,
},
"artifact_refs": [invocation_ref],
}
stdout_ref = _write_text_artifact(run_dir, artifact_dir, "stdout.log", completed.stdout)
stderr_ref = _write_text_artifact(run_dir, artifact_dir, "stderr.log", completed.stderr)
normalized = _normalize_tck_output(completed, artifact_dir, selected_group)
normalized["facts"].update(
{
"selected_check_group": selected_group,
"dependencies": dependency_results,
"command": command,
"returncode": completed.returncode,
"browser_binding_url": _browser_url(context),
"repository_id": _repository_id(context),
}
)
normalized_ref = _write_json_artifact(
run_dir,
artifact_dir,
"normalized-runner-result.json",
normalized,
)
normalized["artifact_refs"].extend([invocation_ref, stdout_ref, stderr_ref, normalized_ref])
return normalized
def _normalize_tck_output(
completed: subprocess.CompletedProcess[str],
artifact_dir: Path,
selected_group: str | None,
) -> dict[str, Any]:
parsed_stdout = _parse_json(completed.stdout)
if isinstance(parsed_stdout, dict):
return _normalize_json_result(parsed_stdout, completed.returncode, selected_group)
junit_files = sorted(artifact_dir.glob("*.xml"))
if junit_files:
return _normalize_junit_result(junit_files[0], completed.returncode, selected_group)
if completed.returncode == 0:
return {
"result": "pass",
"observations": [
"OpenCMIS TCK command completed successfully, but no structured result payload was found."
],
"facts": {
"normalizer": "exit-code",
"result_counts": {"pass": 1},
},
"artifact_refs": [],
}
return {
"result": "fail",
"observations": [
"OpenCMIS TCK command exited with a non-zero status and no structured result payload was found."
],
"facts": {
"normalizer": "exit-code",
"result_counts": {"fail": 1},
},
"artifact_refs": [],
}
def _normalize_json_result(
payload: dict[str, Any],
returncode: int,
selected_group: str | None,
) -> dict[str, Any]:
cases = _json_cases(payload)
if cases:
counts: dict[str, int] = {}
normalized_cases = []
for case in cases:
status = _normalize_case_status(str(case.get("status", "unknown")))
counts[status] = counts.get(status, 0) + 1
normalized_cases.append(
{
"id": str(case.get("id", case.get("name", "unnamed"))),
"status": status,
"message": str(case.get("message", case.get("reason", ""))),
}
)
return {
"result": _aggregate_result(counts, returncode),
"observations": [
f"OpenCMIS TCK group {selected_group!r} produced {sum(counts.values())} normalized case result(s)."
],
"facts": {
"normalizer": "json-cases",
"result_counts": counts,
"cases": normalized_cases[:200],
},
"artifact_refs": [],
}
result = _normalize_case_status(str(payload.get("result", "unknown")))
if returncode != 0 and result in {"pass", "warning", "skipped"}:
result = "infrastructure_error"
return {
"result": result,
"observations": _observations_from_payload(payload, selected_group),
"facts": {
"normalizer": "json-runner-result",
"result_counts": {result: 1},
"payload": payload,
},
"artifact_refs": [],
}
def _normalize_junit_result(
path: Path,
returncode: int,
selected_group: str | None,
) -> dict[str, Any]:
tree = ET.parse(path)
root = tree.getroot()
suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
total = sum(int(suite.get("tests", "0")) for suite in suites)
failures = sum(int(suite.get("failures", "0")) for suite in suites)
errors = sum(int(suite.get("errors", "0")) for suite in suites)
skipped = sum(int(suite.get("skipped", "0")) for suite in suites)
passed = max(0, total - failures - errors - skipped)
counts = {
"pass": passed,
"fail": failures + errors,
"skipped": skipped,
}
counts = {key: value for key, value in counts.items() if value}
return {
"result": _aggregate_result(counts, returncode),
"observations": [
f"OpenCMIS TCK group {selected_group!r} produced JUnit-style XML results."
],
"facts": {
"normalizer": "junit-xml",
"result_counts": counts,
"junit_xml": str(path),
},
"artifact_refs": [],
}
def _json_cases(payload: dict[str, Any]) -> list[dict[str, Any]]:
for key in ("tests", "cases", "results"):
value = payload.get(key)
if isinstance(value, list) and all(isinstance(item, dict) for item in value):
return value
return []
def _aggregate_result(counts: dict[str, int], returncode: int) -> str:
if counts.get("infrastructure_error"):
return "infrastructure_error"
if counts.get("fail"):
return "fail"
if counts.get("pass"):
return "pass"
if counts.get("expected_gap"):
return "expected_gap"
if counts.get("unsupported_by_design"):
return "unsupported_by_design"
if counts.get("skipped"):
return "skipped"
return "infrastructure_error" if returncode else "unknown"
def _normalize_case_status(value: str) -> str:
normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
if normalized in {"ok", "success", "passed"}:
return "pass"
if normalized in {"failure", "failed", "error"}:
return "fail"
if normalized in {"skip", "skipped"}:
return "skipped"
if normalized in {"expected_skip", "expected_gap"}:
return "expected_gap"
if normalized in {"unsupported", "unsupported_by_design"}:
return "unsupported_by_design"
if normalized in {"infra", "infrastructure_error"}:
return "infrastructure_error"
if normalized in {
"pass",
"fail",
"warning",
"manual",
"not_applicable",
"waiver_applied",
"blocked",
"unknown",
}:
return normalized
return "unknown"
def _observations_from_payload(payload: dict[str, Any], selected_group: str | None) -> list[str]:
observations = payload.get("observations")
if isinstance(observations, list):
return [str(item) for item in observations]
message = payload.get("message")
if isinstance(message, str) and message:
return [message]
return [f"OpenCMIS TCK group {selected_group!r} returned a structured result."]
def _opencmis_policy(context: dict[str, Any]) -> dict[str, Any]:
policy = context["assessment_profile"].get("runtime_policy", {}).get("opencmis_tck", {})
return policy if isinstance(policy, dict) else {}
def _configured_command(config: dict[str, Any]) -> list[str] | None:
command = config.get("command")
if isinstance(command, list) and all(isinstance(item, str) and item for item in command):
return command
env_json = os.environ.get("OPENCMIS_TCK_COMMAND_JSON")
if env_json:
parsed = json.loads(env_json)
if isinstance(parsed, list) and all(isinstance(item, str) and item for item in parsed):
return parsed
raise ValueError("OPENCMIS_TCK_COMMAND_JSON must be a JSON string array")
env_command = os.environ.get("OPENCMIS_TCK_COMMAND")
if env_command:
return shlex.split(env_command)
return None
def _expand_arg(
value: str,
context: dict[str, Any],
selected_group: str | None,
artifact_dir: Path,
) -> str:
return (
value.replace("{run_dir}", context["run_dir"])
.replace("{artifact_dir}", str(artifact_dir))
.replace("{browser_url}", _browser_url(context) or "")
.replace("{repository_id}", _repository_id(context) or "")
.replace("{check_group}", selected_group or "")
.replace("{target_id}", context["target_profile"]["id"])
)
def _browser_url(context: dict[str, Any]) -> str | None:
for endpoint in context["target_profile"].get("endpoints", []):
if endpoint.get("binding") == "cmis-browser":
return endpoint.get("url")
return None
def _repository_id(context: dict[str, Any]) -> str | None:
value = _opencmis_policy(context).get("repository_id")
return value if isinstance(value, str) else None
def _timeout_seconds(context: dict[str, Any]) -> float:
runtime_policy = context["assessment_profile"].get("runtime_policy", {})
opencmis_policy = _opencmis_policy(context)
configured = opencmis_policy.get("timeout_seconds", runtime_policy.get("timeout_seconds", 300))
if not isinstance(configured, (int, float)):
return 300.0
return max(1.0, float(configured))
def _write_text_artifact(run_dir: Path, artifact_dir: Path, name: str, value: str) -> str:
path = artifact_dir / name
path.write_text(value, encoding="utf-8")
return str(path.relative_to(run_dir))
def _write_json_artifact(
run_dir: Path,
artifact_dir: Path,
name: str,
value: dict[str, Any],
) -> str:
path = artifact_dir / name
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return str(path.relative_to(run_dir))
def _parse_json(value: str) -> Any:
try:
return json.loads(value)
except json.JSONDecodeError:
return None
def _safe_id(value: str) -> str:
return "".join(char if char.isalnum() or char in {"-", "_"} else "_" for char in value)
def _emit(value: dict[str, Any]) -> None: def _emit(value: dict[str, Any]) -> None:
print(json.dumps(value, indent=2, sort_keys=True)) print(json.dumps(value, indent=2, sort_keys=True))

View File

@@ -117,12 +117,43 @@ def run(context: dict[str, Any]) -> dict[str, Any]:
} }
facts["json_detected"] = True facts["json_detected"] = True
facts.update(_repository_facts(parsed)) repository_facts = _repository_facts(parsed, context)
facts.update(repository_facts)
unsupported = [
item
for item in repository_facts.get("capability_posture", [])
if item.get("status") == "unsupported"
]
expected_gaps = [
item
for item in repository_facts.get("capability_posture", [])
if item.get("status") == "expected_gap"
]
if unsupported:
return {
"result": "fail",
"observations": [
"CMIS Browser Binding endpoint is reachable, but declared capabilities are not supported by repository capability flags.",
"Unsupported declared requirements: "
+ ", ".join(item["requirement_ref"] for item in unsupported)
+ ".",
],
"facts": facts,
"artifact_refs": artifact_refs,
}
observations = [
"CMIS Browser Binding endpoint is reachable and returned parseable JSON."
]
if expected_gaps:
observations.append(
"Unsupported optional capabilities were accepted as known gaps: "
+ ", ".join(item["requirement_ref"] for item in expected_gaps)
+ "."
)
return { return {
"result": "pass", "result": "pass",
"observations": [ "observations": observations,
"CMIS Browser Binding endpoint is reachable and returned parseable JSON."
],
"facts": facts, "facts": facts,
"artifact_refs": artifact_refs, "artifact_refs": artifact_refs,
} }
@@ -150,29 +181,40 @@ def _parse_json(body: bytes) -> Any:
return None return None
def _repository_facts(value: Any) -> dict[str, Any]: def _repository_facts(value: Any, context: dict[str, Any]) -> dict[str, Any]:
if not isinstance(value, dict): if not isinstance(value, dict):
return {"repository_shape": "unknown"} return {"repository_shape": "unknown"}
if "repositoryId" in value: if "repositoryId" in value:
repository_id = str(value["repositoryId"])
return { return {
"repository_shape": "single-repository-info", "repository_shape": "single-repository-info",
"repository_ids": [value["repositoryId"]], "repository_ids": [repository_id],
"selected_repository_id": repository_id,
"cmis_version_supported": value.get("cmisVersionSupported"), "cmis_version_supported": value.get("cmisVersionSupported"),
"capabilities_present": isinstance(value.get("capabilities"), dict), "capabilities_present": isinstance(value.get("capabilities"), dict),
"capability_flags": _capability_flags(value),
"capability_posture": _capability_posture(value, context),
} }
repository_ids = [] repositories: dict[str, dict[str, Any]] = {}
for key, child in value.items(): for key, child in value.items():
if isinstance(child, dict) and ( if isinstance(child, dict) and (
"repositoryId" in child or "repositoryName" in child "repositoryId" in child or "repositoryName" in child
): ):
repository_ids.append(str(child.get("repositoryId", key))) repositories[str(child.get("repositoryId", key))] = child
if repository_ids: if repositories:
selected_repository_id = _selected_repository_id(repositories, context)
selected_repository = repositories[selected_repository_id]
return { return {
"repository_shape": "repository-map", "repository_shape": "repository-map",
"repository_ids": repository_ids, "repository_ids": sorted(repositories),
"selected_repository_id": selected_repository_id,
"cmis_version_supported": selected_repository.get("cmisVersionSupported"),
"capabilities_present": isinstance(selected_repository.get("capabilities"), dict),
"capability_flags": _capability_flags(selected_repository),
"capability_posture": _capability_posture(selected_repository, context),
} }
return { return {
@@ -181,6 +223,99 @@ def _repository_facts(value: Any) -> dict[str, Any]:
} }
def _selected_repository_id(
repositories: dict[str, dict[str, Any]],
context: dict[str, Any],
) -> str:
configured = _opencmis_policy(context).get("repository_id")
if isinstance(configured, str) and configured in repositories:
return configured
return sorted(repositories)[0]
def _capability_flags(repository_info: dict[str, Any]) -> dict[str, Any]:
capabilities = repository_info.get("capabilities", {})
return dict(capabilities) if isinstance(capabilities, dict) else {}
def _capability_posture(
repository_info: dict[str, Any],
context: dict[str, Any],
) -> list[dict[str, Any]]:
target = context["target_profile"]
declared = set(target.get("declared_capabilities", []))
known_gap_refs = {
requirement_ref: gap["id"]
for gap in target.get("known_gaps", [])
for requirement_ref in gap.get("requirement_refs", [])
}
refs = sorted(declared | set(known_gap_refs))
flags = _capability_flags(repository_info)
posture = []
for requirement_ref in refs:
support = _requirement_support(requirement_ref, flags)
if support is True:
status = "supported"
elif support is False and requirement_ref in known_gap_refs:
status = "expected_gap"
elif support is False:
status = "unsupported"
else:
status = "unknown"
posture.append(
{
"requirement_ref": requirement_ref,
"status": status,
"known_gap_ref": known_gap_refs.get(requirement_ref),
"flag_refs": _flag_refs(requirement_ref),
}
)
return posture
def _requirement_support(requirement_ref: str, flags: dict[str, Any]) -> bool | None:
if requirement_ref == "cmis.repository-info":
return True
flag_refs = _flag_refs(requirement_ref)
if not flag_refs:
return None
values = [flags[key] for key in flag_refs if key in flags]
if not values:
return None
return any(_flag_supports(value) for value in values)
def _flag_refs(requirement_ref: str) -> list[str]:
return {
"cmis.query": ["capabilityQuery"],
"cmis.acl": ["capabilityACL"],
"cmis.navigation-services": [
"capabilityGetDescendants",
"capabilityGetFolderTree",
],
"cmis.relationships": ["capabilityJoin"],
"cmis.change-log": ["capabilityChanges"],
"cmis.versioning": [
"capabilityVersionSpecificFiling",
"capabilityPWCSearchable",
"capabilityPWCUpdatable",
],
}.get(requirement_ref, [])
def _flag_supports(value: Any) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() not in {"", "false", "none", "no"}
return value is not None
def _opencmis_policy(context: dict[str, Any]) -> dict[str, Any]:
policy = context["assessment_profile"].get("runtime_policy", {}).get("opencmis_tck", {})
return policy if isinstance(policy, dict) else {}
def _write_response_artifacts( def _write_response_artifacts(
context: dict[str, Any], context: dict[str, Any],
status_code: int, status_code: int,

View File

@@ -0,0 +1,146 @@
"""CMIS-specific profile diagnostics for guide-board target profiles."""
from __future__ import annotations
from typing import Any
from urllib.parse import urlparse
KNOWN_CMIS_REQUIREMENTS = {
"cmis.repository-info",
"cmis.type-definitions",
"cmis.object-services",
"cmis.content-streams",
"cmis.navigation-services",
"cmis.query",
"cmis.relationships",
"cmis.acl",
"cmis.policies",
"cmis.versioning",
"cmis.change-log",
"cmis.extensions",
}
def validate_cmis_profile_config(
target_profile: dict[str, Any],
assessment_profile: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Return actionable CMIS diagnostics for guide-board profiles.
The generic guide-board target profile remains the persisted contract. This
helper explains how the OpenCMIS extension interprets those generic fields.
"""
diagnostics: list[dict[str, str]] = []
browser_endpoints = [
endpoint
for endpoint in target_profile.get("endpoints", [])
if endpoint.get("binding") == "cmis-browser"
]
if target_profile.get("subject_type") != "cmis-browser-binding-endpoint":
diagnostics.append(
_diagnostic(
"warning",
"subject_type",
"CMIS targets should use subject_type 'cmis-browser-binding-endpoint'.",
)
)
if not browser_endpoints:
diagnostics.append(
_diagnostic(
"error",
"endpoints",
"Add one endpoint with binding 'cmis-browser' and the Browser Binding service document URL.",
)
)
for index, endpoint in enumerate(browser_endpoints):
parsed = urlparse(str(endpoint.get("url", "")))
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
diagnostics.append(
_diagnostic(
"error",
f"endpoints[{index}].url",
"Use an absolute http(s) CMIS Browser Binding URL.",
)
)
declared = set(target_profile.get("declared_capabilities", []))
unknown_declared = sorted(declared - KNOWN_CMIS_REQUIREMENTS)
for requirement_ref in unknown_declared:
diagnostics.append(
_diagnostic(
"warning",
"declared_capabilities",
f"Declared CMIS capability {requirement_ref!r} is not in the extension's known requirement list.",
)
)
known_gap_refs = {
requirement_ref
for gap in target_profile.get("known_gaps", [])
for requirement_ref in gap.get("requirement_refs", [])
}
unexpected_gap_refs = sorted(known_gap_refs - KNOWN_CMIS_REQUIREMENTS)
for requirement_ref in unexpected_gap_refs:
diagnostics.append(
_diagnostic(
"warning",
"known_gaps",
f"Known gap {requirement_ref!r} is not in the extension's known requirement list.",
)
)
runtime_policy = (assessment_profile or {}).get("runtime_policy", {})
opencmis_policy = runtime_policy.get("opencmis_tck", {})
if opencmis_policy and not isinstance(opencmis_policy, dict):
diagnostics.append(
_diagnostic(
"error",
"runtime_policy.opencmis_tck",
"OpenCMIS runtime policy must be an object when present.",
)
)
opencmis_policy = {}
timeout = runtime_policy.get("timeout_seconds")
if assessment_profile is not None and not isinstance(timeout, (int, float)):
diagnostics.append(
_diagnostic(
"warning",
"runtime_policy.timeout_seconds",
"Set timeout_seconds to bound preflight and TCK execution.",
)
)
repository_id = None
if isinstance(opencmis_policy, dict):
repository_id = opencmis_policy.get("repository_id")
if repository_id is not None and not isinstance(repository_id, str):
diagnostics.append(
_diagnostic(
"error",
"runtime_policy.opencmis_tck.repository_id",
"repository_id must be a string when configured.",
)
)
status = "invalid" if any(item["severity"] == "error" for item in diagnostics) else "valid"
return {
"status": status,
"diagnostics": diagnostics,
"cmis_config": {
"browser_binding_url": browser_endpoints[0]["url"] if browser_endpoints else None,
"repository_id": repository_id,
"auth_mode": "anonymous" if target_profile.get("credentials_ref") is None else "credentials_ref",
"declared_capabilities": sorted(declared),
"known_gap_refs": sorted(known_gap_refs),
"timeout_seconds": timeout,
},
}
def _diagnostic(severity: str, field: str, message: str) -> dict[str, str]:
return {"severity": severity, "field": field, "message": message}

View File

@@ -1,7 +1,10 @@
from __future__ import annotations from __future__ import annotations
import http.client
import json import json
import sys
import threading import threading
import time
import unittest import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path from pathlib import Path
@@ -10,6 +13,9 @@ from tempfile import TemporaryDirectory
from guide_board.discovery import discover_extensions from guide_board.discovery import discover_extensions
from guide_board.execution import run_assessment from guide_board.execution import run_assessment
from guide_board.planning import build_run_plan, validate_assessment_profile from guide_board.planning import build_run_plan, validate_assessment_profile
from guide_board.retention import build_trend_summary
from guide_board.service import ServiceHandle, start_service
from open_cmis_tck.profile import validate_cmis_profile_config
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
@@ -44,6 +50,37 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
self.assertEqual(plan["extension_snapshots"][0]["path"], str(ROOT)) self.assertEqual(plan["extension_snapshots"][0]["path"], str(ROOT))
self.assertEqual(len(plan["ordered_steps"]), 3) self.assertEqual(len(plan["ordered_steps"]), 3)
def test_validates_cmis_profile_config_with_actionable_diagnostics(self) -> None:
target = json.loads(
(ROOT / "profiles" / "targets" / "kontextual-cmis-compat.json").read_text(
encoding="utf-8"
)
)
assessment = json.loads(
(ROOT / "profiles" / "assessments" / "cmis-browser-baseline.json").read_text(
encoding="utf-8"
)
)
diagnostics = validate_cmis_profile_config(target, assessment)
self.assertEqual(diagnostics["status"], "valid")
self.assertEqual(
diagnostics["cmis_config"]["browser_binding_url"],
"http://127.0.0.1:8000/cmis/compat-tck/browser",
)
self.assertEqual(diagnostics["cmis_config"]["repository_id"], "compat-tck")
self.assertEqual(diagnostics["cmis_config"]["auth_mode"], "anonymous")
broken = dict(target)
broken["endpoints"] = []
broken_diagnostics = validate_cmis_profile_config(broken, assessment)
self.assertEqual(broken_diagnostics["status"], "invalid")
self.assertIn(
"Add one endpoint with binding 'cmis-browser'",
broken_diagnostics["diagnostics"][0]["message"],
)
def test_runs_cmis_preflight_against_local_endpoint(self) -> None: def test_runs_cmis_preflight_against_local_endpoint(self) -> None:
server = HTTPServer(("127.0.0.1", 0), _CmisHandler) server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
thread = threading.Thread(target=server.serve_forever) thread = threading.Thread(target=server.serve_forever)
@@ -88,6 +125,11 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
evidence[0]["facts"]["repository_ids"], evidence[0]["facts"]["repository_ids"],
["local-test-repository"], ["local-test-repository"],
) )
posture = {
item["requirement_ref"]: item["status"]
for item in evidence[0]["facts"]["capability_posture"]
}
self.assertEqual(posture["cmis.repository-info"], "supported")
self.assertEqual(len(package["artifact_manifest"]), 2) self.assertEqual(len(package["artifact_manifest"]), 2)
self.assertTrue( self.assertTrue(
( (
@@ -103,6 +145,60 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
thread.join(timeout=5) thread.join(timeout=5)
server.server_close() server.server_close()
def test_preflight_accepts_unsupported_optional_capability_as_known_gap(self) -> None:
server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
thread = threading.Thread(target=server.serve_forever)
thread.daemon = True
thread.start()
try:
with TemporaryDirectory() as temporary_directory:
temp_root = Path(temporary_directory)
target_path = temp_root / "target.json"
assessment_path = temp_root / "assessment.json"
_write_target(target_path, server.server_port, "local-cmis-query-gap")
target = json.loads(target_path.read_text(encoding="utf-8"))
target["declared_capabilities"].append("cmis.query")
target["known_gaps"].append(
{
"id": "query-not-targeted",
"requirement_refs": ["cmis.query"],
"reason": "The local fixture deliberately reports no query support.",
"status": "unsupported_by_design",
}
)
target_path.write_text(json.dumps(target), encoding="utf-8")
_write_assessment(
assessment_path,
"local-cmis-known-gap",
"local-cmis-query-gap",
[],
None,
)
result = run_assessment(
CORE_ROOT,
target_path,
assessment_path,
temp_root / "run",
[ROOT],
)
evidence = json.loads(
(Path(result["run_dir"]) / "normalized" / "evidence.json").read_text(
encoding="utf-8"
)
)["evidence"]
posture = {
item["requirement_ref"]: item["status"]
for item in evidence[0]["facts"]["capability_posture"]
}
self.assertEqual(result["status"], "completed")
self.assertEqual(posture["cmis.query"], "expected_gap")
finally:
server.shutdown()
thread.join(timeout=5)
server.server_close()
def test_runs_cmis_tck_command_wrapper_boundary(self) -> None: def test_runs_cmis_tck_command_wrapper_boundary(self) -> None:
server = HTTPServer(("127.0.0.1", 0), _CmisHandler) server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
thread = threading.Thread(target=server.serve_forever) thread = threading.Thread(target=server.serve_forever)
@@ -163,6 +259,144 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
thread.join(timeout=5) thread.join(timeout=5)
server.server_close() server.server_close()
def test_runs_configured_tck_command_and_normalizes_json_results(self) -> None:
server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
thread = threading.Thread(target=server.serve_forever)
thread.daemon = True
thread.start()
try:
with TemporaryDirectory() as temporary_directory:
temp_root = Path(temporary_directory)
target_path = temp_root / "target.json"
assessment_path = temp_root / "assessment.json"
fake_tck = temp_root / "fake_tck.py"
fake_tck.write_text(
"\n".join(
[
"import json",
"print(json.dumps({",
" 'tests': [",
" {'id': 'repository-info', 'status': 'pass'},",
" {'id': 'type-definitions', 'status': 'pass'}",
" ]",
"}))",
]
),
encoding="utf-8",
)
_write_target(target_path, server.server_port, "local-cmis-configured-tck")
_write_assessment(
assessment_path,
"local-cmis-configured-tck",
"local-cmis-configured-tck",
["repository-type"],
None,
{
"requires_java_maven": False,
"repository_id": "local-test-repository",
"command": [
sys.executable,
str(fake_tck),
"--url",
"{browser_url}",
"--repository",
"{repository_id}",
"--group",
"{check_group}",
],
},
)
result = run_assessment(
CORE_ROOT,
target_path,
assessment_path,
temp_root / "run",
[ROOT],
)
run_dir = Path(result["run_dir"])
evidence = json.loads(
(run_dir / "normalized" / "evidence.json").read_text(encoding="utf-8")
)["evidence"]
retention = json.loads(
(run_dir / "retention-summary.json").read_text(encoding="utf-8")
)
trend = build_trend_summary(temp_root)
self.assertEqual(result["status"], "completed")
self.assertEqual(evidence[1]["result"], "pass")
self.assertEqual(evidence[1]["facts"]["normalizer"], "json-cases")
self.assertEqual(evidence[1]["facts"]["result_counts"], {"pass": 2})
self.assertTrue(
(
run_dir
/ "artifacts"
/ "open-cmis-tck"
/ "tck"
/ "repository-type"
/ "stdout.log"
).exists()
)
self.assertEqual(retention["summary"]["status"], "completed")
self.assertGreaterEqual(retention["summary"]["artifact_count"], 4)
self.assertEqual(trend["run_count"], 1)
finally:
server.shutdown()
thread.join(timeout=5)
server.server_close()
def test_guide_board_service_runs_cmis_extension(self) -> None:
server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
thread = threading.Thread(target=server.serve_forever)
thread.daemon = True
thread.start()
service = start_service(CORE_ROOT, [ROOT], host="127.0.0.1", port=0)
try:
with TemporaryDirectory() as temporary_directory:
temp_root = Path(temporary_directory)
target_path = temp_root / "target.json"
assessment_path = temp_root / "assessment.json"
_write_target(target_path, server.server_port, "local-cmis-service")
_write_assessment(
assessment_path,
"local-cmis-service",
"local-cmis-service",
[],
None,
)
extensions = _request_json(service, "GET", "/extensions")
self.assertIn(
"open-cmis-tck",
[extension["id"] for extension in extensions["extensions"]],
)
job = _request_json(
service,
"POST",
"/runs",
{
"target": str(target_path),
"assessment": str(assessment_path),
"output_dir": str(temp_root / "service-run"),
},
expected_status=202,
)
status = _wait_for_job(service, job["job_id"])
reports = _request_json(service, "GET", f"/runs/{job['job_id']}/reports")
self.assertEqual(status["status"], "succeeded")
self.assertEqual(status["result"]["status"], "completed")
self.assertEqual(
reports["assessment_package"]["json"]["extensions"][0]["id"],
"open-cmis-tck",
)
finally:
service.stop()
server.shutdown()
thread.join(timeout=5)
server.server_close()
def test_preflight_failure_blocks_downstream_checks(self) -> None: def test_preflight_failure_blocks_downstream_checks(self) -> None:
with TemporaryDirectory() as temporary_directory: with TemporaryDirectory() as temporary_directory:
temp_root = Path(temporary_directory) temp_root = Path(temporary_directory)
@@ -259,7 +493,14 @@ def _write_assessment(
target_id: str, target_id: str,
check_groups: list[str], check_groups: list[str],
waiver_ref: str | None, waiver_ref: str | None,
opencmis_policy: dict[str, object] | None = None,
) -> None: ) -> None:
runtime_policy: dict[str, object] = {
"offline": False,
"timeout_seconds": 15,
}
if opencmis_policy is not None:
runtime_policy["opencmis_tck"] = opencmis_policy
path.write_text( path.write_text(
json.dumps( json.dumps(
{ {
@@ -278,10 +519,7 @@ def _write_assessment(
"summary_days": 365, "summary_days": 365,
"raw_artifact_days": 0, "raw_artifact_days": 0,
}, },
"runtime_policy": { "runtime_policy": runtime_policy,
"offline": False,
"timeout_seconds": 15,
},
} }
), ),
encoding="utf-8", encoding="utf-8",
@@ -316,6 +554,42 @@ def _write_command_waiver(path: Path, target_id: str) -> None:
) )
def _request_json(
service: ServiceHandle,
method: str,
path: str,
payload: dict[str, object] | None = None,
expected_status: int = 200,
) -> dict[str, object]:
connection = http.client.HTTPConnection(service.host, service.port, timeout=5)
body = None
headers = {}
if payload is not None:
body = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
try:
connection.request(method, path, body=body, headers=headers)
response = connection.getresponse()
data = response.read().decode("utf-8")
finally:
connection.close()
if response.status != expected_status:
raise AssertionError(f"expected HTTP {expected_status}, got {response.status}: {data}")
value = json.loads(data)
if not isinstance(value, dict):
raise AssertionError(f"expected JSON object response, got {type(value).__name__}")
return value
def _wait_for_job(service: ServiceHandle, job_id: str) -> dict[str, object]:
for _ in range(50):
status = _request_json(service, "GET", f"/runs/{job_id}")
if status["status"] in {"succeeded", "failed"}:
return status
time.sleep(0.05)
raise AssertionError(f"job did not finish: {job_id}")
class _CmisHandler(BaseHTTPRequestHandler): class _CmisHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None: def do_GET(self) -> None:
body = json.dumps( body = json.dumps(
@@ -324,7 +598,13 @@ class _CmisHandler(BaseHTTPRequestHandler):
"repositoryId": "local-test-repository", "repositoryId": "local-test-repository",
"repositoryName": "Local Test Repository", "repositoryName": "Local Test Repository",
"cmisVersionSupported": "1.1", "cmisVersionSupported": "1.1",
"capabilities": {}, "capabilities": {
"capabilityACL": "discover",
"capabilityChanges": "none",
"capabilityGetDescendants": True,
"capabilityGetFolderTree": True,
"capabilityQuery": "none",
},
} }
} }
).encode("utf-8") ).encode("utf-8")

View File

@@ -2,10 +2,10 @@
id: OPEN-CMIS-TCK-WP-0001 id: OPEN-CMIS-TCK-WP-0001
type: extension-workplan type: extension-workplan
title: "OpenCMIS TCK Harness Foundation" title: "OpenCMIS TCK Harness Foundation"
repo: guide-board repo: open-cmis-tck
extension: open-cmis-tck extension: open-cmis-tck
domain: markitect domain: markitect
status: active status: completed
owner: codex owner: codex
planning_priority: high planning_priority: high
planning_order: 2 planning_order: 2
@@ -81,7 +81,7 @@ Progress:
```task ```task
id: OPEN-CMIS-TCK-WP-0001-T002 id: OPEN-CMIS-TCK-WP-0001-T002
status: todo status: done
priority: high priority: high
state_hub_task_id: "2ccc74a7-bed9-4769-8608-d579fdf3a0cd" state_hub_task_id: "2ccc74a7-bed9-4769-8608-d579fdf3a0cd"
``` ```
@@ -95,11 +95,19 @@ Acceptance:
- Profile validation produces actionable diagnostics for missing or invalid - Profile validation produces actionable diagnostics for missing or invalid
fields. fields.
Progress:
- Added CMIS-specific profile diagnostics in `open_cmis_tck.profile`.
- Documented the CMIS profile contract in `docs/CMIS-PROFILES.md`.
- Added `runtime_policy.opencmis_tck.repository_id` to the baseline assessment
profile while keeping the persisted profiles compatible with guide-board core
schemas.
## D1.3 - CMIS Preflight Probe ## D1.3 - CMIS Preflight Probe
```task ```task
id: OPEN-CMIS-TCK-WP-0001-T003 id: OPEN-CMIS-TCK-WP-0001-T003
status: in_progress status: done
priority: high priority: high
state_hub_task_id: "6d45885b-78a4-4e8b-8fcc-b8d6488e703b" state_hub_task_id: "6d45885b-78a4-4e8b-8fcc-b8d6488e703b"
``` ```
@@ -119,14 +127,15 @@ Progress:
assessment-package fingerprinting. assessment-package fingerprinting.
- Failed CMIS preflight now blocks downstream OpenCMIS TCK groups instead of - Failed CMIS preflight now blocks downstream OpenCMIS TCK groups instead of
invoking the Java/Maven wrapper against an invalid target. invoking the Java/Maven wrapper against an invalid target.
- Capability flag normalization remains to be expanded after a live target sample - Capability flags are normalized into repository capability posture facts.
is captured. - Unsupported optional capabilities can be accepted as target-profile known gaps
without hiding unexpected unsupported declared capabilities.
## D1.4 - OpenCMIS TCK Runner Wrapper ## D1.4 - OpenCMIS TCK Runner Wrapper
```task ```task
id: OPEN-CMIS-TCK-WP-0001-T004 id: OPEN-CMIS-TCK-WP-0001-T004
status: in_progress status: done
priority: high priority: high
state_hub_task_id: "502d7586-6f9e-475e-9683-43260666d5d9" state_hub_task_id: "502d7586-6f9e-475e-9683-43260666d5d9"
``` ```
@@ -145,14 +154,17 @@ Progress:
evidence when dependencies or final TCK invocation details are missing. evidence when dependencies or final TCK invocation details are missing.
- `profiles/expectations/cmis-local-harness.json` marks local bootstrap blockers - `profiles/expectations/cmis-local-harness.json` marks local bootstrap blockers
as expected without hiding target preflight failures. as expected without hiding target preflight failures.
- Actual Apache Chemistry TCK classpath resolution, group invocation, and raw log - The wrapper can invoke a configured argv command from
capture remain to be implemented. `runtime_policy.opencmis_tck.command` or environment variables.
- The wrapper expands CMIS placeholders, captures stdout/stderr and invocation
metadata under the run directory, and skips cleanly when Java/Maven or command
configuration are missing.
## D1.5 - CMIS Result Normalization ## D1.5 - CMIS Result Normalization
```task ```task
id: OPEN-CMIS-TCK-WP-0001-T005 id: OPEN-CMIS-TCK-WP-0001-T005
status: todo status: done
priority: high priority: high
state_hub_task_id: "716486b6-6f14-41f8-8417-5015ba746005" state_hub_task_id: "716486b6-6f14-41f8-8417-5015ba746005"
``` ```
@@ -165,11 +177,19 @@ Acceptance:
- Failures include enough context to map back to TCK group, capability group, - Failures include enough context to map back to TCK group, capability group,
target profile, and raw artifact paths. target profile, and raw artifact paths.
Progress:
- Added runner-side normalization for JSON case lists, JSON runner results,
JUnit-style XML, and exit-code-only output.
- Normalized result counts, case IDs, selected check group, target URL,
repository ID, return code, and artifact paths are captured in guide-board
evidence facts.
## D1.6 - Capability Mapping And Reports ## D1.6 - Capability Mapping And Reports
```task ```task
id: OPEN-CMIS-TCK-WP-0001-T006 id: OPEN-CMIS-TCK-WP-0001-T006
status: in_progress status: done
priority: high priority: high
state_hub_task_id: "9f7dacc5-4d19-4755-aa9a-8572d4285514" state_hub_task_id: "9f7dacc5-4d19-4755-aa9a-8572d4285514"
``` ```
@@ -190,12 +210,16 @@ Progress:
groups. groups.
- Guide-board writes normalized mapping records and includes capability-group - Guide-board writes normalized mapping records and includes capability-group
counts in Markdown reports. counts in Markdown reports.
- Mapping coverage now includes relationships, change log, policy, and extension
gap requirement refs.
- Additional check groups are declared for relationships, change log, and
extension/known-gap review.
## D1.7 - Optional Local Service API Adapter ## D1.7 - Optional Local Service API Adapter
```task ```task
id: OPEN-CMIS-TCK-WP-0001-T007 id: OPEN-CMIS-TCK-WP-0001-T007
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "a05e47bd-88db-4878-aef4-bf328790c3f0" state_hub_task_id: "a05e47bd-88db-4878-aef4-bf328790c3f0"
``` ```
@@ -207,11 +231,17 @@ Acceptance:
- CLI operation remains the primary path. - CLI operation remains the primary path.
- Long-running TCK jobs are tracked without blocking the API process. - Long-running TCK jobs are tracked without blocking the API process.
Progress:
- Verified `open-cmis-tck` through guide-board's local service as an external
extension.
- Documented service usage in `docs/SERVICE-AND-RETENTION.md`.
## D1.8 - Historical Result Retention ## D1.8 - Historical Result Retention
```task ```task
id: OPEN-CMIS-TCK-WP-0001-T008 id: OPEN-CMIS-TCK-WP-0001-T008
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "c27ea43f-41ec-49d0-a890-3681455f7c6c" state_hub_task_id: "c27ea43f-41ec-49d0-a890-3681455f7c6c"
``` ```
@@ -223,6 +253,14 @@ Acceptance:
- Summaries are suitable for trend charts and downstream capability-score - Summaries are suitable for trend charts and downstream capability-score
updates. updates.
Progress:
- CMIS runs now exercise guide-board retention summaries and trend input through
extension tests.
- The sample assessment profile declares 365-day summary retention and 30-day
raw artifact retention.
- Retention behavior is documented in `docs/SERVICE-AND-RETENTION.md`.
## Definition Of Done ## Definition Of Done
- A developer can configure a CMIS Browser Binding endpoint. - A developer can configure a CMIS Browser Binding endpoint.