Skill Tracer
Skills › Scientific Agent Skills › iso-standards-readiness › check_traceability.py

check_traceability.py

171 lines · skills/iso-standards-readiness/scripts/check_traceability.py at 5f2bc03

1#!/usr/bin/env python3
2"""Check risk/design/production/postmarket traceability references."""
3
4from __future__ import annotations
5
6from typing import Any
7
8from _catalog import TRACEABILITY_LINKS
9from _common import (
10 ALLOWED_STATUSES,
11 Review,
12 finish,
13 guarded_main,
14 load_json,
15 require_root_object,
16 standard_parser,
19ARTIFACT_TYPES = {
20 "intended-use",
21 "hazard",
22 "postmarket-signal",
23 "risk-evaluation",
24 "risk-control",
25 "design-input",
26 "design-output",
27 "verification",
28 "validation",
29 "production-control",
30 "change-control",
31 "capa",
32 "not-applicable-decision",
33 "other",
37def _reference_list(
38 review: Review,
39 value: Any,
40 path: str,
41 artifact_ids: set[str],
42) -> list[str]:
43 raw = review.list(value, path, min_items=1)
44 if raw is None:
45 return []
46 result: list[str] = []
47 for index, item in enumerate(raw):
48 item_path = f"{path}[{index}]"
49 if not isinstance(item, str) or not item.strip():
50 review.add("REFERENCE_REQUIRED", item_path, "requires an artifact id")
51 continue
52 result.append(item)
53 if item not in artifact_ids:
54 review.add(
55 "REFERENCE_MISSING",
56 item_path,
57 f"artifact id is not defined: {item}",
58 )
59 if len(result) != len(set(result)):
60 review.add("REFERENCE_DUPLICATE", path, "contains duplicate artifact ids")
61 return result
64def validate(data: dict[str, Any]) -> tuple[Review, dict[str, int]]:
65 review = Review()
66 metadata = review.object(data.get("metadata"), "metadata")
67 if metadata is not None:
68 review.controlled_item(metadata, "metadata", require_approved=True)
69 review.text(metadata, "matrix_id", "metadata", max_chars=120)
70 review.date(metadata, "review_date", "metadata")
72 artifacts = review.list(data.get("artifacts"), "artifacts", min_items=1) or []
73 artifact_ids: set[str] = set()
74 for index, value in enumerate(artifacts):
75 path = f"artifacts[{index}]"
76 artifact = review.object(value, path)
77 if artifact is None:
78 continue
79 identifier = review.text(artifact, "id", path, max_chars=120)
80 if identifier:
81 artifact_ids.add(identifier)
82 review.choice(artifact, "type", ARTIFACT_TYPES, path)
83 review.text(artifact, "title", path, max_chars=300)
84 review.text(artifact, "owner", path, max_chars=200)
85 status = review.choice(artifact, "status", ALLOWED_STATUSES, path)
86 review.evidence(artifact, path)
87 review.approval(
88 artifact,
89 path,
90 require_approved=status in {"approved", "implemented", "verified", "closed"},
91 )
92 review.source_refs(artifact, path)
93 if artifact.get("type") == "not-applicable-decision":
94 review.text(artifact, "rationale", path, max_chars=2_000)
95 review.approval(artifact, path, require_approved=True)
96 review.unique_ids(artifacts, "artifacts")
98 rows = review.list(data.get("rows"), "rows", min_items=1) or []
99 verified_rows = 0
100 for index, value in enumerate(rows):
101 path = f"rows[{index}]"
102 row = review.object(value, path)
103 if row is None:
104 continue
105 review.text(row, "id", path, max_chars=120)
106 review.text(row, "product_id", path, max_chars=120)
107 review.text(row, "owner", path, max_chars=200)
108 status = review.choice(row, "status", ALLOWED_STATUSES, path)
109 review.text(row, "linkage_rationale", path, max_chars=3_000)
110 review.evidence(row, path)
111 review.source_refs(row, path)
112 review.approval(row, path, require_approved=status == "verified")
114 links_path = f"{path}.links"
115 links = review.object(row.get("links"), links_path)
116 if links is not None:
117 for link_name in TRACEABILITY_LINKS:
118 _reference_list(
119 review,
120 links.get(link_name),
121 f"{links_path}.{link_name}",
122 artifact_ids,
123 )
125 change_refs = row.get("change_control_refs")
126 if change_refs in (None, []):
127 review.add(
128 "CHANGE_LINK_REQUIRED",
129 f"{path}.change_control_refs",
130 "requires a change-control artifact or approved not-applicable decision",
131 )
132 else:
133 _reference_list(
134 review,
135 change_refs,
136 f"{path}.change_control_refs",
137 artifact_ids,
138 )
140 gaps = review.list(row.get("open_gaps"), f"{path}.open_gaps", min_items=0)
141 if status == "verified":
142 verified_rows += 1
143 if gaps:
144 review.add(
145 "VERIFICATION_BLOCKED",
146 f"{path}.open_gaps",
147 "verified row must not contain open gaps",
148 "blocker",
149 )
150 review.unique_ids(rows, "rows")
152 return review, {
153 "artifacts": len(artifacts),
154 "rows": len(rows),
155 "verified_rows": verified_rows,
156 }
159def main() -> int:
160 parser = standard_parser(
161 "Check explicit traceability from risks and design through postmarket evidence.",
162 "Path to the local traceability-matrix JSON file",
163 )
164 args = parser.parse_args()
165 data = require_root_object(load_json(args.input))
166 review, metrics = validate(data)
167 return finish("check_traceability", review, args, metrics=metrics)
170if __name__ == "__main__":
171 guarded_main(main)