generated from coulomb/repo-seed
maturity scorecard generation
This commit is contained in:
@@ -62,6 +62,10 @@ Runner command configuration lives in
|
||||
`runtime_policy.opencmis_tck.command`. See
|
||||
[docs/OPENCMIS-TCK-RUNNER.md](docs/OPENCMIS-TCK-RUNNER.md).
|
||||
|
||||
If the workstation does not have Java and Maven in WSL, see
|
||||
[docs/LOCAL-RUNBOOK.md](docs/LOCAL-RUNBOOK.md) for the repo-local toolchain
|
||||
installer path.
|
||||
|
||||
## Tests
|
||||
|
||||
Run extension tests with the guide-board core on `PYTHONPATH`:
|
||||
@@ -73,6 +77,7 @@ PYTHONPATH=../guide-board/src python3 -m unittest discover -s tests
|
||||
## Docs
|
||||
|
||||
- [docs/CMIS-PROFILES.md](docs/CMIS-PROFILES.md)
|
||||
- [docs/CMIS-MATURITY-SCORECARD.md](docs/CMIS-MATURITY-SCORECARD.md)
|
||||
- [docs/CONTAINER-HANDOFF.md](docs/CONTAINER-HANDOFF.md)
|
||||
- [docs/LOCAL-RUNBOOK.md](docs/LOCAL-RUNBOOK.md)
|
||||
- [docs/LOCAL-TCK-RUNTIME.md](docs/LOCAL-TCK-RUNTIME.md)
|
||||
|
||||
@@ -7,10 +7,19 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from open_cmis_tck.normalization import ( # noqa: E402
|
||||
aggregate_case_result,
|
||||
parse_text_report,
|
||||
result_counts,
|
||||
)
|
||||
|
||||
|
||||
GROUP_CLASSES = {
|
||||
"repository-type": [
|
||||
@@ -57,7 +66,14 @@ def main() -> int:
|
||||
|
||||
result = run_console_adapter(args)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 if result["result"] in {"pass", "skipped"} else 1
|
||||
non_error_results = {
|
||||
"pass",
|
||||
"skipped",
|
||||
"warning",
|
||||
"expected_gap",
|
||||
"unsupported_by_design",
|
||||
}
|
||||
return 0 if result["result"] in non_error_results else 1
|
||||
|
||||
|
||||
def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
|
||||
@@ -157,14 +173,16 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
|
||||
]
|
||||
)
|
||||
|
||||
cases = _cases_from_console_output(completed.stdout)
|
||||
if completed.returncode == 0 and not any(case["status"] == "fail" for case in cases):
|
||||
cases = parse_text_report(completed.stdout, args.check_group, group_classes)
|
||||
if cases:
|
||||
status = aggregate_case_result(result_counts(cases), completed.returncode)
|
||||
elif completed.returncode == 0:
|
||||
status = "pass"
|
||||
else:
|
||||
status = "fail"
|
||||
status = "infrastructure_error"
|
||||
return _result(
|
||||
status,
|
||||
[f"OpenCMIS TCK ConsoleRunner exited with {completed.returncode} for {args.check_group}."],
|
||||
_console_observations(completed.returncode, args.check_group, cases),
|
||||
args,
|
||||
group_classes,
|
||||
args.run_dir,
|
||||
@@ -301,26 +319,6 @@ def _maven_command(args: argparse.Namespace, session_path: Path, groups_path: Pa
|
||||
]
|
||||
|
||||
|
||||
def _cases_from_console_output(output: str) -> list[dict[str, str]]:
|
||||
cases = []
|
||||
for line in output.splitlines():
|
||||
stripped = line.strip()
|
||||
upper = stripped.upper()
|
||||
if not stripped:
|
||||
continue
|
||||
if "UNEXPECTED_EXCEPTION" in upper:
|
||||
cases.append({"id": stripped[:120], "status": "infrastructure_error", "message": stripped})
|
||||
elif "FAILURE" in upper:
|
||||
cases.append({"id": stripped[:120], "status": "fail", "message": stripped})
|
||||
elif "WARNING" in upper:
|
||||
cases.append({"id": stripped[:120], "status": "warning", "message": stripped})
|
||||
elif "SKIPPED" in upper:
|
||||
cases.append({"id": stripped[:120], "status": "skipped", "message": stripped})
|
||||
elif "OK" in upper:
|
||||
cases.append({"id": stripped[:120], "status": "pass", "message": stripped})
|
||||
return cases
|
||||
|
||||
|
||||
def _result(
|
||||
status: str,
|
||||
observations: list[str],
|
||||
@@ -329,14 +327,12 @@ def _result(
|
||||
run_dir: Path | None,
|
||||
artifact_dir: Path,
|
||||
artifact_refs: list[str],
|
||||
cases: list[dict[str, str]] | None = None,
|
||||
cases: list[dict[str, Any]] | None = None,
|
||||
returncode: int | None = None,
|
||||
extra_facts: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
cases = cases or []
|
||||
counts: dict[str, int] = {}
|
||||
for case in cases:
|
||||
counts[case["status"]] = counts.get(case["status"], 0) + 1
|
||||
counts = result_counts(cases)
|
||||
if not counts:
|
||||
counts[status] = 1
|
||||
facts = {
|
||||
@@ -361,6 +357,20 @@ def _result(
|
||||
}
|
||||
|
||||
|
||||
def _console_observations(returncode: int, check_group: str, cases: list[dict[str, Any]]) -> list[str]:
|
||||
if cases:
|
||||
counts = result_counts(cases)
|
||||
return [
|
||||
f"OpenCMIS TCK ConsoleRunner exited with {returncode} for {check_group}.",
|
||||
"Normalized OpenCMIS TextReport case statuses: "
|
||||
+ ", ".join(f"{key}: {value}" for key, value in counts.items())
|
||||
+ ".",
|
||||
]
|
||||
return [
|
||||
f"OpenCMIS TCK ConsoleRunner exited with {returncode} for {check_group}, but no TextReport cases were parsed."
|
||||
]
|
||||
|
||||
|
||||
def _artifact_ref(path: Path, run_dir: Path | None, artifact_dir: Path) -> str:
|
||||
resolved = path.resolve()
|
||||
if run_dir is not None:
|
||||
|
||||
77
docs/CMIS-MATURITY-SCORECARD.md
Normal file
77
docs/CMIS-MATURITY-SCORECARD.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# CMIS Capability Maturity Scorecard
|
||||
|
||||
Status: draft
|
||||
Created: 2026-05-08
|
||||
|
||||
## Purpose
|
||||
|
||||
The CMIS scorecard is an interpretation layer over guide-board evidence. It does
|
||||
not replace the raw evidence, findings, mappings, or assessment package. It
|
||||
summarizes how mature the target appears across Browser Binding capability
|
||||
groups.
|
||||
|
||||
It does not certify CMIS conformance.
|
||||
|
||||
## Generate
|
||||
|
||||
After a guide-board run:
|
||||
|
||||
```sh
|
||||
cd /home/worsch/open-cmis-tck
|
||||
PYTHONPATH=src python3 scripts/cmis_scorecard.py \
|
||||
--run-dir /tmp/open-cmis-tck-live
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
```text
|
||||
/tmp/open-cmis-tck-live/reports/cmis-maturity-scorecard.json
|
||||
/tmp/open-cmis-tck-live/reports/cmis-maturity-scorecard.md
|
||||
```
|
||||
|
||||
To preview without writing:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python3 scripts/cmis_scorecard.py \
|
||||
--run-dir /tmp/open-cmis-tck-live \
|
||||
--print
|
||||
```
|
||||
|
||||
## Capability Groups
|
||||
|
||||
The first model scores:
|
||||
|
||||
- repository/type metadata
|
||||
- object/content services
|
||||
- navigation services
|
||||
- query
|
||||
- relationships
|
||||
- ACL/policy
|
||||
- versioning
|
||||
- change log
|
||||
- extensions and known gaps
|
||||
|
||||
## Levels
|
||||
|
||||
Each group receives a 0-4 score:
|
||||
|
||||
- `0`: not assessed or unknown
|
||||
- `1`: blocked, infrastructure-blocked, or unexpectedly failing
|
||||
- `2`: scoped gap, unsupported by design, skipped, manual, or incomplete
|
||||
- `3`: partial, warnings need review
|
||||
- `4`: demonstrated by mapped passing evidence
|
||||
|
||||
The overall score is a weighted percentage across all groups. Core repository
|
||||
and object/content groups carry more weight than optional or gap-review groups.
|
||||
|
||||
## Evidence Boundary
|
||||
|
||||
The scorecard uses:
|
||||
|
||||
- `normalized/evidence.json`
|
||||
- `normalized/findings.json`
|
||||
- `normalized/mappings.json`
|
||||
- `reports/assessment-package.json`
|
||||
|
||||
It keeps conformance evidence separate from maturity interpretation. A formal
|
||||
review should always inspect the underlying evidence and raw artifacts.
|
||||
@@ -61,9 +61,18 @@ Expected dry-run artifacts:
|
||||
|
||||
If preflight fails, fix the target profile or endpoint before continuing.
|
||||
|
||||
Generate a maturity scorecard from the dry-run output:
|
||||
|
||||
```sh
|
||||
cd /home/worsch/open-cmis-tck
|
||||
PYTHONPATH=src python3 scripts/cmis_scorecard.py \
|
||||
--run-dir /tmp/open-cmis-tck-dry-run
|
||||
```
|
||||
|
||||
## Install Java And Maven
|
||||
|
||||
The current WSL environment needs Java and Maven before the real TCK can run:
|
||||
The current WSL environment does not expose system `java` and `mvn` on `PATH`.
|
||||
Either install them as system packages:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
@@ -73,10 +82,33 @@ sudo apt-get install -y openjdk-17-jdk maven
|
||||
Use a managed local Java/Maven installation instead if preferred. The bootstrap
|
||||
only requires `java` and `mvn` on `PATH`.
|
||||
|
||||
Or use the repo-local toolchain under `.local/`:
|
||||
|
||||
```sh
|
||||
cd /home/worsch/open-cmis-tck
|
||||
python3 scripts/install_local_toolchain.py
|
||||
source .local/toolchains/env.sh
|
||||
PYTHONPATH=src python3 scripts/bootstrap_opencmis_tck.py --resolve
|
||||
```
|
||||
|
||||
The local installer downloads a Linux x64 Temurin JDK 17 archive and Apache
|
||||
Maven 3.9.11, extracts them under `.local/toolchains`, verifies Maven's SHA-512
|
||||
checksum, writes `.local/toolchains/env.sh`, and leaves the downloaded binaries
|
||||
outside version control.
|
||||
|
||||
This workspace has already been bootstrapped with the repo-local path. In a new
|
||||
shell, source the environment file before running live TCK commands:
|
||||
|
||||
```sh
|
||||
cd /home/worsch/open-cmis-tck
|
||||
source .local/toolchains/env.sh
|
||||
```
|
||||
|
||||
## Resolve The TCK Runtime
|
||||
|
||||
```sh
|
||||
cd /home/worsch/open-cmis-tck
|
||||
source .local/toolchains/env.sh
|
||||
PYTHONPATH=src python3 scripts/bootstrap_opencmis_tck.py --resolve
|
||||
```
|
||||
|
||||
@@ -100,6 +132,7 @@ After bootstrap reports `ready`, run the baseline assessment:
|
||||
|
||||
```sh
|
||||
cd /home/worsch/guide-board
|
||||
source /home/worsch/open-cmis-tck/.local/toolchains/env.sh
|
||||
PYTHONPATH=src python3 -m guide_board \
|
||||
--extension-dir ../open-cmis-tck \
|
||||
run \
|
||||
@@ -116,6 +149,26 @@ The baseline currently selects:
|
||||
Expand selected check groups only after the repository/type run produces useful
|
||||
output.
|
||||
|
||||
Then generate the maturity scorecard:
|
||||
|
||||
```sh
|
||||
cd /home/worsch/open-cmis-tck
|
||||
PYTHONPATH=src python3 scripts/cmis_scorecard.py \
|
||||
--run-dir /tmp/open-cmis-tck-live
|
||||
```
|
||||
|
||||
Review the real TCK evidence before expanding the scope:
|
||||
|
||||
```text
|
||||
/tmp/open-cmis-tck-live/normalized/evidence.json
|
||||
/tmp/open-cmis-tck-live/artifacts/open-cmis-tck/tck/<check-group>/console-runner-stdout.txt
|
||||
/tmp/open-cmis-tck-live/artifacts/open-cmis-tck/tck/<check-group>/normalized-runner-result.json
|
||||
```
|
||||
|
||||
The normalizer preserves native OpenCMIS statuses (`OK`, `WARNING`, `FAILURE`,
|
||||
`SKIPPED`, `UNEXPECTED_EXCEPTION`, and `INFO`) as case-level facts while mapping
|
||||
the overall check group to guide-board's result vocabulary.
|
||||
|
||||
## Authenticated Targets
|
||||
|
||||
For environment credentials:
|
||||
|
||||
@@ -74,6 +74,17 @@ sudo apt-get install -y openjdk-17-jdk maven
|
||||
Use an already managed Java/Maven installation instead if this workstation has
|
||||
one outside WSL.
|
||||
|
||||
When sudo is not available, use the local toolchain installer:
|
||||
|
||||
```sh
|
||||
python3 scripts/install_local_toolchain.py
|
||||
source .local/toolchains/env.sh
|
||||
```
|
||||
|
||||
This keeps the JDK and Maven under `.local/toolchains`, which is ignored by git.
|
||||
It is intended as a workstation bootstrap convenience, not as a committed
|
||||
runtime artifact.
|
||||
|
||||
## Guide-Board Invocation
|
||||
|
||||
For the full local sequence, see `docs/LOCAL-RUNBOOK.md`.
|
||||
@@ -101,6 +112,11 @@ runtime/opencmis-tck/pom.xml
|
||||
That Maven descriptor pulls the OpenCMIS TCK artifact and runs
|
||||
`ConsoleRunner`.
|
||||
|
||||
The adapter normalizes the native `ConsoleRunner` text report into guide-board
|
||||
case evidence. The retained raw stdout/stderr files remain the audit trail; the
|
||||
normalized result records OpenCMIS result statuses, test names, messages,
|
||||
source locations where present, and per-status counts.
|
||||
|
||||
## Session Parameters
|
||||
|
||||
For Browser Binding runs, the adapter writes OpenCMIS session parameters such
|
||||
|
||||
@@ -97,11 +97,18 @@ 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.
|
||||
3. Native OpenCMIS `TextReport` output written by `ConsoleRunner`.
|
||||
4. 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.
|
||||
The native text normalizer follows Apache Chemistry's `TextReport` structure:
|
||||
group headings, test headings, and result lines whose statuses are `INFO`,
|
||||
`SKIPPED`, `OK`, `WARNING`, `FAILURE`, or `UNEXPECTED_EXCEPTION`. It preserves
|
||||
the OpenCMIS status, test name, group name, source file/line where present,
|
||||
message, duration, selected check group, and raw artifact references.
|
||||
|
||||
This is enough to run the real local ConsoleRunner adapter while retaining the
|
||||
raw logs needed for later pilot-review refinements.
|
||||
|
||||
@@ -14,10 +14,20 @@ import os
|
||||
import shutil
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from open_cmis_tck.normalization import ( # noqa: E402
|
||||
aggregate_case_result,
|
||||
normalize_case_status,
|
||||
parse_text_report,
|
||||
result_counts,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -215,6 +225,25 @@ def _normalize_tck_output(
|
||||
if junit_files:
|
||||
return _normalize_junit_result(junit_files[0], completed.returncode, selected_group)
|
||||
|
||||
text_cases = parse_text_report(completed.stdout, selected_group)
|
||||
if text_cases:
|
||||
counts = result_counts(text_cases)
|
||||
return {
|
||||
"result": aggregate_case_result(counts, completed.returncode),
|
||||
"observations": [
|
||||
f"OpenCMIS TCK group {selected_group!r} produced native TextReport output.",
|
||||
"Normalized OpenCMIS TextReport case statuses: "
|
||||
+ ", ".join(f"{key}: {value}" for key, value in counts.items())
|
||||
+ ".",
|
||||
],
|
||||
"facts": {
|
||||
"normalizer": "opencmis-text-report",
|
||||
"result_counts": counts,
|
||||
"cases": text_cases[:500],
|
||||
},
|
||||
"artifact_refs": [],
|
||||
}
|
||||
|
||||
if completed.returncode == 0:
|
||||
return {
|
||||
"result": "pass",
|
||||
@@ -254,17 +283,11 @@ def _normalize_json_result(
|
||||
counts: dict[str, int] = {}
|
||||
normalized_cases = []
|
||||
for case in cases:
|
||||
status = _normalize_case_status(str(case.get("status", "unknown")))
|
||||
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", ""))),
|
||||
}
|
||||
)
|
||||
normalized_cases.append(_normalize_json_case(case, status))
|
||||
return {
|
||||
"result": _aggregate_result(counts, returncode),
|
||||
"result": aggregate_case_result(counts, returncode),
|
||||
"observations": [
|
||||
f"OpenCMIS TCK group {selected_group!r} produced {sum(counts.values())} normalized case result(s)."
|
||||
],
|
||||
@@ -277,7 +300,7 @@ def _normalize_json_result(
|
||||
"artifact_refs": artifact_refs,
|
||||
}
|
||||
|
||||
result = _normalize_case_status(str(payload.get("result", "unknown")))
|
||||
result = normalize_case_status(str(payload.get("result", "unknown")))
|
||||
if returncode != 0 and result in {"pass", "warning", "skipped"}:
|
||||
result = "infrastructure_error"
|
||||
return {
|
||||
@@ -313,7 +336,7 @@ def _normalize_junit_result(
|
||||
}
|
||||
counts = {key: value for key, value in counts.items() if value}
|
||||
return {
|
||||
"result": _aggregate_result(counts, returncode),
|
||||
"result": aggregate_case_result(counts, returncode),
|
||||
"observations": [
|
||||
f"OpenCMIS TCK group {selected_group!r} produced JUnit-style XML results."
|
||||
],
|
||||
@@ -334,6 +357,30 @@ def _json_cases(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
|
||||
def _normalize_json_case(case: dict[str, Any], status: str) -> dict[str, Any]:
|
||||
normalized = {
|
||||
"id": str(case.get("id", case.get("name", "unnamed"))),
|
||||
"status": status,
|
||||
"message": str(case.get("message", case.get("reason", ""))),
|
||||
}
|
||||
for key in [
|
||||
"status_native",
|
||||
"group_name",
|
||||
"selected_check_group",
|
||||
"test_name",
|
||||
"class_name",
|
||||
"group_class",
|
||||
"group_classes",
|
||||
"duration_ms",
|
||||
"level",
|
||||
"source",
|
||||
"source_location",
|
||||
]:
|
||||
if key in case:
|
||||
normalized[key] = case[key]
|
||||
return normalized
|
||||
|
||||
|
||||
def _artifact_refs_from_payload(payload: dict[str, Any]) -> list[str]:
|
||||
refs = payload.get("artifact_refs", [])
|
||||
if not isinstance(refs, list):
|
||||
@@ -341,50 +388,6 @@ def _artifact_refs_from_payload(payload: dict[str, Any]) -> list[str]:
|
||||
return [ref for ref in refs if isinstance(ref, str) and ref]
|
||||
|
||||
|
||||
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):
|
||||
|
||||
30
scripts/cmis_scorecard.py
Normal file
30
scripts/cmis_scorecard.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a CMIS capability maturity scorecard from a guide-board run."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from open_cmis_tck.scorecard import build_scorecard, write_scorecard
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--run-dir", type=Path, required=True)
|
||||
parser.add_argument("--output-dir", type=Path)
|
||||
parser.add_argument("--print", action="store_true", dest="print_json")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.print_json:
|
||||
print(json.dumps(build_scorecard(args.run_dir), indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
result = write_scorecard(args.run_dir, args.output_dir)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
267
scripts/install_local_toolchain.py
Normal file
267
scripts/install_local_toolchain.py
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install a local Java/Maven toolchain under .local without sudo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_JDK_URL = (
|
||||
"https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/"
|
||||
"jdk/hotspot/normal/eclipse?project=jdk"
|
||||
)
|
||||
DEFAULT_MAVEN_VERSION = "3.9.11"
|
||||
DEFAULT_MAVEN_URL = (
|
||||
"https://archive.apache.org/dist/maven/maven-3/{version}/binaries/"
|
||||
"apache-maven-{version}-bin.tar.gz"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--install-dir", type=Path, default=Path(".local") / "toolchains")
|
||||
parser.add_argument("--jdk-url", default=DEFAULT_JDK_URL)
|
||||
parser.add_argument("--jdk-sha256", default="")
|
||||
parser.add_argument("--maven-version", default=DEFAULT_MAVEN_VERSION)
|
||||
parser.add_argument("--maven-url", default="")
|
||||
parser.add_argument("--skip-jdk", action="store_true")
|
||||
parser.add_argument("--skip-maven", action="store_true")
|
||||
parser.add_argument("--force", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
extension_root = Path(__file__).resolve().parents[1]
|
||||
install_dir = (extension_root / args.install_dir).resolve()
|
||||
summary = install_toolchain(
|
||||
install_dir=install_dir,
|
||||
jdk_url=args.jdk_url,
|
||||
jdk_sha256=args.jdk_sha256 or None,
|
||||
maven_version=args.maven_version,
|
||||
maven_url=args.maven_url or DEFAULT_MAVEN_URL.format(version=args.maven_version),
|
||||
install_jdk=not args.skip_jdk,
|
||||
install_maven=not args.skip_maven,
|
||||
force=args.force,
|
||||
)
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
return 0 if summary["status"] == "ready" else 2
|
||||
|
||||
|
||||
def install_toolchain(
|
||||
install_dir: Path,
|
||||
jdk_url: str,
|
||||
jdk_sha256: str | None,
|
||||
maven_version: str,
|
||||
maven_url: str,
|
||||
install_jdk: bool = True,
|
||||
install_maven: bool = True,
|
||||
force: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
downloads_dir = install_dir / "downloads"
|
||||
jdks_dir = install_dir / "jdks"
|
||||
mavens_dir = install_dir / "mavens"
|
||||
downloads_dir.mkdir(parents=True, exist_ok=True)
|
||||
jdks_dir.mkdir(parents=True, exist_ok=True)
|
||||
mavens_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
jdk_home = _existing_link(install_dir / "current-jdk")
|
||||
maven_home = _existing_link(install_dir / "current-maven")
|
||||
downloads: list[dict[str, Any]] = []
|
||||
|
||||
if install_jdk and (force or jdk_home is None):
|
||||
archive = downloads_dir / "temurin-jdk-17-linux-x64.tar.gz"
|
||||
downloads.append(_download(jdk_url, archive, expected_sha256=jdk_sha256))
|
||||
if force:
|
||||
_remove_link_or_dir(install_dir / "current-jdk")
|
||||
jdk_home = _extract_tool(archive, jdks_dir, "bin/java")
|
||||
_replace_symlink_or_copy(jdk_home, install_dir / "current-jdk")
|
||||
|
||||
if install_maven and (force or maven_home is None):
|
||||
archive = downloads_dir / f"apache-maven-{maven_version}-bin.tar.gz"
|
||||
downloads.append(_download(maven_url, archive))
|
||||
_verify_maven_sha512(maven_url, archive)
|
||||
if force:
|
||||
_remove_link_or_dir(install_dir / "current-maven")
|
||||
maven_home = _extract_tool(archive, mavens_dir, "bin/mvn")
|
||||
_replace_symlink_or_copy(maven_home, install_dir / "current-maven")
|
||||
|
||||
jdk_home = _existing_link(install_dir / "current-jdk")
|
||||
maven_home = _existing_link(install_dir / "current-maven")
|
||||
env_path = install_dir / "env.sh"
|
||||
summary_path = install_dir / "toolchain-summary.json"
|
||||
probes = _probe_tools(jdk_home, maven_home)
|
||||
status = "ready" if probes["java"]["available"] and probes["maven"]["available"] else "blocked"
|
||||
if jdk_home is not None and maven_home is not None:
|
||||
_write_env(env_path, jdk_home, maven_home)
|
||||
|
||||
summary = {
|
||||
"id": "opencmis-local-toolchain",
|
||||
"status": status,
|
||||
"created_at": _now(),
|
||||
"install_dir": str(install_dir),
|
||||
"jdk_home": str(jdk_home) if jdk_home else None,
|
||||
"maven_home": str(maven_home) if maven_home else None,
|
||||
"env_path": str(env_path) if env_path.exists() else None,
|
||||
"summary_path": str(summary_path),
|
||||
"downloads": downloads,
|
||||
"probes": probes,
|
||||
}
|
||||
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return summary
|
||||
|
||||
|
||||
def _download(url: str, destination: Path, expected_sha256: str | None = None) -> dict[str, Any]:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with _open_url(url, timeout=120) as response:
|
||||
with destination.open("wb") as handle:
|
||||
shutil.copyfileobj(response, handle)
|
||||
digest = _sha256(destination)
|
||||
if expected_sha256 and digest.lower() != expected_sha256.lower():
|
||||
raise ValueError(f"SHA256 mismatch for {destination.name}")
|
||||
return {
|
||||
"url": url,
|
||||
"path": str(destination),
|
||||
"bytes": destination.stat().st_size,
|
||||
"sha256": digest,
|
||||
}
|
||||
|
||||
|
||||
def _verify_maven_sha512(url: str, archive: Path) -> None:
|
||||
with _open_url(url + ".sha512", timeout=60) as response:
|
||||
expected = response.read().decode("utf-8").strip().split()[0]
|
||||
actual = hashlib.sha512(archive.read_bytes()).hexdigest()
|
||||
if actual.lower() != expected.lower():
|
||||
raise ValueError(f"SHA512 mismatch for {archive.name}")
|
||||
|
||||
|
||||
def _open_url(url: str, timeout: int):
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
headers={"User-Agent": "open-cmis-tck-local-toolchain/0.1"},
|
||||
)
|
||||
return urllib.request.urlopen(request, timeout=timeout)
|
||||
|
||||
|
||||
def _extract_tool(archive: Path, destination: Path, marker: str) -> Path:
|
||||
before = {path.resolve() for path in destination.iterdir()} if destination.exists() else set()
|
||||
with tarfile.open(archive, "r:gz") as handle:
|
||||
_safe_extract(handle, destination)
|
||||
after = {path.resolve() for path in destination.iterdir()}
|
||||
candidates = sorted(after - before)
|
||||
if not candidates:
|
||||
candidates = sorted(after)
|
||||
for candidate in candidates:
|
||||
if (candidate / marker).exists():
|
||||
return candidate
|
||||
for candidate in sorted(after):
|
||||
if (candidate / marker).exists():
|
||||
return candidate
|
||||
raise ValueError(f"Could not find extracted tool marker {marker!r} from {archive.name}")
|
||||
|
||||
|
||||
def _safe_extract(handle: tarfile.TarFile, destination: Path) -> None:
|
||||
destination = destination.resolve()
|
||||
for member in handle.getmembers():
|
||||
target = (destination / member.name).resolve()
|
||||
try:
|
||||
target.relative_to(destination)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Archive member escapes destination: {member.name}") from exc
|
||||
handle.extractall(destination)
|
||||
|
||||
|
||||
def _replace_symlink_or_copy(source: Path, link: Path) -> None:
|
||||
_remove_link_or_dir(link)
|
||||
try:
|
||||
link.symlink_to(source, target_is_directory=True)
|
||||
except OSError:
|
||||
shutil.copytree(source, link)
|
||||
|
||||
|
||||
def _remove_link_or_dir(path: Path) -> None:
|
||||
if path.is_symlink() or path.is_file():
|
||||
path.unlink()
|
||||
elif path.exists():
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
def _existing_link(path: Path) -> Path | None:
|
||||
if not path.exists():
|
||||
return None
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _probe_tools(jdk_home: Path | None, maven_home: Path | None) -> dict[str, Any]:
|
||||
java = _probe([str(jdk_home / "bin" / "java"), "-version"]) if jdk_home else _missing_probe()
|
||||
env = os.environ.copy()
|
||||
if jdk_home:
|
||||
env["JAVA_HOME"] = str(jdk_home)
|
||||
env["PATH"] = f"{jdk_home / 'bin'}{os.pathsep}{env.get('PATH', '')}"
|
||||
maven = _probe([str(maven_home / "bin" / "mvn"), "-version"], env=env) if maven_home else _missing_probe()
|
||||
return {"java": java, "maven": maven}
|
||||
|
||||
|
||||
def _probe(command: list[str], env: dict[str, str] | None = None) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
output = "\n".join(part.strip() for part in [completed.stdout, completed.stderr] if part.strip())
|
||||
return {
|
||||
"available": completed.returncode == 0,
|
||||
"command": command,
|
||||
"returncode": completed.returncode,
|
||||
"version_output": output[:4000],
|
||||
}
|
||||
|
||||
|
||||
def _missing_probe() -> dict[str, Any]:
|
||||
return {
|
||||
"available": False,
|
||||
"command": None,
|
||||
"returncode": None,
|
||||
"version_output": None,
|
||||
}
|
||||
|
||||
|
||||
def _write_env(path: Path, jdk_home: Path, maven_home: Path) -> None:
|
||||
path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"# Source this file to use the local OpenCMIS TCK toolchain.",
|
||||
f"export JAVA_HOME='{jdk_home}'",
|
||||
f"export MAVEN_HOME='{maven_home}'",
|
||||
'export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH"',
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
247
src/open_cmis_tck/normalization.py
Normal file
247
src/open_cmis_tck/normalization.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""Normalization helpers for Apache Chemistry OpenCMIS TCK output."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
|
||||
OPENCMIS_STATUSES = {
|
||||
"INFO",
|
||||
"SKIPPED",
|
||||
"OK",
|
||||
"WARNING",
|
||||
"FAILURE",
|
||||
"UNEXPECTED_EXCEPTION",
|
||||
}
|
||||
|
||||
_STATUS_PATTERN = re.compile(
|
||||
r"^(?P<indent>\s*)(?P<status>INFO|SKIPPED|OK|WARNING|FAILURE|UNEXPECTED_EXCEPTION):\s*(?P<message>.*)$"
|
||||
)
|
||||
_TEST_HEADER_PATTERN = re.compile(r"^(?P<name>.+?)\s+\((?P<duration>\d+)\s+ms\)$")
|
||||
_PROGRESS_TEST_PATTERN = re.compile(
|
||||
r"^\s{2}(?P<name>.+?)\s+\((?P<duration>\d+)ms\):\s+"
|
||||
r"(?P<status>INFO|SKIPPED|OK|WARNING|FAILURE|UNEXPECTED_EXCEPTION)\s*$"
|
||||
)
|
||||
_PROGRESS_GROUP_PATTERN = re.compile(r"^(?P<name>.+?)\s+\((?P<count>\d+)\s+tests\)$")
|
||||
_SOURCE_LOCATION_PATTERN = re.compile(
|
||||
r"\s+\((?P<file>[A-Za-z0-9_.$-]+\.java):(?P<line>\d+)\)$"
|
||||
)
|
||||
|
||||
|
||||
def parse_text_report(
|
||||
output: str,
|
||||
selected_group: str | None = None,
|
||||
group_classes: list[str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Parse the native OpenCMIS TextReport/ConsoleRunner text output."""
|
||||
|
||||
lines = output.splitlines()
|
||||
cases = _parse_text_report_cases(lines, selected_group, group_classes or [])
|
||||
if cases:
|
||||
return cases
|
||||
return _parse_progress_cases(lines, selected_group, group_classes or [])
|
||||
|
||||
|
||||
def result_counts(cases: list[dict[str, Any]]) -> dict[str, int]:
|
||||
counts = Counter(str(case.get("status", "unknown")) for case in cases)
|
||||
return dict(sorted(counts.items()))
|
||||
|
||||
|
||||
def aggregate_case_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("warning"):
|
||||
return "warning"
|
||||
if counts.get("pass") or counts.get("info"):
|
||||
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"
|
||||
if counts.get("manual"):
|
||||
return "manual"
|
||||
if counts.get("not_applicable"):
|
||||
return "not_applicable"
|
||||
if counts.get("blocked"):
|
||||
return "blocked"
|
||||
return "infrastructure_error" if returncode else "unknown"
|
||||
|
||||
|
||||
def normalize_case_status(value: str) -> str:
|
||||
normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
|
||||
if normalized == "info":
|
||||
return "info"
|
||||
if normalized in {"ok", "success", "passed"}:
|
||||
return "pass"
|
||||
if normalized in {"failure", "failed", "error"}:
|
||||
return "fail"
|
||||
if normalized in {"unexpected_exception", "infra", "infrastructure_error"}:
|
||||
return "infrastructure_error"
|
||||
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 {
|
||||
"pass",
|
||||
"fail",
|
||||
"warning",
|
||||
"manual",
|
||||
"not_applicable",
|
||||
"waiver_applied",
|
||||
"blocked",
|
||||
"unknown",
|
||||
}:
|
||||
return normalized
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _parse_text_report_cases(
|
||||
lines: list[str],
|
||||
selected_group: str | None,
|
||||
group_classes: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
cases: list[dict[str, Any]] = []
|
||||
current_group: str | None = None
|
||||
current_test: str | None = None
|
||||
current_duration_ms: int | None = None
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
if _is_separator(stripped, "=") and i + 2 < len(lines):
|
||||
candidate = lines[i + 1].strip()
|
||||
if candidate and _is_separator(lines[i + 2].strip(), "="):
|
||||
current_group = candidate
|
||||
current_test = None
|
||||
current_duration_ms = None
|
||||
i += 3
|
||||
continue
|
||||
if _is_separator(stripped, "-") and i + 2 < len(lines):
|
||||
match = _TEST_HEADER_PATTERN.match(lines[i + 1].strip())
|
||||
if match and _is_separator(lines[i + 2].strip(), "-"):
|
||||
current_test = match.group("name")
|
||||
current_duration_ms = int(match.group("duration"))
|
||||
i += 3
|
||||
continue
|
||||
|
||||
match = _STATUS_PATTERN.match(line)
|
||||
if match:
|
||||
case = _case_from_match(
|
||||
match,
|
||||
len(cases) + 1,
|
||||
selected_group,
|
||||
group_classes,
|
||||
current_group,
|
||||
current_test,
|
||||
current_duration_ms,
|
||||
)
|
||||
cases.append(case)
|
||||
i += 1
|
||||
return cases
|
||||
|
||||
|
||||
def _parse_progress_cases(
|
||||
lines: list[str],
|
||||
selected_group: str | None,
|
||||
group_classes: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
cases: list[dict[str, Any]] = []
|
||||
current_group: str | None = None
|
||||
for line in lines:
|
||||
group_match = _PROGRESS_GROUP_PATTERN.match(line.strip())
|
||||
if group_match:
|
||||
current_group = group_match.group("name")
|
||||
continue
|
||||
test_match = _PROGRESS_TEST_PATTERN.match(line)
|
||||
if not test_match:
|
||||
continue
|
||||
native_status = test_match.group("status")
|
||||
test_name = test_match.group("name")
|
||||
case_id = _case_id(selected_group, current_group, test_name, len(cases) + 1)
|
||||
cases.append(
|
||||
{
|
||||
"id": case_id,
|
||||
"status": normalize_case_status(native_status),
|
||||
"status_native": native_status,
|
||||
"message": f"{test_name} completed with {native_status}.",
|
||||
"group_name": current_group,
|
||||
"selected_check_group": selected_group,
|
||||
"test_name": test_name,
|
||||
"duration_ms": int(test_match.group("duration")),
|
||||
"level": 0,
|
||||
"group_classes": group_classes,
|
||||
"source": "opencmis-console-progress",
|
||||
}
|
||||
)
|
||||
return cases
|
||||
|
||||
|
||||
def _case_from_match(
|
||||
match: re.Match[str],
|
||||
index: int,
|
||||
selected_group: str | None,
|
||||
group_classes: list[str],
|
||||
current_group: str | None,
|
||||
current_test: str | None,
|
||||
current_duration_ms: int | None,
|
||||
) -> dict[str, Any]:
|
||||
native_status = match.group("status")
|
||||
message = match.group("message").strip()
|
||||
source_location = _source_location(message)
|
||||
if source_location is not None:
|
||||
message = _SOURCE_LOCATION_PATTERN.sub("", message).rstrip()
|
||||
case_id = _case_id(selected_group, current_group, current_test, index)
|
||||
return {
|
||||
"id": case_id,
|
||||
"status": normalize_case_status(native_status),
|
||||
"status_native": native_status,
|
||||
"message": message,
|
||||
"group_name": current_group,
|
||||
"selected_check_group": selected_group,
|
||||
"test_name": current_test,
|
||||
"duration_ms": current_duration_ms,
|
||||
"level": len(match.group("indent")) // 2,
|
||||
"group_classes": group_classes,
|
||||
"source_location": source_location,
|
||||
"source": "opencmis-text-report",
|
||||
}
|
||||
|
||||
|
||||
def _source_location(message: str) -> dict[str, Any] | None:
|
||||
match = _SOURCE_LOCATION_PATTERN.search(message)
|
||||
if not match:
|
||||
return None
|
||||
return {
|
||||
"file": match.group("file"),
|
||||
"line": int(match.group("line")),
|
||||
}
|
||||
|
||||
|
||||
def _case_id(
|
||||
selected_group: str | None,
|
||||
current_group: str | None,
|
||||
current_test: str | None,
|
||||
index: int,
|
||||
) -> str:
|
||||
group = _safe_id(selected_group or current_group or "opencmis")
|
||||
test = _safe_id(current_test or "case")
|
||||
return f"opencmis-tck:{group}:{test}:{index:04d}"
|
||||
|
||||
|
||||
def _safe_id(value: str) -> str:
|
||||
lowered = value.strip().lower()
|
||||
safe = "".join(char if char.isalnum() else "-" for char in lowered)
|
||||
safe = "-".join(part for part in safe.split("-") if part)
|
||||
return safe or "unknown"
|
||||
|
||||
|
||||
def _is_separator(value: str, char: str) -> bool:
|
||||
return len(value) >= 20 and set(value) == {char}
|
||||
333
src/open_cmis_tck/scorecard.py
Normal file
333
src/open_cmis_tck/scorecard.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""CMIS capability maturity scorecard generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
CAPABILITY_GROUPS = [
|
||||
{
|
||||
"id": "repository-type",
|
||||
"label": "Repository And Type Metadata",
|
||||
"weight": 2.0,
|
||||
"description": "Repository identity, repository information, and type metadata.",
|
||||
},
|
||||
{
|
||||
"id": "object-content",
|
||||
"label": "Object And Content Services",
|
||||
"weight": 2.0,
|
||||
"description": "Object services, properties, content streams, and lifecycle operations.",
|
||||
},
|
||||
{
|
||||
"id": "navigation",
|
||||
"label": "Navigation Services",
|
||||
"weight": 1.5,
|
||||
"description": "Folder tree, children, descendants, and filing behavior.",
|
||||
},
|
||||
{
|
||||
"id": "query",
|
||||
"label": "Query",
|
||||
"weight": 1.0,
|
||||
"description": "Query support and query-result behavior.",
|
||||
},
|
||||
{
|
||||
"id": "relationships",
|
||||
"label": "Relationships",
|
||||
"weight": 0.75,
|
||||
"description": "Relationship object and relationship navigation behavior.",
|
||||
},
|
||||
{
|
||||
"id": "acl-policy",
|
||||
"label": "ACL And Policy",
|
||||
"weight": 1.0,
|
||||
"description": "ACL and policy support where claimed by the target.",
|
||||
},
|
||||
{
|
||||
"id": "versioning",
|
||||
"label": "Versioning",
|
||||
"weight": 1.0,
|
||||
"description": "Checkout, checkin, version series, and version-specific behavior.",
|
||||
},
|
||||
{
|
||||
"id": "change-log",
|
||||
"label": "Change Log",
|
||||
"weight": 0.75,
|
||||
"description": "Change token and change event behavior.",
|
||||
},
|
||||
{
|
||||
"id": "extension-gaps",
|
||||
"label": "Extensions And Known Gaps",
|
||||
"weight": 0.5,
|
||||
"description": "Explicitly scoped extensions, unsupported optional services, and gaps.",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def build_scorecard(run_dir: Path) -> dict[str, Any]:
|
||||
run_metadata = _load_json(run_dir / "run.json")
|
||||
evidence = _load_json(run_dir / "normalized" / "evidence.json").get("evidence", [])
|
||||
mappings = _load_json(run_dir / "normalized" / "mappings.json").get("mappings", [])
|
||||
findings = _load_json(run_dir / "normalized" / "findings.json").get("findings", [])
|
||||
assessment_package = _load_json(run_dir / "reports" / "assessment-package.json")
|
||||
|
||||
evidence_by_id = {item["id"]: item for item in evidence}
|
||||
findings_by_check = _findings_by_check(findings)
|
||||
mapping_groups = _mappings_by_group(mappings)
|
||||
target_known_gap_refs = _known_gap_refs(assessment_package)
|
||||
|
||||
groups = [
|
||||
_score_group(group, mapping_groups.get(group["id"], []), evidence_by_id, findings_by_check, target_known_gap_refs)
|
||||
for group in CAPABILITY_GROUPS
|
||||
]
|
||||
assessed_groups = [group for group in groups if group["status"] != "not_assessed"]
|
||||
max_weighted_score = sum(group["weight"] * 4 for group in groups)
|
||||
weighted_score = sum(group["weighted_score"] for group in groups)
|
||||
maturity_score = round((weighted_score / max_weighted_score) * 100, 2) if max_weighted_score else 0.0
|
||||
|
||||
return {
|
||||
"id": f"cmis-maturity-scorecard:{run_metadata['id']}",
|
||||
"run_id": run_metadata["id"],
|
||||
"target_profile_ref": run_metadata["target_profile_ref"],
|
||||
"assessment_profile_ref": run_metadata["assessment_profile_ref"],
|
||||
"created_at": _now(),
|
||||
"summary": {
|
||||
"maturity_score": maturity_score,
|
||||
"maturity_level": _overall_level(maturity_score),
|
||||
"assessed_groups": len(assessed_groups),
|
||||
"total_groups": len(groups),
|
||||
"coverage_percent": round((len(assessed_groups) / len(groups)) * 100, 2),
|
||||
"groups_with_failures": sum(1 for group in groups if group["status"] == "failing"),
|
||||
"groups_blocked": sum(1 for group in groups if group["status"] == "blocked"),
|
||||
"groups_with_expected_gaps": sum(1 for group in groups if group["status"] == "scoped_gap"),
|
||||
},
|
||||
"groups": groups,
|
||||
"certification_boundary": "This scorecard interprets guide-board preparation evidence only and does not certify CMIS conformance.",
|
||||
}
|
||||
|
||||
|
||||
def write_scorecard(run_dir: Path, output_dir: Path | None = None) -> dict[str, str]:
|
||||
output = output_dir or run_dir / "reports"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
scorecard = build_scorecard(run_dir)
|
||||
json_path = output / "cmis-maturity-scorecard.json"
|
||||
markdown_path = output / "cmis-maturity-scorecard.md"
|
||||
json_path.write_text(json.dumps(scorecard, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
markdown_path.write_text(markdown_scorecard(scorecard), encoding="utf-8")
|
||||
return {
|
||||
"status": "written",
|
||||
"json": str(json_path),
|
||||
"markdown": str(markdown_path),
|
||||
}
|
||||
|
||||
|
||||
def markdown_scorecard(scorecard: dict[str, Any]) -> str:
|
||||
summary = scorecard["summary"]
|
||||
lines = [
|
||||
f"# CMIS Capability Maturity Scorecard: {scorecard['run_id']}",
|
||||
"",
|
||||
f"Target: {scorecard['target_profile_ref']}",
|
||||
f"Assessment: {scorecard['assessment_profile_ref']}",
|
||||
f"Maturity score: {summary['maturity_score']} ({summary['maturity_level']})",
|
||||
f"Coverage: {summary['assessed_groups']}/{summary['total_groups']} groups ({summary['coverage_percent']}%)",
|
||||
"",
|
||||
"## Capability Groups",
|
||||
"",
|
||||
]
|
||||
for group in scorecard["groups"]:
|
||||
lines.extend(
|
||||
[
|
||||
f"### {group['label']}",
|
||||
"",
|
||||
f"- status: {group['status']}",
|
||||
f"- maturity level: {group['maturity_level']}",
|
||||
f"- score: {group['score']}/4",
|
||||
f"- evidence results: {_format_counts(group['evidence_results'])}",
|
||||
f"- requirements: {', '.join(group['requirement_refs']) or 'none'}",
|
||||
f"- interpretation: {group['interpretation']}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
lines.extend(["## Boundary", "", scorecard["certification_boundary"], ""])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _score_group(
|
||||
group: dict[str, Any],
|
||||
mappings: list[dict[str, Any]],
|
||||
evidence_by_id: dict[str, dict[str, Any]],
|
||||
findings_by_check: dict[str, list[dict[str, Any]]],
|
||||
target_known_gap_refs: set[str],
|
||||
) -> dict[str, Any]:
|
||||
results = Counter(mapping["result"] for mapping in mappings)
|
||||
requirement_refs = sorted({mapping["requirement_ref"] for mapping in mappings})
|
||||
evidence_refs = sorted({mapping["evidence_id"] for mapping in mappings})
|
||||
check_ids = sorted({mapping["check_id"] for mapping in mappings})
|
||||
findings = [
|
||||
finding
|
||||
for check_id in check_ids
|
||||
for finding in findings_by_check.get(check_id, [])
|
||||
]
|
||||
score, status, level, interpretation = _interpret_group(
|
||||
results,
|
||||
requirement_refs,
|
||||
target_known_gap_refs,
|
||||
findings,
|
||||
)
|
||||
return {
|
||||
"id": group["id"],
|
||||
"label": group["label"],
|
||||
"description": group["description"],
|
||||
"weight": group["weight"],
|
||||
"status": status,
|
||||
"maturity_level": level,
|
||||
"score": score,
|
||||
"weighted_score": round(score * group["weight"], 3),
|
||||
"evidence_results": dict(sorted(results.items())),
|
||||
"requirement_refs": requirement_refs,
|
||||
"evidence_refs": evidence_refs,
|
||||
"check_ids": check_ids,
|
||||
"finding_refs": sorted({finding["id"] for finding in findings}),
|
||||
"artifact_refs": sorted(
|
||||
{
|
||||
artifact_ref
|
||||
for evidence_ref in evidence_refs
|
||||
for artifact_ref in evidence_by_id.get(evidence_ref, {}).get("artifact_refs", [])
|
||||
}
|
||||
),
|
||||
"interpretation": interpretation,
|
||||
}
|
||||
|
||||
|
||||
def _interpret_group(
|
||||
results: Counter[str],
|
||||
requirement_refs: list[str],
|
||||
known_gap_refs: set[str],
|
||||
findings: list[dict[str, Any]],
|
||||
) -> tuple[int, str, str, str]:
|
||||
if not results:
|
||||
return (
|
||||
0,
|
||||
"not_assessed",
|
||||
"not_assessed",
|
||||
"No mapped evidence has been produced for this capability group yet.",
|
||||
)
|
||||
unexpected_findings = [finding for finding in findings if not finding.get("expected")]
|
||||
if results.get("infrastructure_error"):
|
||||
return (
|
||||
1,
|
||||
"blocked",
|
||||
"infrastructure_blocked",
|
||||
"The capability group could not be assessed because the test infrastructure or target endpoint failed.",
|
||||
)
|
||||
if results.get("blocked"):
|
||||
return (
|
||||
1,
|
||||
"blocked",
|
||||
"blocked",
|
||||
"The capability group is blocked by prerequisite, preflight, dependency, or invocation setup.",
|
||||
)
|
||||
if unexpected_findings or results.get("fail"):
|
||||
return (
|
||||
1,
|
||||
"failing",
|
||||
"fails_claimed_capability",
|
||||
"One or more mapped checks failed unexpectedly for this capability group.",
|
||||
)
|
||||
if results.get("warning"):
|
||||
return (
|
||||
3,
|
||||
"partial",
|
||||
"partially_demonstrated",
|
||||
"The capability group produced warnings and needs review before it can be treated as stable.",
|
||||
)
|
||||
if results.get("expected_gap") or results.get("unsupported_by_design"):
|
||||
expected_refs = sorted(set(requirement_refs).intersection(known_gap_refs))
|
||||
detail = (
|
||||
" Known gap refs: " + ", ".join(expected_refs) + "."
|
||||
if expected_refs
|
||||
else ""
|
||||
)
|
||||
return (
|
||||
2,
|
||||
"scoped_gap",
|
||||
"scoped_or_unsupported",
|
||||
"The capability group is explicitly scoped as unsupported or partially supported." + detail,
|
||||
)
|
||||
if results.get("manual") or results.get("skipped") or results.get("not_applicable"):
|
||||
return (
|
||||
2,
|
||||
"not_automated",
|
||||
"evidence_incomplete",
|
||||
"Evidence exists, but the capability group was not executed as an automated pass/fail check.",
|
||||
)
|
||||
if results.get("pass"):
|
||||
return (
|
||||
4,
|
||||
"demonstrated",
|
||||
"demonstrated",
|
||||
"Mapped checks passed for this capability group.",
|
||||
)
|
||||
return (
|
||||
0,
|
||||
"unknown",
|
||||
"unknown",
|
||||
"Evidence exists, but its result vocabulary was not recognized by the scorecard.",
|
||||
)
|
||||
|
||||
|
||||
def _mappings_by_group(mappings: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
||||
groups: dict[str, list[dict[str, Any]]] = {}
|
||||
for mapping in mappings:
|
||||
if mapping.get("target_type") != "capability_group":
|
||||
continue
|
||||
groups.setdefault(mapping["target_id"], []).append(mapping)
|
||||
return groups
|
||||
|
||||
|
||||
def _findings_by_check(findings: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
||||
by_check: dict[str, list[dict[str, Any]]] = {}
|
||||
for finding in findings:
|
||||
by_check.setdefault(finding["check_id"], []).append(finding)
|
||||
return by_check
|
||||
|
||||
|
||||
def _known_gap_refs(assessment_package: dict[str, Any]) -> set[str]:
|
||||
target = assessment_package.get("target", {})
|
||||
refs = set()
|
||||
for gap in target.get("known_gaps", []):
|
||||
refs.update(gap.get("requirement_refs", []))
|
||||
return refs
|
||||
|
||||
|
||||
def _overall_level(score: float) -> str:
|
||||
if score >= 85:
|
||||
return "strong"
|
||||
if score >= 65:
|
||||
return "developing"
|
||||
if score >= 35:
|
||||
return "limited"
|
||||
if score > 0:
|
||||
return "initial"
|
||||
return "not_assessed"
|
||||
|
||||
|
||||
def _format_counts(counts: dict[str, int]) -> str:
|
||||
if not counts:
|
||||
return "none"
|
||||
return ", ".join(f"{key}: {value}" for key, value in sorted(counts.items()))
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
value = json.load(handle)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} must contain a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
49
tests/fixtures/opencmis-text-report-sanitized.txt
vendored
Normal file
49
tests/fixtures/opencmis-text-report-sanitized.txt
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
Basics Test Group (3 tests)
|
||||
Repository Info Test (12ms): WARNING
|
||||
Types Test (7ms): OK
|
||||
Query Smoke Test (4ms): SKIPPED
|
||||
|
||||
************************************************************
|
||||
Test Report: Fri May 08 10:15:00 UTC 2026
|
||||
************************************************************
|
||||
org.apache.chemistry.opencmis.binding.spi.type = browser
|
||||
org.apache.chemistry.opencmis.binding.browser.url = http://127.0.0.1/cmis/browser
|
||||
org.apache.chemistry.opencmis.session.repository.id = compat-tck
|
||||
************************************************************
|
||||
============================================================
|
||||
Basics Test Group
|
||||
============================================================
|
||||
------------------------------------------------------------
|
||||
Repository Info Test (12 ms)
|
||||
------------------------------------------------------------
|
||||
|
||||
OK: Repository ID: compat-tck (RepositoryInfoTest.java:59)
|
||||
|
||||
WARNING: HTTPS is not used. Credentials might be transferred as plain text! (SecurityTest.java:52)
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
Types Test (7 ms)
|
||||
------------------------------------------------------------
|
||||
|
||||
OK: Base type definitions exposed. (BaseTypesTest.java:75)
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
Query Smoke Test (4 ms)
|
||||
------------------------------------------------------------
|
||||
|
||||
SKIPPED: Query not supported. Test Skipped! (QuerySmokeTest.java:127)
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
Create And Delete Document Test (9 ms)
|
||||
------------------------------------------------------------
|
||||
|
||||
FAILURE: Test folder could not be created. (CreateAndDeleteDocumentTest.java:87)
|
||||
|
||||
UNEXPECTED_EXCEPTION: Repository connection dropped during cleanup. (AsyncCreateAndDeleteDocumentTest.java:90)
|
||||
|
||||
Stacktrace:
|
||||
|
||||
org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException: sanitized
|
||||
@@ -22,7 +22,13 @@ from guide_board.planning import (
|
||||
from guide_board.retention import build_trend_summary
|
||||
from guide_board.service import ServiceHandle, start_service
|
||||
from open_cmis_tck.bootstrap import TCK_COORDINATE, check_runtime
|
||||
from open_cmis_tck.normalization import (
|
||||
aggregate_case_result,
|
||||
parse_text_report,
|
||||
result_counts,
|
||||
)
|
||||
from open_cmis_tck.profile import validate_cmis_profile_config
|
||||
from open_cmis_tck.scorecard import build_scorecard, write_scorecard
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -118,6 +124,60 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(output.exists())
|
||||
|
||||
def test_bootstrap_ready_path_with_fake_local_toolchain(self) -> None:
|
||||
with TemporaryDirectory() as temporary_directory:
|
||||
temp_root = Path(temporary_directory)
|
||||
bin_dir = temp_root / "bin"
|
||||
bin_dir.mkdir()
|
||||
java = bin_dir / "java"
|
||||
maven = bin_dir / "mvn"
|
||||
java.write_text("#!/usr/bin/env sh\necho 'openjdk version \"17\"' >&2\n", encoding="utf-8")
|
||||
maven.write_text("#!/usr/bin/env sh\necho 'Apache Maven 3.9.0'\n", encoding="utf-8")
|
||||
java.chmod(0o755)
|
||||
maven.chmod(0o755)
|
||||
output = temp_root / "runtime-summary.json"
|
||||
original_path = os.environ.get("PATH", "")
|
||||
os.environ["PATH"] = f"{bin_dir}{os.pathsep}{original_path}"
|
||||
try:
|
||||
summary = check_runtime(ROOT, output, resolve=False)
|
||||
finally:
|
||||
os.environ["PATH"] = original_path
|
||||
|
||||
self.assertEqual(summary["status"], "ready")
|
||||
self.assertTrue(summary["runtime"]["java"]["available"])
|
||||
self.assertTrue(summary["runtime"]["maven"]["available"])
|
||||
self.assertTrue(output.exists())
|
||||
|
||||
def test_parses_native_opencmis_text_report_fixture(self) -> None:
|
||||
fixture = (ROOT / "tests" / "fixtures" / "opencmis-text-report-sanitized.txt").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
cases = parse_text_report(
|
||||
fixture,
|
||||
"repository-type",
|
||||
["org.apache.chemistry.opencmis.tck.tests.basics.BasicsTestGroup"],
|
||||
)
|
||||
counts = result_counts(cases)
|
||||
warning = next(case for case in cases if case["status"] == "warning")
|
||||
failure = next(case for case in cases if case["status"] == "fail")
|
||||
|
||||
self.assertEqual(
|
||||
counts,
|
||||
{
|
||||
"fail": 1,
|
||||
"infrastructure_error": 1,
|
||||
"pass": 2,
|
||||
"skipped": 1,
|
||||
"warning": 1,
|
||||
},
|
||||
)
|
||||
self.assertEqual(aggregate_case_result(counts, 0), "infrastructure_error")
|
||||
self.assertEqual(warning["status_native"], "WARNING")
|
||||
self.assertEqual(warning["test_name"], "Repository Info Test")
|
||||
self.assertEqual(warning["source_location"], {"file": "SecurityTest.java", "line": 52})
|
||||
self.assertEqual(failure["message"], "Test folder could not be created.")
|
||||
|
||||
def test_console_adapter_dry_run_writes_session_and_group_files(self) -> None:
|
||||
with TemporaryDirectory() as temporary_directory:
|
||||
temp_root = Path(temporary_directory)
|
||||
@@ -469,6 +529,89 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
|
||||
self.assertEqual(retention["summary"]["status"], "completed")
|
||||
self.assertGreaterEqual(retention["summary"]["artifact_count"], 4)
|
||||
self.assertEqual(trend["run_count"], 1)
|
||||
|
||||
scorecard = build_scorecard(run_dir)
|
||||
self.assertEqual(scorecard["run_id"], result["run_id"])
|
||||
groups = {group["id"]: group for group in scorecard["groups"]}
|
||||
self.assertEqual(groups["repository-type"]["status"], "demonstrated")
|
||||
self.assertEqual(groups["repository-type"]["score"], 4)
|
||||
self.assertEqual(groups["object-content"]["status"], "not_assessed")
|
||||
written = write_scorecard(run_dir)
|
||||
self.assertTrue(Path(written["json"]).exists())
|
||||
self.assertTrue(Path(written["markdown"]).exists())
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=5)
|
||||
server.server_close()
|
||||
|
||||
def test_runs_configured_tck_command_and_normalizes_text_report_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_text.py"
|
||||
fixture = ROOT / "tests" / "fixtures" / "opencmis-text-report-sanitized.txt"
|
||||
fake_tck.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"from pathlib import Path",
|
||||
f"print(Path({str(fixture)!r}).read_text(encoding='utf-8'))",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
_write_target(target_path, server.server_port, "local-cmis-text-tck")
|
||||
_write_assessment(
|
||||
assessment_path,
|
||||
"local-cmis-text-tck",
|
||||
"local-cmis-text-tck",
|
||||
["repository-type"],
|
||||
None,
|
||||
{
|
||||
"requires_java_maven": False,
|
||||
"repository_id": "local-test-repository",
|
||||
"command": [sys.executable, str(fake_tck)],
|
||||
},
|
||||
)
|
||||
|
||||
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"]
|
||||
cases = evidence[1]["facts"]["cases"]
|
||||
|
||||
self.assertEqual(result["status"], "infrastructure_error")
|
||||
self.assertEqual(evidence[1]["result"], "infrastructure_error")
|
||||
self.assertEqual(evidence[1]["facts"]["normalizer"], "opencmis-text-report")
|
||||
self.assertEqual(
|
||||
evidence[1]["facts"]["result_counts"],
|
||||
{
|
||||
"fail": 1,
|
||||
"infrastructure_error": 1,
|
||||
"pass": 2,
|
||||
"skipped": 1,
|
||||
"warning": 1,
|
||||
},
|
||||
)
|
||||
self.assertEqual(cases[0]["status_native"], "OK")
|
||||
self.assertEqual(cases[0]["group_name"], "Basics Test Group")
|
||||
self.assertEqual(cases[0]["test_name"], "Repository Info Test")
|
||||
self.assertIn(
|
||||
"artifacts/open-cmis-tck/tck/repository-type/stdout.log",
|
||||
evidence[1]["artifact_refs"],
|
||||
)
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=5)
|
||||
|
||||
@@ -10,7 +10,7 @@ owner: codex
|
||||
planning_priority: high
|
||||
planning_order: 3
|
||||
created: "2026-05-07"
|
||||
updated: "2026-05-07"
|
||||
updated: "2026-05-08"
|
||||
depends_on:
|
||||
- "OPEN-CMIS-TCK-WP-0001"
|
||||
state_hub_workstream_id: "da3f0d16-ba8e-4147-b0fc-ab3462e0b7b0"
|
||||
@@ -104,7 +104,7 @@ Progress:
|
||||
|
||||
```task
|
||||
id: OPEN-CMIS-TCK-WP-0002-T002
|
||||
status: in_progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f993c1ef-8e6f-4ad1-8375-4487887deb8b"
|
||||
```
|
||||
@@ -128,6 +128,16 @@ Progress:
|
||||
- Added `docs/LOCAL-RUNBOOK.md` with the local sequence from dry-run adapter
|
||||
check through Java/Maven install, Maven dependency resolution, and first real
|
||||
guide-board run.
|
||||
- Added regression coverage for both the blocked local posture and a ready path
|
||||
using a fake Java/Maven toolchain, so the command behavior is stable while the
|
||||
actual workstation prerequisites remain to be installed.
|
||||
- Added `scripts/install_local_toolchain.py` as a no-sudo path that installs a
|
||||
local JDK/Maven toolchain under `.local/toolchains`, writes `env.sh`, and
|
||||
keeps downloaded runtime assets out of version control.
|
||||
- Ran the local installer on this WSL workspace. It installed Temurin JDK
|
||||
17.0.19 and Apache Maven 3.9.11 under `.local/toolchains`.
|
||||
- Ran `scripts/bootstrap_opencmis_tck.py --resolve` with the local toolchain;
|
||||
the runtime summary reports `ready` and Maven dependency resolution succeeded.
|
||||
|
||||
## D2.3 - OpenCMIS TCK Adapter Invocation
|
||||
|
||||
@@ -161,8 +171,14 @@ Progress:
|
||||
artifact capture without requiring Java/Maven.
|
||||
- The guide-board dry-run captures redacted session properties and group lists
|
||||
as fingerprinted assessment artifacts.
|
||||
- Live execution remains blocked until Java/Maven are installed and Maven can
|
||||
resolve the TCK runtime.
|
||||
- Local Java/Maven are now installed under `.local/toolchains`, and Maven has
|
||||
resolved the OpenCMIS TCK runtime.
|
||||
- Ran a non-dry ConsoleRunner adapter smoke against an intentionally unreachable
|
||||
local URL. Maven launched the real TCK runner, captured session properties,
|
||||
group list, invocation metadata, stdout, and stderr, and correctly returned
|
||||
`infrastructure_error` because no CMIS target was reachable.
|
||||
- Live repository/type execution remains open until a reachable CMIS Browser
|
||||
Binding target is available.
|
||||
|
||||
## D2.4 - Target Profiles And Credential References
|
||||
|
||||
@@ -198,7 +214,7 @@ Progress:
|
||||
|
||||
```task
|
||||
id: OPEN-CMIS-TCK-WP-0002-T005
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "03ba9506-cce8-44fa-a036-2ab1ffc7a176"
|
||||
```
|
||||
@@ -214,11 +230,24 @@ Acceptance:
|
||||
- Add fixtures derived from real sanitized output so normalization remains
|
||||
regression-tested.
|
||||
|
||||
Progress:
|
||||
|
||||
- Added `open_cmis_tck.normalization` as the shared OpenCMIS result parser.
|
||||
- The ConsoleRunner adapter now parses Apache Chemistry `TextReport` stdout and
|
||||
emits case-level evidence with native status, guide-board status, group/test
|
||||
names, source location, selected check group, and group class facts.
|
||||
- The guide-board wrapper now accepts native TextReport output directly in
|
||||
addition to adapter JSON and JUnit-style XML.
|
||||
- Added a sanitized TextReport-format fixture and regression tests for direct
|
||||
parsing and guide-board run normalization.
|
||||
- Documented the TextReport normalization path in the runner and local runtime
|
||||
docs.
|
||||
|
||||
## D2.6 - Live Pilot Run
|
||||
|
||||
```task
|
||||
id: OPEN-CMIS-TCK-WP-0002-T006
|
||||
status: todo
|
||||
status: in_progress
|
||||
priority: high
|
||||
state_hub_task_id: "d9eb9384-3352-4b71-9918-57282ee00411"
|
||||
```
|
||||
@@ -233,11 +262,24 @@ Acceptance:
|
||||
blocked by infrastructure, or were unsupported by design.
|
||||
- Known gaps do not hide unexpected failures in the same capability group.
|
||||
|
||||
Progress:
|
||||
|
||||
- Local Java/Maven/TCK runtime is ready via `.local/toolchains`.
|
||||
- Probed the configured `kontextual-cmis-compat` Browser Binding URL
|
||||
`http://127.0.0.1:8000/cmis/compat-tck/browser`; it currently returns HTTP
|
||||
404, so the CMIS target is not reachable at the profile URL.
|
||||
- Ran a guide-board baseline attempt into
|
||||
`.local/runs/open-cmis-tck-live-attempt`; it produced a run directory,
|
||||
assessment package, Markdown report, retention summary, and CMIS maturity
|
||||
scorecard with status `infrastructure_error` at preflight.
|
||||
- Live pilot execution is blocked on starting or providing a reachable CMIS
|
||||
Browser Binding endpoint and, if needed, credentials.
|
||||
|
||||
## D2.7 - CMIS Capability Maturity Scorecard
|
||||
|
||||
```task
|
||||
id: OPEN-CMIS-TCK-WP-0002-T007
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "7365052f-0d76-4fb2-b32c-236476b0f937"
|
||||
```
|
||||
@@ -251,6 +293,14 @@ Acceptance:
|
||||
- Generate a compact JSON scorecard and Markdown summary suitable for downstream
|
||||
product-quality tracking.
|
||||
|
||||
Progress:
|
||||
|
||||
- Added `open_cmis_tck.scorecard` and `scripts/cmis_scorecard.py`.
|
||||
- The scorecard reads guide-board evidence, findings, mappings, and assessment
|
||||
package outputs and writes JSON/Markdown reports.
|
||||
- Added `docs/CMIS-MATURITY-SCORECARD.md` and linked scorecard generation from
|
||||
the local runbook.
|
||||
|
||||
## D2.8 - Operator Documentation
|
||||
|
||||
```task
|
||||
|
||||
Reference in New Issue
Block a user