requirement refs map to capability groups

This commit is contained in:
2026-05-07 13:46:17 +02:00
parent 0b90004a6e
commit 5a6091fd2a
12 changed files with 331 additions and 5 deletions

View File

@@ -363,6 +363,10 @@ Maps evidence to:
Mappings belong to extensions or assessment packs, not the core.
The first implementation loads extension-owned JSON mapping sets from
`extensions/<extension-id>/mappings/`, joins them to evidence `requirement_refs`,
and writes normalized mapping records under each run directory.
### Expectation And Waiver Engine
Applies declared target posture after evidence normalization.

View File

@@ -51,6 +51,7 @@ The key runtime fields are:
- `check_groups`: named groups that assessment profiles can select.
- `preflight_runner`: optional runner ID used before selected check groups.
- `runner_entrypoints`: concrete runner declarations.
- `mappings`: mapping set IDs under `mappings/<mapping-id>.json`.
- `certification_boundary`: explicit statement of what the extension does not
certify.
@@ -102,6 +103,30 @@ Command placeholders:
The command is executed with the extension directory as its working directory.
The core does not use a shell for command runners.
## Mapping Sets
Mapping sets connect normalized evidence requirement refs to capability groups,
controls, conformance classes, quality dimensions, or other assessment targets.
Each mapping set lives under:
```text
extensions/<extension-id>/mappings/<mapping-id>.json
```
and validates against:
```text
docs/schemas/mapping-set.schema.json
```
The core does not embed domain policy. It only joins evidence `requirement_refs`
to extension-owned mappings and writes normalized mapping records to:
```text
runs/<run-id>/normalized/mappings.json
```
## Python Runner Contract
A Python runner receives one context object and returns one result object.
@@ -167,6 +192,6 @@ Initial statuses:
## Next SDK Steps
- Add normalizer and mapping plug-in contracts.
- Add normalizer plug-in contracts.
- Add extension-owned schema validation for domain-specific target profile
fields.

View File

@@ -11,6 +11,7 @@
"extensions",
"source_lock",
"summary",
"mapping_summary",
"findings",
"evidence_refs",
"artifact_manifest",
@@ -26,6 +27,7 @@
"extensions": { "type": "array", "items": { "type": "object" } },
"source_lock": { "type": "object" },
"summary": { "type": "object" },
"mapping_summary": { "type": "object" },
"findings": { "type": "array", "items": { "type": "object" } },
"evidence_refs": { "type": "array", "items": { "type": "string" } },
"artifact_manifest": { "type": "array", "items": { "type": "object" } },

View File

@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Guide Board Mapping Set",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"extension_id",
"framework_refs",
"mappings"
],
"properties": {
"id": { "type": "string" },
"extension_id": { "type": "string" },
"framework_refs": { "type": "array", "items": { "type": "string" } },
"mappings": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"requirement_ref",
"target_type",
"target_id",
"label",
"description"
],
"properties": {
"requirement_ref": { "type": "string" },
"target_type": { "type": "string" },
"target_id": { "type": "string" },
"label": { "type": "string" },
"description": { "type": "string" }
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
{
"id": "cmis-capability-map",
"extension_id": "open-cmis-tck",
"framework_refs": [
"cmis.browser-binding.compatibility.v1"
],
"mappings": [
{
"requirement_ref": "cmis.repository-info",
"target_type": "capability_group",
"target_id": "repository-type",
"label": "Repository And Type Metadata",
"description": "Repository identity, repository information, and supported CMIS metadata posture."
},
{
"requirement_ref": "cmis.type-definitions",
"target_type": "capability_group",
"target_id": "repository-type",
"label": "Repository And Type Metadata",
"description": "Type definition exposure and type metadata consistency."
},
{
"requirement_ref": "cmis.object-services",
"target_type": "capability_group",
"target_id": "object-content",
"label": "Object And Content Services",
"description": "Object service behavior for folders, documents, properties, and object lifecycle operations."
},
{
"requirement_ref": "cmis.content-streams",
"target_type": "capability_group",
"target_id": "object-content",
"label": "Object And Content Services",
"description": "Content stream creation, retrieval, update, and deletion behavior."
},
{
"requirement_ref": "cmis.navigation-services",
"target_type": "capability_group",
"target_id": "navigation",
"label": "Navigation Services",
"description": "Folder tree, descendants, children, checked-out documents, and relationship navigation behavior."
},
{
"requirement_ref": "cmis.query",
"target_type": "capability_group",
"target_id": "query",
"label": "Query",
"description": "CMIS query support, query capability flags, and query result behavior."
},
{
"requirement_ref": "cmis.acl",
"target_type": "capability_group",
"target_id": "acl-policy",
"label": "ACL And Policy",
"description": "Access-control list and policy behavior where supported by the target profile."
},
{
"requirement_ref": "cmis.versioning",
"target_type": "capability_group",
"target_id": "versioning",
"label": "Versioning",
"description": "Checkout, checkin, version series, and version-specific object behavior."
}
]
}

View File

@@ -157,7 +157,7 @@ Acceptance:
```task
id: OPEN-CMIS-TCK-WP-0001-T006
status: todo
status: in_progress
priority: high
state_hub_task_id: "9f7dacc5-4d19-4755-aa9a-8572d4285514"
```
@@ -171,6 +171,14 @@ Acceptance:
- Markdown reports summarize pass/fail/skip counts and unexpected gaps.
- Known gaps do not hide unexpected failures in the same capability area.
Progress:
- `cmis-capability-map` maps initial CMIS requirement refs to repository/type,
object/content, navigation, query, ACL/policy, and versioning capability
groups.
- Guide-board writes normalized mapping records and includes capability-group
counts in Markdown reports.
## D1.7 - Optional Local Service API Adapter
```task

View File

@@ -28,7 +28,9 @@
"preflight_runner": null,
"runner_entrypoints": [],
"normalizers": [],
"mappings": [],
"mappings": [
"sample-readiness-map"
],
"report_fragments": [],
"dependencies": [],
"restricted_assets": [],

View File

@@ -0,0 +1,16 @@
{
"id": "sample-readiness-map",
"extension_id": "sample-noop",
"framework_refs": [
"guide-board.sample-readiness.v0"
],
"mappings": [
{
"requirement_ref": "guide-board.sample-readiness.v0.profile-shape",
"target_type": "quality_dimension",
"target_id": "profile-readiness",
"label": "Profile Readiness",
"description": "The sample target and assessment profiles can be discovered, validated, and planned."
}
]
}

View File

@@ -9,6 +9,7 @@ from typing import Any
from guide_board.artifacts import build_artifact_manifest
from guide_board.io import write_json
from guide_board.mapping import build_mapping_records, summarize_mappings
from guide_board.planning import build_run_plan
from guide_board.runners import run_step
from guide_board.schema import assert_valid
@@ -37,6 +38,8 @@ def run_assessment(
assert_valid(finding, "finding")
artifact_manifest = build_artifact_manifest(run_dir, run_id, evidence)
mapping_records = build_mapping_records(root, run_id, plan, evidence)
mapping_summary = summarize_mappings(mapping_records)
assessment_package = _assessment_package(
run_id,
@@ -44,6 +47,7 @@ def run_assessment(
evidence,
findings,
artifact_manifest,
mapping_summary,
created_at,
)
assert_valid(assessment_package, "assessment-package")
@@ -57,7 +61,15 @@ def run_assessment(
"assessment_profile_ref": plan["assessment_profile_snapshot"]["id"],
}
_write_run_directory(run_dir, run_metadata, plan, evidence, findings, assessment_package)
_write_run_directory(
run_dir,
run_metadata,
plan,
evidence,
findings,
mapping_records,
assessment_package,
)
return {
"status": run_metadata["status"],
"run_id": run_id,
@@ -175,6 +187,7 @@ def _assessment_package(
evidence: list[dict[str, Any]],
findings: list[dict[str, Any]],
artifact_manifest: list[dict[str, Any]],
mapping_summary: dict[str, Any],
created_at: str,
) -> dict[str, Any]:
summary = dict(Counter(item["result"] for item in evidence))
@@ -188,6 +201,7 @@ def _assessment_package(
"extensions": plan["extension_snapshots"],
"source_lock": plan["source_lock"],
"summary": summary,
"mapping_summary": mapping_summary,
"findings": findings,
"evidence_refs": [item["id"] for item in evidence],
"artifact_manifest": artifact_manifest,
@@ -203,6 +217,7 @@ def _write_run_directory(
plan: dict[str, Any],
evidence: list[dict[str, Any]],
findings: list[dict[str, Any]],
mapping_records: list[dict[str, Any]],
assessment_package: dict[str, Any],
) -> None:
write_json(run_dir / "run.json", run_metadata)
@@ -215,7 +230,7 @@ def _write_run_directory(
)
write_json(run_dir / "normalized" / "evidence.json", {"evidence": evidence})
write_json(run_dir / "normalized" / "findings.json", {"findings": findings})
write_json(run_dir / "normalized" / "mappings.json", {"mappings": []})
write_json(run_dir / "normalized" / "mappings.json", {"mappings": mapping_records})
write_json(run_dir / "reports" / "assessment-package.json", assessment_package)
(run_dir / "reports").mkdir(parents=True, exist_ok=True)
(run_dir / "reports" / "report.md").write_text(
@@ -230,6 +245,7 @@ def _markdown_report(run_metadata: dict[str, Any], package: dict[str, Any]) -> s
)
if not summary_lines:
summary_lines = "- no evidence produced"
mapping_lines = _mapping_summary_lines(package)
return "\n".join(
[
@@ -243,6 +259,10 @@ def _markdown_report(run_metadata: dict[str, Any], package: dict[str, Any]) -> s
"",
summary_lines,
"",
"## Mappings",
"",
mapping_lines,
"",
"## Boundary",
"",
package["certification_boundary"],
@@ -251,6 +271,20 @@ def _markdown_report(run_metadata: dict[str, Any], package: dict[str, Any]) -> s
)
def _mapping_summary_lines(package: dict[str, Any]) -> str:
targets = package.get("mapping_summary", {}).get("targets", [])
if not targets:
return "- no mapped evidence"
lines = []
for target in targets:
results = ", ".join(
f"{status}: {count}"
for status, count in sorted(target.get("results", {}).items())
)
lines.append(f"- {target['label']} ({target['target_id']}): {results}")
return "\n".join(lines)
def _run_status(evidence: list[dict[str, Any]]) -> str:
if any(item["result"] == "fail" for item in evidence):
return "failed"

103
src/guide_board/mapping.py Normal file
View File

@@ -0,0 +1,103 @@
"""Evidence-to-capability/control mapping."""
from __future__ import annotations
from collections import defaultdict
from pathlib import Path
from typing import Any
from guide_board.io import load_json
from guide_board.schema import assert_valid
def build_mapping_records(
root: Path,
run_id: str,
plan: dict[str, Any],
evidence: list[dict[str, Any]],
) -> list[dict[str, Any]]:
index = _mapping_index(root, plan)
records: list[dict[str, Any]] = []
for item in evidence:
extension_id = item["extension_id"]
for requirement_ref in item.get("requirement_refs", []):
mappings = index.get((extension_id, requirement_ref), [])
for mapping in mappings:
records.append(
{
"id": _record_id(item["id"], mapping),
"run_id": run_id,
"evidence_id": item["id"],
"check_id": item["check_id"],
"extension_id": extension_id,
"requirement_ref": requirement_ref,
"result": item["result"],
"target_type": mapping["target_type"],
"target_id": mapping["target_id"],
"label": mapping["label"],
"description": mapping["description"],
}
)
return records
def summarize_mappings(mapping_records: list[dict[str, Any]]) -> dict[str, Any]:
targets: dict[tuple[str, str], dict[str, Any]] = {}
for record in mapping_records:
key = (record["target_type"], record["target_id"])
if key not in targets:
targets[key] = {
"target_type": record["target_type"],
"target_id": record["target_id"],
"label": record["label"],
"results": {},
"requirement_refs": [],
}
target = targets[key]
target["results"][record["result"]] = target["results"].get(record["result"], 0) + 1
if record["requirement_ref"] not in target["requirement_refs"]:
target["requirement_refs"].append(record["requirement_ref"])
return {
"targets": sorted(
targets.values(),
key=lambda item: (item["target_type"], item["target_id"]),
)
}
def _mapping_index(
root: Path,
plan: dict[str, Any],
) -> dict[tuple[str, str], list[dict[str, Any]]]:
by_requirement: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
for extension in plan["extension_snapshots"]:
extension_path = root / extension["path"]
manifest = load_json(extension_path / "extension.json")
for mapping_id in manifest.get("mappings", []):
mapping_path = extension_path / "mappings" / f"{mapping_id}.json"
if not mapping_path.exists():
continue
mapping_set = load_json(mapping_path)
assert_valid(mapping_set, "mapping-set")
for mapping in mapping_set["mappings"]:
by_requirement[
(mapping_set["extension_id"], mapping["requirement_ref"])
].append(mapping)
return by_requirement
def _record_id(evidence_id: str, mapping: dict[str, Any]) -> str:
return "mapping:" + _safe_id(
":".join(
[
evidence_id,
mapping["requirement_ref"],
mapping["target_type"],
mapping["target_id"],
]
)
)
def _safe_id(value: str) -> str:
return "".join(char if char.isalnum() or char in {"-", "_"} else "_" for char in value)

View File

@@ -81,6 +81,11 @@ class CoreArchitectureTests(unittest.TestCase):
self.assertTrue((run_dir / "normalized" / "evidence.json").exists())
self.assertTrue((run_dir / "reports" / "assessment-package.json").exists())
self.assertTrue((run_dir / "reports" / "report.md").exists())
mappings = json.loads(
(run_dir / "normalized" / "mappings.json").read_text(encoding="utf-8")
)["mappings"]
self.assertEqual(len(mappings), 1)
self.assertEqual(mappings[0]["target_id"], "profile-readiness")
def test_runs_cmis_preflight_against_local_endpoint(self) -> None:
server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
@@ -158,6 +163,11 @@ class CoreArchitectureTests(unittest.TestCase):
encoding="utf-8"
)
)
mappings = json.loads(
(Path(result["run_dir"]) / "normalized" / "mappings.json").read_text(
encoding="utf-8"
)
)["mappings"]
self.assertEqual(result["status"], "completed")
self.assertEqual(evidence["evidence"][0]["result"], "pass")
@@ -173,6 +183,7 @@ class CoreArchitectureTests(unittest.TestCase):
["local-test-repository"],
)
self.assertEqual(len(package["artifact_manifest"]), 2)
self.assertEqual(mappings, [])
self.assertTrue(
(
Path(result["run_dir"])
@@ -270,6 +281,11 @@ class CoreArchitectureTests(unittest.TestCase):
encoding="utf-8"
)
)
mappings = json.loads(
(Path(result["run_dir"]) / "normalized" / "mappings.json").read_text(
encoding="utf-8"
)
)["mappings"]
self.assertEqual(result["status"], "blocked")
self.assertEqual(evidence[0]["result"], "pass")
@@ -284,6 +300,15 @@ class CoreArchitectureTests(unittest.TestCase):
evidence[1]["facts"]["blocked_reason"],
)
self.assertGreaterEqual(len(package["artifact_manifest"]), 3)
self.assertEqual(len(mappings), 2)
self.assertEqual(
{mapping["target_id"] for mapping in mappings},
{"repository-type"},
)
self.assertEqual(
package["mapping_summary"]["targets"][0]["results"],
{"blocked": 2},
)
finally:
server.shutdown()
thread.join(timeout=5)

View File

@@ -208,6 +208,8 @@ Acceptance:
normalized evidence through the same runner result contract.
- Runner artifact refs are constrained to the run directory and fingerprinted in
the assessment package artifact manifest.
- Extension-owned mapping sets connect evidence requirement refs to capability,
control, conformance, or quality targets.
## D1.8 - CMIS Seed Extension Integration
@@ -233,6 +235,8 @@ Progress:
- The OpenCMIS Java/Maven TCK wrapper executes through the command runner bridge
and currently reports dependency or configuration blockers as structured
evidence.
- CMIS requirement refs map to extension-owned capability groups in
`normalized/mappings.json` and the Markdown report.
## D1.9 - Containerized Execution Design