generated from coulomb/repo-seed
requirement refs map to capability groups
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" } },
|
||||
|
||||
38
docs/schemas/mapping-set.schema.json
Normal file
38
docs/schemas/mapping-set.schema.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
extensions/open-cmis-tck/mappings/cmis-capability-map.json
Normal file
65
extensions/open-cmis-tck/mappings/cmis-capability-map.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"preflight_runner": null,
|
||||
"runner_entrypoints": [],
|
||||
"normalizers": [],
|
||||
"mappings": [],
|
||||
"mappings": [
|
||||
"sample-readiness-map"
|
||||
],
|
||||
"report_fragments": [],
|
||||
"dependencies": [],
|
||||
"restricted_assets": [],
|
||||
|
||||
16
extensions/sample-noop/mappings/sample-readiness-map.json
Normal file
16
extensions/sample-noop/mappings/sample-readiness-map.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
103
src/guide_board/mapping.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user