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

validate_scope_intake.py

209 lines · skills/iso-standards-readiness/scripts/validate_scope_intake.py at 5f2bc03

1#!/usr/bin/env python3
2"""Validate a declared applicability and scope intake for one standard profile.
3
4The script checks whether accountable humans documented decisions and evidence. It
5does not decide whether a law, regulation, standard, conformity route, or
6accreditation scheme applies.
7"""
8
9from __future__ import annotations
11from typing import Any
13from _catalog import StandardProfile
14from _common import (
15 Review,
16 finish,
17 guarded_main,
18 load_json,
19 profile_for,
20 require_root_object,
21 standard_parser,
24APPLICABILITY = {"applicable", "not-applicable", "undetermined"}
27def _text_list(
28 review: Review,
29 value: Any,
30 path: str,
31 *,
32 allowed: set[str] | None = None,
33 min_items: int = 1,
34) -> list[str]:
35 raw = review.list(value, path, min_items=min_items)
36 if raw is None:
37 return []
38 result: list[str] = []
39 for index, item in enumerate(raw):
40 item_path = f"{path}[{index}]"
41 if not isinstance(item, str) or not item.strip():
42 review.add("TEXT_REQUIRED", item_path, "requires non-empty text")
43 continue
44 if allowed is not None and item not in allowed:
45 review.add(
46 "VALUE_UNKNOWN",
47 item_path,
48 f"must be one of: {', '.join(sorted(allowed))}",
49 )
50 continue
51 result.append(item)
52 if len(result) != len(set(result)):
53 review.add("VALUE_DUPLICATE", path, "must not contain duplicate values")
54 return result
57def validate(
58 data: dict[str, Any],
59 profile: StandardProfile,
60) -> tuple[Review, dict[str, int]]:
61 review = Review()
62 activities = set(profile.scope_activities)
64 metadata = review.object(data.get("metadata"), "metadata")
65 if metadata is not None:
66 review.controlled_item(metadata, "metadata", require_approved=True)
67 review.text(metadata, "intake_id", "metadata", max_chars=120)
68 review.date(metadata, "review_date", "metadata")
70 organization = review.object(data.get("organization"), "organization")
71 if organization is not None:
72 review.text(organization, "legal_name", "organization", max_chars=300)
73 review.text(
74 organization,
75 "declared_lifecycle_role",
76 "organization",
77 max_chars=300,
78 )
79 review.text(
80 organization,
81 "authorized_management_representative",
82 "organization",
83 max_chars=200,
84 )
85 review.text(organization, "raqa_owner", "organization", max_chars=200)
86 review.text(
87 organization,
88 "applicability_decision_owner",
89 "organization",
90 max_chars=200,
91 )
93 scope = review.object(data.get("scope"), "scope")
94 sites: list[Any] = []
95 scope_items: list[Any] = []
96 markets: list[Any] = []
97 if scope is not None:
98 _text_list(
99 review,
100 scope.get(profile.activities_section),
101 f"scope.{profile.activities_section}",
102 allowed=activities,
103 )
105 sites = review.list(scope.get("sites"), "scope.sites", min_items=1) or []
106 for index, item in enumerate(sites):
107 path = f"scope.sites[{index}]"
108 site = review.object(item, path)
109 if site is None:
110 continue
111 review.text(site, "id", path, max_chars=120)
112 review.text(site, "name", path, max_chars=300)
113 review.text(site, "address", path, max_chars=500)
114 _text_list(
115 review,
116 site.get("activities"),
117 f"{path}.activities",
118 allowed=activities,
119 )
120 review.controlled_item(site, path, require_approved=True)
121 review.unique_ids(sites, "scope.sites")
123 section = profile.scope_item_section
124 scope_items = (
125 review.list(scope.get(section), f"scope.{section}", min_items=1) or []
126 )
127 for index, item in enumerate(scope_items):
128 path = f"scope.{section}[{index}]"
129 scope_item = review.object(item, path)
130 if scope_item is None:
131 continue
132 review.text(scope_item, "id", path, max_chars=120)
133 for field, max_chars in profile.scope_item_fields:
134 review.text(scope_item, field, path, max_chars=max_chars)
135 _text_list(review, scope_item.get("market_ids"), f"{path}.market_ids")
136 review.controlled_item(scope_item, path, require_approved=True)
137 review.unique_ids(scope_items, f"scope.{section}")
139 markets = review.list(scope.get("markets"), "scope.markets", min_items=1) or []
140 for index, item in enumerate(markets):
141 path = f"scope.markets[{index}]"
142 market = review.object(item, path)
143 if market is None:
144 continue
145 review.text(market, "id", path, max_chars=120)
146 review.text(market, "jurisdiction", path, max_chars=300)
147 decision = review.choice(
148 market,
149 "applicability",
150 APPLICABILITY,
151 path,
152 )
153 if decision == "undetermined":
154 review.add(
155 "HUMAN_DECISION_REQUIRED",
156 f"{path}.applicability",
157 "authorized RA/QA or legal review must resolve applicability",
158 "blocker",
159 )
160 review.text(market, "decision_rationale", path, max_chars=2_000)
161 review.controlled_item(market, path, require_approved=True)
162 review.unique_ids(markets, "scope.markets")
164 outsourced = review.list(
165 scope.get("outsourced_processes"),
166 "scope.outsourced_processes",
167 min_items=0,
168 )
169 if outsourced is not None:
170 for index, item in enumerate(outsourced):
171 path = f"scope.outsourced_processes[{index}]"
172 process = review.object(item, path)
173 if process is None:
174 continue
175 review.text(process, "id", path, max_chars=120)
176 review.text(process, "process", path, max_chars=300)
177 review.text(process, "provider", path, max_chars=300)
178 review.text(process, "control_strategy", path, max_chars=2_000)
179 review.controlled_item(process, path, require_approved=True)
180 review.unique_ids(outsourced, "scope.outsourced_processes")
182 return review, {
183 "markets": len(markets),
184 profile.scope_item_section: len(scope_items),
185 "sites": len(sites),
186 }
189def main() -> int:
190 parser = standard_parser(
191 "Validate a local scope/applicability intake without deciding applicability.",
192 "Path to the local scope-intake JSON file",
193 with_standard=True,
194 )
195 args = parser.parse_args()
196 profile = profile_for(args)
197 data = require_root_object(load_json(args.input))
198 review, metrics = validate(data, profile)
199 return finish(
200 "validate_scope_intake",
201 review,
202 args,
203 metrics=metrics,
204 standard=profile.key,
205 )
208if __name__ == "__main__":
209 guarded_main(main)