Skills › Scientific Agent Skills › iso-standards-readiness › audit_document_records.py
audit_document_records.py
148 lines · skills/iso-standards-readiness/scripts/audit_document_records.py at 5f2bc03
1#!/usr/bin/env python3
2"""Audit local document/record register metadata without opening evidence files."""
4from __future__ import annotations
6from typing import Any
8from _common import (
9 Review,
10 finish,
11 guarded_main,
12 load_json,
13 require_root_object,
14 standard_parser,
15)
17DOCUMENT_STATUSES = {"draft", "in-review", "effective", "obsolete"}
18RECORD_STATUSES = {"active", "archived", "disposed"}
19EXTERNAL_STATUSES = {"current", "review-due", "superseded"}
22def _core(
23 review: Review,
24 item: dict[str, Any],
25 path: str,
26 *,
27 require_approved: bool,
28) -> None:
29 review.text(item, "owner", path, max_chars=200)
30 review.evidence(item, path)
31 review.approval(item, path, require_approved=require_approved)
32 review.source_refs(item, path)
35def validate(data: dict[str, Any]) -> tuple[Review, dict[str, int]]:
36 review = Review()
38 metadata = review.object(data.get("metadata"), "metadata")
39 if metadata is not None:
40 review.controlled_item(metadata, "metadata", require_approved=True)
41 review.text(metadata, "register_id", "metadata", max_chars=120)
42 review.date(metadata, "review_date", "metadata")
44 documents = review.list(data.get("documents"), "documents", min_items=1) or []
45 document_ids: set[str] = set()
46 for index, value in enumerate(documents):
47 path = f"documents[{index}]"
48 item = review.object(value, path)
49 if item is None:
50 continue
51 identifier = review.text(item, "id", path, max_chars=120)
52 if identifier:
53 document_ids.add(identifier)
54 review.text(item, "title", path, max_chars=300)
55 review.text(item, "document_type", path, max_chars=120)
56 review.text(item, "revision", path, max_chars=80)
57 status = review.choice(item, "status", DOCUMENT_STATUSES, path)
58 review.text(item, "change_summary", path, max_chars=2_000)
59 review.text(item, "training_impact", path, max_chars=1_000)
60 _core(review, item, path, require_approved=status == "effective")
61 if status == "effective":
62 review.date(item, "effective_date", path)
63 if status == "obsolete":
64 review.text(item, "disposition", path, max_chars=500)
65 review.date(item, "obsolete_date", path)
66 review.unique_ids(documents, "documents")
68 for index, value in enumerate(documents):
69 if not isinstance(value, dict):
70 continue
71 supersedes = value.get("supersedes")
72 if supersedes in (None, ""):
73 continue
74 if not isinstance(supersedes, str) or supersedes not in document_ids:
75 review.add(
76 "REFERENCE_MISSING",
77 f"documents[{index}].supersedes",
78 "must reference another document id in this register",
79 )
81 records = review.list(data.get("records"), "records", min_items=1) or []
82 for index, value in enumerate(records):
83 path = f"records[{index}]"
84 item = review.object(value, path)
85 if item is None:
86 continue
87 review.text(item, "id", path, max_chars=120)
88 review.text(item, "record_type", path, max_chars=200)
89 status = review.choice(item, "status", RECORD_STATUSES, path)
90 review.text(item, "retention_period", path, max_chars=300)
91 review.text(item, "retention_basis", path, max_chars=1_000)
92 review.text(item, "storage_and_integrity_controls", path, max_chars=2_000)
93 review.text(item, "retrieval_method", path, max_chars=1_000)
94 review.text(item, "disposition_method", path, max_chars=1_000)
95 _core(review, item, path, require_approved=True)
96 if status == "disposed":
97 review.date(item, "disposition_date", path)
98 review.text(item, "disposition_authorized_by", path, max_chars=200)
99 review.unique_ids(records, "records")
101 external = (
102 review.list(
103 data.get("external_sources"),
104 "external_sources",
105 min_items=1,
106 )
107 or []
108 )
109 for index, value in enumerate(external):
110 path = f"external_sources[{index}]"
111 item = review.object(value, path)
112 if item is None:
113 continue
114 review.text(item, "id", path, max_chars=120)
115 review.text(item, "title", path, max_chars=300)
116 review.text(item, "publisher", path, max_chars=200)
117 review.text(item, "version_or_date", path, max_chars=120)
118 status = review.choice(item, "status", EXTERNAL_STATUSES, path)
119 review.date(item, "last_currency_review", path)
120 _core(review, item, path, require_approved=status == "current")
121 if status != "current":
122 review.add(
123 "SOURCE_NOT_CURRENT",
124 f"{path}.status",
125 "source requires documented impact assessment or replacement",
126 )
127 review.unique_ids(external, "external_sources")
129 return review, {
130 "documents": len(documents),
131 "external_sources": len(external),
132 "records": len(records),
133 }
136def main() -> int:
137 parser = standard_parser(
138 "Audit a bounded local document, record, and external-source register.",
139 "Path to the local document-register JSON file",
140 )
141 args = parser.parse_args()
142 data = require_root_object(load_json(args.input))
143 review, metrics = validate(data)
144 return finish("audit_document_records", review, args, metrics=metrics)
147if __name__ == "__main__":
148 guarded_main(main)