aero-agent-roles 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +17 -3
- package/package.json +1 -1
- package/roles/software-product-assurance-engineer/ROLE.md +243 -0
- package/roles/software-product-assurance-engineer/SOURCES.md +40 -0
- package/roles/software-product-assurance-engineer/cli.py +542 -0
- package/roles/software-product-assurance-engineer/core/software_product_assurance_core.py +2108 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-AUDIT-2026-01.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-CSTD-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-DSA-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-DSTD-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-RB-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SCMP-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SDP-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SPAP-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SPAR-2026-Q3.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SPR-LOG.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SRF-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SVALP-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-SVERP-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/docs/EX-TS-001.md +7 -0
- package/roles/software-product-assurance-engineer/example/evidence.csv +251 -0
- package/roles/software-product-assurance-engineer/example/metrics.csv +11 -0
- package/roles/software-product-assurance-engineer/example/pdr-pack.csv +17 -0
- package/roles/software-product-assurance-engineer/templates/q80-compliance-matrix-template.md +572 -0
- package/roles/software-product-assurance-engineer/tests/test_role_software_product_assurance_engineer.py +202 -0
- package/roles/software-product-assurance-engineer/tests/test_software_product_assurance_cli.py +271 -0
- package/roles/software-product-assurance-engineer/tests/test_software_product_assurance_core.py +570 -0
|
@@ -0,0 +1,2108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""software_product_assurance_core.py - the engine of the Software Product
|
|
3
|
+
Assurance Engineer role.
|
|
4
|
+
|
|
5
|
+
Turns a space project's software product assurance evidence into a
|
|
6
|
+
clause-by-clause compliance matrix against ECSS-Q-ST-80C Rev.2 (30 April
|
|
7
|
+
2025), and stops at a human sign-off. Standalone: stdlib only, no Aero
|
|
8
|
+
Agent Skills checkout needed. When the skills library is present the role
|
|
9
|
+
CLI dispatches the bound q80 leaves and cross-checks this engine against
|
|
10
|
+
them (a second, independent implementation of the same rules).
|
|
11
|
+
|
|
12
|
+
What the engine does
|
|
13
|
+
--------------------
|
|
14
|
+
1. Software criticality category: taken as given (A to D) or derived from
|
|
15
|
+
the highest severity of the functions the software serves (I to IV)
|
|
16
|
+
and the compensating provisions at system level.
|
|
17
|
+
2. Tailoring: every requirement of the standard resolved for the category
|
|
18
|
+
(applicable, reduced, not applicable; the security clauses follow the
|
|
19
|
+
security sensitivity instead of the category).
|
|
20
|
+
3. Compliance matrix: the clause list joined to the evidence index
|
|
21
|
+
(clause, document, section, status, justification). One row per
|
|
22
|
+
clause; several evidence rows combine to the weakest status; silence
|
|
23
|
+
is never compliance; a claim with no document reference is downgraded;
|
|
24
|
+
a not-applicable claim the tailoring keeps is a gap.
|
|
25
|
+
4. Document trace: every document the evidence cites is looked up in the
|
|
26
|
+
project's document folder; a citation that resolves to no file is a
|
|
27
|
+
gap, and a compliant claim that rests only on missing files is
|
|
28
|
+
downgraded, so every clause traces to a real file.
|
|
29
|
+
5. Milestone evidence: the documents a review (SRR, PDR, CDR, QR, AR...)
|
|
30
|
+
owes for the category and scope, each with the clauses that drive it
|
|
31
|
+
and the state of their evidence; a submitted review pack graded for
|
|
32
|
+
missing and immature items; the SPAP maturity owed; the milestone
|
|
33
|
+
report (SPAMR) skeleton with the sections the evidence can fill.
|
|
34
|
+
6. Product metrics: a measurement set graded against the category
|
|
35
|
+
thresholds (illustrative defaults; the project's agreed values win).
|
|
36
|
+
7. Gates: the deliverable is checked for its DRAFT status, the stop line,
|
|
37
|
+
one row per clause, a coverage summary that matches the rows, and an
|
|
38
|
+
honest sign-off state. The engine never marks a matrix approved.
|
|
39
|
+
|
|
40
|
+
The standard is referenced, never reproduced: clause identifiers are
|
|
41
|
+
factual identifiers, and every topic label and rule below is our own
|
|
42
|
+
paraphrase. Author: ashfordeOU.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import csv
|
|
47
|
+
import io
|
|
48
|
+
import os
|
|
49
|
+
import re
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
|
|
53
|
+
STANDARD = "ECSS-Q-ST-80C Rev.2 (30 April 2025)"
|
|
54
|
+
TITLE = ("Software Product Assurance Compliance Matrix "
|
|
55
|
+
"(ECSS-Q-ST-80C Rev.2)")
|
|
56
|
+
|
|
57
|
+
CATEGORIES = ("A", "B", "C", "D")
|
|
58
|
+
SEVERITIES = ("I", "II", "III", "IV")
|
|
59
|
+
STATUSES = ("compliant", "partially-compliant", "not-compliant",
|
|
60
|
+
"not-applicable")
|
|
61
|
+
REVIEWS = ("srr", "pdr", "cdr", "trr", "qr", "ar", "orr")
|
|
62
|
+
REVIEW_NAMES = {
|
|
63
|
+
"srr": "system requirements review (SRR)",
|
|
64
|
+
"pdr": "preliminary design review (PDR)",
|
|
65
|
+
"cdr": "critical design review (CDR)",
|
|
66
|
+
"trr": "test readiness review (TRR)",
|
|
67
|
+
"qr": "qualification review (QR)",
|
|
68
|
+
"ar": "acceptance review (AR)",
|
|
69
|
+
"orr": "operational readiness review (ORR)",
|
|
70
|
+
}
|
|
71
|
+
SCOPE_FLAGS = ("suppliers", "procured", "reuse", "security", "operations")
|
|
72
|
+
PROVISION_KINDS = ("hardware", "software", "operational-procedure")
|
|
73
|
+
|
|
74
|
+
DRAFT_BANNER = ("DRAFT - prepared by an agent for review. Not a statement of "
|
|
75
|
+
"compliance until signed off by the responsible human.")
|
|
76
|
+
STOP_LINE = "STOP: human sign-off required before submission."
|
|
77
|
+
NOT_SIGNED = "Sign-off: none recorded."
|
|
78
|
+
|
|
79
|
+
# Sign-off block a HUMAN appends to the Markdown matrix (never the engine).
|
|
80
|
+
SIGN_OFF_KEYS = ("Signed-off-by", "Sign-off-role", "Sign-off-date",
|
|
81
|
+
"Sign-off-decision", "Accepted-open-gaps")
|
|
82
|
+
|
|
83
|
+
STATUS_ALIASES = {
|
|
84
|
+
"c": "compliant", "compliant": "compliant", "yes": "compliant",
|
|
85
|
+
"y": "compliant", "full": "compliant", "fc": "compliant",
|
|
86
|
+
"pc": "partially-compliant", "partial": "partially-compliant",
|
|
87
|
+
"partially": "partially-compliant",
|
|
88
|
+
"partially compliant": "partially-compliant",
|
|
89
|
+
"partially-compliant": "partially-compliant",
|
|
90
|
+
"nc": "not-compliant", "no": "not-compliant", "n": "not-compliant",
|
|
91
|
+
"not compliant": "not-compliant", "not-compliant": "not-compliant",
|
|
92
|
+
"non-compliant": "not-compliant", "noncompliant": "not-compliant",
|
|
93
|
+
"na": "not-applicable", "n/a": "not-applicable",
|
|
94
|
+
"not applicable": "not-applicable", "not-applicable": "not-applicable",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_SEVERITY_ALIASES = {
|
|
98
|
+
"1": "I", "2": "II", "3": "III", "4": "IV",
|
|
99
|
+
"i": "I", "ii": "II", "iii": "III", "iv": "IV",
|
|
100
|
+
"catastrophic": "I", "critical": "II", "major": "III",
|
|
101
|
+
"minor": "IV", "negligible": "IV",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_PROVISION_ALIASES = {
|
|
105
|
+
"hw": "hardware", "hardware": "hardware",
|
|
106
|
+
"sw": "software", "software": "software",
|
|
107
|
+
"procedure": "operational-procedure",
|
|
108
|
+
"operational": "operational-procedure",
|
|
109
|
+
"operational-procedure": "operational-procedure",
|
|
110
|
+
"ops-procedure": "operational-procedure",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_REVIEW_ALIASES = {
|
|
114
|
+
"system-requirements-review": "srr",
|
|
115
|
+
"preliminary-design-review": "pdr",
|
|
116
|
+
"critical-design-review": "cdr",
|
|
117
|
+
"test-readiness-review": "trr",
|
|
118
|
+
"qualification-review": "qr",
|
|
119
|
+
"acceptance-review": "ar",
|
|
120
|
+
"operational-readiness-review": "orr",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Plan (SPAP) maturity owed at each review (Annex B practice, paraphrased).
|
|
124
|
+
PLAN_MATURITY_BY_REVIEW = {
|
|
125
|
+
"srr": "issued", "pdr": "updated", "cdr": "updated",
|
|
126
|
+
"qr": "maintained", "ar": "maintained", "orr": "maintained",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_MATURITY = {"draft": 0, "issued": 1, "approved": 2}
|
|
130
|
+
|
|
131
|
+
# Which clause groups feed each input of the milestone report skeleton.
|
|
132
|
+
# A SPAMR input counts as available when at least one applicable clause of
|
|
133
|
+
# its groups carries evidence that resolves to a real document.
|
|
134
|
+
SPAMR_INPUT_CLAUSES = {
|
|
135
|
+
"verification_activities": ("6.2.6",),
|
|
136
|
+
"methods_tools": ("5.6",),
|
|
137
|
+
"standards_adherence": ("6.3.3", "6.3.4"),
|
|
138
|
+
"metrics": ("7.1", "6.2.5"),
|
|
139
|
+
"testing": ("6.3.5",),
|
|
140
|
+
"problems": ("5.2.5", "5.2.6"),
|
|
141
|
+
"progress_reports": ("5.2.2",),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Where a gap goes next: the bound skill that resolves it.
|
|
145
|
+
_LEAF = "space-systems/ecss/"
|
|
146
|
+
GAP_ROUTES_BY_KIND = {
|
|
147
|
+
"na-conflicts-with-tailoring": _LEAF + "q80-software-criticality-tailoring",
|
|
148
|
+
"evidence-for-tailored-out-clause": _LEAF + "q80-software-criticality-tailoring",
|
|
149
|
+
"evidence-for-unlisted-clause": _LEAF + "q80-compliance-matrix",
|
|
150
|
+
"conflicting-status": _LEAF + "q80-compliance-matrix",
|
|
151
|
+
}
|
|
152
|
+
GAP_ROUTES_BY_CLAUSE = (
|
|
153
|
+
("7.1.", _LEAF + "q80-software-product-quality-metrics"),
|
|
154
|
+
("7.4.", _LEAF + "q80-software-product-assurance-plan"),
|
|
155
|
+
("7.5.", _LEAF + "q80-software-product-assurance-plan"),
|
|
156
|
+
("5.", _LEAF + "q80-software-product-assurance-plan"),
|
|
157
|
+
("6.", _LEAF + "q80-software-process-assurance"),
|
|
158
|
+
("7.", _LEAF + "q80-software-process-assurance"),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
TOLERANCE = 1e-9
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Clause and applicability data. Derived from the clause structure of the
|
|
165
|
+
# standard: identifiers are facts; the topic labels are our own wording and
|
|
166
|
+
# no requirement or heading text is reproduced. Applicability codes per
|
|
167
|
+
# requirement in category order A B C D: Y applicable, N not applicable,
|
|
168
|
+
# R reduced, S set by security sensitivity. Default YYYY.
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
TOPICS = {'5': 'assurance programme',
|
|
172
|
+
'5.1': 'organisation',
|
|
173
|
+
'5.1.1': 'assurance organisation set-up',
|
|
174
|
+
'5.1.2': 'who answers for what',
|
|
175
|
+
'5.1.3': 'staff and means',
|
|
176
|
+
'5.1.4': 'the assurance lead',
|
|
177
|
+
'5.1.5': 'skills and training',
|
|
178
|
+
'5.2': 'programme management',
|
|
179
|
+
'5.2.1': 'planning and control of the programme',
|
|
180
|
+
'5.2.2': 'assurance reporting',
|
|
181
|
+
'5.2.3': 'audit programme',
|
|
182
|
+
'5.2.4': 'alert handling',
|
|
183
|
+
'5.2.5': 'problem reporting',
|
|
184
|
+
'5.2.6': 'nonconformance handling',
|
|
185
|
+
'5.2.7': 'quality model',
|
|
186
|
+
'5.3': 'risks and critical items',
|
|
187
|
+
'5.3.1': 'risk handling',
|
|
188
|
+
'5.3.2': 'critical items',
|
|
189
|
+
'5.4': 'suppliers',
|
|
190
|
+
'5.4.1': 'choosing suppliers',
|
|
191
|
+
'5.4.2': 'what suppliers are held to',
|
|
192
|
+
'5.4.3': 'watching suppliers',
|
|
193
|
+
'5.4.4': 'category flow-down to suppliers',
|
|
194
|
+
'5.4.5': 'security flow-down to suppliers',
|
|
195
|
+
'5.5': 'buying software',
|
|
196
|
+
'5.5.1': 'purchase documents',
|
|
197
|
+
'5.5.2': 'bought-in component list',
|
|
198
|
+
'5.5.3': 'purchase data',
|
|
199
|
+
'5.5.4': 'marking of bought items',
|
|
200
|
+
'5.5.5': 'incoming checks',
|
|
201
|
+
'5.5.6': 'export constraints',
|
|
202
|
+
'5.6': 'tools and environment',
|
|
203
|
+
'5.6.1': 'methods and tools chosen',
|
|
204
|
+
'5.6.2': 'choice of development environment',
|
|
205
|
+
'5.7': 'process capability',
|
|
206
|
+
'5.7.1': 'capability assessment',
|
|
207
|
+
'5.7.2': 'how assessments are run',
|
|
208
|
+
'5.7.3': 'improving the process',
|
|
209
|
+
'6': 'process assurance',
|
|
210
|
+
'6.1': 'life cycle',
|
|
211
|
+
'6.1.1': 'defining the life cycle',
|
|
212
|
+
'6.1.2': 'process targets',
|
|
213
|
+
'6.1.3': 'reviewing the life cycle',
|
|
214
|
+
'6.1.4': 'means for the life cycle',
|
|
215
|
+
'6.1.5': 'validation timing',
|
|
216
|
+
'6.2': 'cross-process obligations',
|
|
217
|
+
'6.2.1': 'process documentation',
|
|
218
|
+
'6.2.2': 'dependability and safety of software',
|
|
219
|
+
'6.2.3': 'critical software',
|
|
220
|
+
'6.2.4': 'configuration management',
|
|
221
|
+
'6.2.5': 'process measurement',
|
|
222
|
+
'6.2.6': 'verification',
|
|
223
|
+
'6.2.7': 'reusing existing software',
|
|
224
|
+
'6.2.8': 'generated code',
|
|
225
|
+
'6.2.9': 'security',
|
|
226
|
+
'6.2.10': 'security-sensitive software',
|
|
227
|
+
'6.3': 'per-process obligations',
|
|
228
|
+
'6.3.1': 'system-level software requirements',
|
|
229
|
+
'6.3.2': 'requirements analysis',
|
|
230
|
+
'6.3.3': 'architecture and design',
|
|
231
|
+
'6.3.4': 'coding',
|
|
232
|
+
'6.3.5': 'testing and validation',
|
|
233
|
+
'6.3.6': 'delivery and installation',
|
|
234
|
+
'6.3.7': 'acceptance',
|
|
235
|
+
'6.3.8': 'operations',
|
|
236
|
+
'6.3.9': 'maintenance',
|
|
237
|
+
'7': 'product quality assurance',
|
|
238
|
+
'7.1': 'quality targets and measurement',
|
|
239
|
+
'7.1.1': 'deriving quality requirements',
|
|
240
|
+
'7.1.2': 'quality requirements as numbers',
|
|
241
|
+
'7.1.3': 'checking quality requirements',
|
|
242
|
+
'7.1.4': 'product measures',
|
|
243
|
+
'7.1.5': 'basic measures',
|
|
244
|
+
'7.1.6': 'reporting measures',
|
|
245
|
+
'7.1.7': 'numerical accuracy',
|
|
246
|
+
'7.1.8': 'maturity analysis',
|
|
247
|
+
'7.2': 'product quality obligations',
|
|
248
|
+
'7.2.1': 'requirement documents',
|
|
249
|
+
'7.2.2': 'design documents',
|
|
250
|
+
'7.2.3': 'test and validation documents',
|
|
251
|
+
'7.3': 'software built for reuse',
|
|
252
|
+
'7.3.1': 'what the customer asks for',
|
|
253
|
+
'7.3.2': 'own documentation set',
|
|
254
|
+
'7.3.3': 'standalone information',
|
|
255
|
+
'7.3.4': 'reuse requirements',
|
|
256
|
+
'7.3.5': 'configuration of reusable items',
|
|
257
|
+
'7.3.6': 'multi-platform testing',
|
|
258
|
+
'7.3.7': 'conformance certificate',
|
|
259
|
+
'7.4': 'ground hardware and services',
|
|
260
|
+
'7.4.1': 'buying ground hardware',
|
|
261
|
+
'7.4.2': 'buying services',
|
|
262
|
+
'7.4.3': 'limits',
|
|
263
|
+
'7.4.4': 'choice',
|
|
264
|
+
'7.4.5': 'upkeep',
|
|
265
|
+
'7.5': 'programmable devices',
|
|
266
|
+
'7.5.1': 'programming devices',
|
|
267
|
+
'7.5.2': 'device marking',
|
|
268
|
+
'7.5.3': 'device calibration'}
|
|
269
|
+
|
|
270
|
+
REQUIREMENT_IDS = (
|
|
271
|
+
'5.1.1', '5.1.2.1', '5.1.2.2', '5.1.2.3', '5.1.3.1', '5.1.3.2', '5.1.4.1', '5.1.4.2',
|
|
272
|
+
'5.1.5.1', '5.1.5.2', '5.1.5.3', '5.1.5.4', '5.2.1.1', '5.2.1.2', '5.2.1.3', '5.2.1.4',
|
|
273
|
+
'5.2.1.5', '5.2.2.1', '5.2.2.2', '5.2.2.3', '5.2.3', '5.2.4', '5.2.5.1', '5.2.5.2',
|
|
274
|
+
'5.2.5.3', '5.2.5.4', '5.2.6.1', '5.2.6.2', '5.2.7.1', '5.2.7.2', '5.3.1', '5.3.2.1',
|
|
275
|
+
'5.3.2.2', '5.4.1.1', '5.4.1.2', '5.4.2.1', '5.4.2.2', '5.4.3.1', '5.4.3.2', '5.4.3.3',
|
|
276
|
+
'5.4.3.4', '5.4.4', '5.4.5', '5.5.1', '5.5.2', '5.5.3', '5.5.4', '5.5.5',
|
|
277
|
+
'5.5.6', '5.6.1.1', '5.6.1.2', '5.6.1.3', '5.6.2.1', '5.6.2.2', '5.6.2.3', '5.7.1',
|
|
278
|
+
'5.7.2.1', '5.7.2.2', '5.7.2.3', '5.7.2.4', '5.7.3.1', '5.7.3.2', '5.7.3.3', '6.1.1',
|
|
279
|
+
'6.1.2', '6.1.3', '6.1.4', '6.1.5', '6.2.1.1', '6.2.1.2', '6.2.1.3', '6.2.1.4',
|
|
280
|
+
'6.2.1.5', '6.2.1.6', '6.2.1.7', '6.2.1.8', '6.2.1.9', '6.2.2.1', '6.2.2.2', '6.2.2.3',
|
|
281
|
+
'6.2.2.4', '6.2.2.5', '6.2.2.6', '6.2.2.7', '6.2.2.8', '6.2.2.9', '6.2.2.10', '6.2.3.2',
|
|
282
|
+
'6.2.3.3', '6.2.3.4', '6.2.3.5', '6.2.3.6', '6.2.3.7', '6.2.3.8', '6.2.4.1', '6.2.4.2',
|
|
283
|
+
'6.2.4.3', '6.2.4.4', '6.2.4.5', '6.2.4.6', '6.2.4.7', '6.2.4.8', '6.2.4.9', '6.2.4.10',
|
|
284
|
+
'6.2.4.11', '6.2.4.12', '6.2.5.1', '6.2.5.2', '6.2.5.3', '6.2.5.4', '6.2.5.5', '6.2.6.1',
|
|
285
|
+
'6.2.6.2', '6.2.6.3', '6.2.6.4', '6.2.6.5', '6.2.6.6', '6.2.6.7', '6.2.6.8', '6.2.6.9',
|
|
286
|
+
'6.2.6.10', '6.2.6.11', '6.2.6.12', '6.2.6.13', '6.2.7.1', '6.2.7.2', '6.2.7.3', '6.2.7.4',
|
|
287
|
+
'6.2.7.5', '6.2.7.6', '6.2.7.7', '6.2.7.8', '6.2.7.9', '6.2.7.10', '6.2.7.11', '6.2.8.1',
|
|
288
|
+
'6.2.8.2', '6.2.8.3', '6.2.8.4', '6.2.8.5', '6.2.8.6', '6.2.8.7', '6.2.9.1', '6.2.9.2',
|
|
289
|
+
'6.2.9.3', '6.2.9.4', '6.2.9.5', '6.2.9.6', '6.2.9.7', '6.2.10.1', '6.2.10.2', '6.2.10.3',
|
|
290
|
+
'6.2.10.4', '6.3.1.1', '6.3.1.2', '6.3.1.3', '6.3.2.1', '6.3.2.2', '6.3.2.3', '6.3.2.4',
|
|
291
|
+
'6.3.2.5', '6.3.3.1', '6.3.3.2', '6.3.3.3', '6.3.3.4', '6.3.3.5', '6.3.3.6', '6.3.3.7',
|
|
292
|
+
'6.3.4.1', '6.3.4.2', '6.3.4.3', '6.3.4.4', '6.3.4.5', '6.3.4.6', '6.3.4.7', '6.3.4.8',
|
|
293
|
+
'6.3.5.1', '6.3.5.2', '6.3.5.3', '6.3.5.4', '6.3.5.5', '6.3.5.6', '6.3.5.7', '6.3.5.8',
|
|
294
|
+
'6.3.5.9', '6.3.5.10', '6.3.5.11', '6.3.5.12', '6.3.5.13', '6.3.5.14', '6.3.5.15', '6.3.5.16',
|
|
295
|
+
'6.3.5.17', '6.3.5.18', '6.3.5.19', '6.3.5.20', '6.3.5.21', '6.3.5.22', '6.3.5.23', '6.3.5.24',
|
|
296
|
+
'6.3.5.25', '6.3.5.26', '6.3.5.27', '6.3.5.28', '6.3.5.29', '6.3.5.30', '6.3.5.31', '6.3.5.32',
|
|
297
|
+
'6.3.5.33', '6.3.6.1', '6.3.6.2', '6.3.6.3', '6.3.6.4', '6.3.7.1', '6.3.7.2', '6.3.7.3',
|
|
298
|
+
'6.3.7.5', '6.3.7.6', '6.3.7.7', '6.3.8.1', '6.3.8.2', '6.3.8.3', '6.3.9.1', '6.3.9.2',
|
|
299
|
+
'6.3.9.3', '6.3.9.4', '6.3.9.5', '6.3.9.6', '6.3.9.7', '7.1.1', '7.1.2', '7.1.3',
|
|
300
|
+
'7.1.4', '7.1.5', '7.1.6', '7.1.7', '7.1.8', '7.2.1.1', '7.2.1.2', '7.2.1.3',
|
|
301
|
+
'7.2.2.1', '7.2.2.2', '7.2.2.3', '7.2.3.1', '7.2.3.2', '7.2.3.3', '7.2.3.4', '7.2.3.5',
|
|
302
|
+
'7.2.3.6', '7.3.1', '7.3.2', '7.3.3', '7.3.4', '7.3.5', '7.3.6', '7.3.7',
|
|
303
|
+
'7.4.1', '7.4.2', '7.4.3', '7.4.4', '7.4.5', '7.5.1', '7.5.2', '7.5.3',
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
APPLICABILITY = {'5.1.3.2': 'YYYN',
|
|
307
|
+
'5.1.5.1': 'YYYR',
|
|
308
|
+
'5.1.5.2': 'YYYN',
|
|
309
|
+
'5.2.3': 'YYYR',
|
|
310
|
+
'5.2.7.2': 'YYYR',
|
|
311
|
+
'5.4.1.1': 'YYYR',
|
|
312
|
+
'5.4.2.2': 'YYYN',
|
|
313
|
+
'5.4.3.4': 'YYYN',
|
|
314
|
+
'5.6.1.1': 'YYYR',
|
|
315
|
+
'5.6.1.2': 'YYYR',
|
|
316
|
+
'5.6.1.3': 'YYYR',
|
|
317
|
+
'5.6.2.1': 'YYYR',
|
|
318
|
+
'5.6.2.2': 'YYYR',
|
|
319
|
+
'5.7.1': 'YYYN',
|
|
320
|
+
'5.7.2.1': 'YYYN',
|
|
321
|
+
'5.7.2.2': 'YYYN',
|
|
322
|
+
'5.7.2.3': 'YYYN',
|
|
323
|
+
'5.7.2.4': 'YYYN',
|
|
324
|
+
'5.7.3.1': 'YYYN',
|
|
325
|
+
'5.7.3.2': 'YYYN',
|
|
326
|
+
'5.7.3.3': 'YYYN',
|
|
327
|
+
'6.2.1.9': 'YYYN',
|
|
328
|
+
'6.2.2.2': 'YYYN',
|
|
329
|
+
'6.2.2.3': 'YYYN',
|
|
330
|
+
'6.2.2.4': 'YYYN',
|
|
331
|
+
'6.2.2.5': 'YYYN',
|
|
332
|
+
'6.2.2.6': 'YYYN',
|
|
333
|
+
'6.2.2.7': 'YYYN',
|
|
334
|
+
'6.2.3.2': 'YYYN',
|
|
335
|
+
'6.2.3.3': 'YYYN',
|
|
336
|
+
'6.2.3.4': 'YYYN',
|
|
337
|
+
'6.2.3.5': 'YYYN',
|
|
338
|
+
'6.2.3.6': 'YYYN',
|
|
339
|
+
'6.2.3.7': 'YYNN',
|
|
340
|
+
'6.2.3.8': 'YYYN',
|
|
341
|
+
'6.2.5.4': 'YYYR',
|
|
342
|
+
'6.2.6.5': 'YYYN',
|
|
343
|
+
'6.2.6.6': 'YYYN',
|
|
344
|
+
'6.2.6.13': 'YYNN',
|
|
345
|
+
'6.2.7.4': 'YYYR',
|
|
346
|
+
'6.2.7.7': 'YYYR',
|
|
347
|
+
'6.2.7.8': 'YYYR',
|
|
348
|
+
'6.2.9.1': 'SSSS',
|
|
349
|
+
'6.2.9.2': 'SSSS',
|
|
350
|
+
'6.2.9.3': 'SSSS',
|
|
351
|
+
'6.2.9.4': 'SSSS',
|
|
352
|
+
'6.2.9.5': 'SSSS',
|
|
353
|
+
'6.2.9.6': 'SSSS',
|
|
354
|
+
'6.2.9.7': 'SSSS',
|
|
355
|
+
'6.2.10.1': 'SSSS',
|
|
356
|
+
'6.2.10.2': 'SSSS',
|
|
357
|
+
'6.2.10.3': 'SSSS',
|
|
358
|
+
'6.2.10.4': 'SSSS',
|
|
359
|
+
'6.3.3.1': 'YYYR',
|
|
360
|
+
'6.3.3.2': 'YYYR',
|
|
361
|
+
'6.3.3.3': 'YYYN',
|
|
362
|
+
'6.3.3.4': 'YYYR',
|
|
363
|
+
'6.3.3.5': 'YYYN',
|
|
364
|
+
'6.3.3.6': 'YYYN',
|
|
365
|
+
'6.3.4.3': 'YYYN',
|
|
366
|
+
'6.3.4.4': 'YYYR',
|
|
367
|
+
'6.3.4.8': 'YYYR',
|
|
368
|
+
'6.3.5.1': 'YYYR',
|
|
369
|
+
'6.3.5.2': 'YYYR',
|
|
370
|
+
'6.3.5.3': 'YYYR',
|
|
371
|
+
'6.3.5.4': 'YYYR',
|
|
372
|
+
'6.3.5.9': 'YYYN',
|
|
373
|
+
'6.3.5.10': 'YYYN',
|
|
374
|
+
'6.3.5.14': 'YYYR',
|
|
375
|
+
'6.3.5.19': 'YYYN',
|
|
376
|
+
'6.3.5.28': 'YYNN',
|
|
377
|
+
'6.3.5.30': 'YYYN',
|
|
378
|
+
'6.3.5.31': 'YYYN',
|
|
379
|
+
'6.3.8.2': 'YYRR',
|
|
380
|
+
'6.3.9.7': 'YYYR',
|
|
381
|
+
'7.1.4': 'YYYR',
|
|
382
|
+
'7.1.5': 'YYYR',
|
|
383
|
+
'7.1.8': 'YYYN'}
|
|
384
|
+
|
|
385
|
+
REDUCTION_NOTES = {'5.1.5.1': 'training need still met; the formal training output is not demanded',
|
|
386
|
+
'5.2.3': 'audits held only where a need is identified, not on a fixed plan',
|
|
387
|
+
'5.2.7.2': 'only the quality characteristics that matter for this software are kept',
|
|
388
|
+
'5.4.1.1': 'supplier selection still done; the formal output is not demanded',
|
|
389
|
+
'5.6.1.1': 'methods and tools must have a prior successful use, any domain',
|
|
390
|
+
'5.6.1.2': 'the activity stays; the formal output is not demanded',
|
|
391
|
+
'5.6.1.3': 'the activity stays; the formal output is not demanded',
|
|
392
|
+
'5.6.2.1': 'the activity stays; the formal output is not demanded',
|
|
393
|
+
'5.6.2.2': 'the activity stays; the formal output is not demanded',
|
|
394
|
+
'6.2.5.4': 'process metrics limited to problems found during validation',
|
|
395
|
+
'6.2.7.4': 'reuse analysis trimmed to a subset of its items, design scope limited',
|
|
396
|
+
'6.2.7.7': 'kept only as far as maintainability of the software needs it',
|
|
397
|
+
'6.2.7.8': 'kept only as far as maintainability of the software needs it',
|
|
398
|
+
'6.3.3.1': 'only document control is asked of the design',
|
|
399
|
+
'6.3.3.2': 'design standards become a recommendation',
|
|
400
|
+
'6.3.3.4': 'applies only if design standards were adopted',
|
|
401
|
+
'6.3.4.4': 'not asked unless the software is security sensitive',
|
|
402
|
+
'6.3.4.8': 'code enters configuration control no later than validation start',
|
|
403
|
+
'6.3.5.1': 'no formal unit and integration test activity is demanded',
|
|
404
|
+
'6.3.5.2': 'no formal unit and integration test activity is demanded',
|
|
405
|
+
'6.3.5.3': 'test procedures and data are verified by sampling',
|
|
406
|
+
'6.3.5.4': 'held for validation and acceptance testing only',
|
|
407
|
+
'6.3.5.14': 'held for validation and acceptance testing only',
|
|
408
|
+
'6.3.8.2': 'the safety-feature item of the clause drops out',
|
|
409
|
+
'6.3.9.7': 'statistical data need not be collected',
|
|
410
|
+
'7.1.4': 'one product metric item drops out',
|
|
411
|
+
'7.1.5': 'design and fault-density or failure-intensity metrics not demanded'}
|
|
412
|
+
|
|
413
|
+
DOCUMENT_TITLES = {'acceptance-documentation': 'acceptance test plan, report and joint review',
|
|
414
|
+
'alerts': 'alert information raised and received',
|
|
415
|
+
'audit-plan': 'audit plan and schedule',
|
|
416
|
+
'coding-standards': 'coding standards and their tools',
|
|
417
|
+
'criticality-classification': 'criticality classification of software products and '
|
|
418
|
+
'components',
|
|
419
|
+
'dependability-safety-analysis': 'software dependability and safety analysis report',
|
|
420
|
+
'design-justification': 'justification of design choices',
|
|
421
|
+
'design-standards': 'design and modelling standards',
|
|
422
|
+
'isvv-plan': 'independent software verification and validation plan',
|
|
423
|
+
'isvv-report': 'independent software verification and validation report',
|
|
424
|
+
'maintenance': 'maintenance plan and records',
|
|
425
|
+
'nonconformance-reports': 'nonconformance reports and review board records',
|
|
426
|
+
'operations': 'operations support and ground equipment selection',
|
|
427
|
+
'problem-reports': 'software problem reports and their closure',
|
|
428
|
+
'procedures-standards': 'procedures and standards in force',
|
|
429
|
+
'process-assessment': 'process assessment and improvement records',
|
|
430
|
+
'procurement': 'procurement data and receiving inspection',
|
|
431
|
+
'requirements-baseline': 'quality requirements in the requirements baseline',
|
|
432
|
+
'review-inspection-records': 'review and inspection plans and reports',
|
|
433
|
+
'scm-plan': 'software configuration management plan',
|
|
434
|
+
'sdp': 'software development plan and project plans',
|
|
435
|
+
'security-analysis': 'software security analysis report',
|
|
436
|
+
'security-management-plan': 'software security management plan',
|
|
437
|
+
'software-configuration-file': 'software configuration file and release data',
|
|
438
|
+
'software-reuse-file': 'software reuse file',
|
|
439
|
+
'software-verification-plan': 'software verification plan',
|
|
440
|
+
'software-verification-report': 'software verification report',
|
|
441
|
+
'spa-reports': 'periodic software product assurance reports',
|
|
442
|
+
'spamr': 'software product assurance milestone report (SPAMR)',
|
|
443
|
+
'spap': 'software product assurance plan (SPAP)',
|
|
444
|
+
'supplier-control': 'supplier selection, requirements flow-down and monitoring',
|
|
445
|
+
'technical-specification': 'quality requirements in the technical specification',
|
|
446
|
+
'test-readiness': 'test readiness confirmation and test compliance statement',
|
|
447
|
+
'training': 'training plan and records',
|
|
448
|
+
'validation-documentation': 'test and validation plans, specifications and reports'}
|
|
449
|
+
|
|
450
|
+
DOCUMENT_CONDITIONS = {'software-reuse-file': 'reuse',
|
|
451
|
+
'security-analysis': 'security',
|
|
452
|
+
'security-management-plan': 'security',
|
|
453
|
+
'supplier-control': 'suppliers',
|
|
454
|
+
'procurement': 'procured',
|
|
455
|
+
'operations': 'operations',
|
|
456
|
+
'maintenance': 'operations'}
|
|
457
|
+
|
|
458
|
+
DOCUMENT_CLAUSE_REVIEWS = {'acceptance-documentation': {'6.3.7.1': ('qr', 'ar'),
|
|
459
|
+
'6.3.7.6': ('ar',),
|
|
460
|
+
'6.3.7.7': ('ar',)},
|
|
461
|
+
'alerts': {'5.2.4': ()},
|
|
462
|
+
'audit-plan': {'5.2.3': ('srr',)},
|
|
463
|
+
'coding-standards': {'6.3.4.1': ('pdr',), '6.3.4.2': ('pdr',), '6.3.4.4': ('pdr',)},
|
|
464
|
+
'criticality-classification': {'6.2.2.1': ('srr', 'pdr'), '6.2.2.3': ('pdr',)},
|
|
465
|
+
'dependability-safety-analysis': {'6.2.2.2': ('pdr',),
|
|
466
|
+
'6.2.2.5': ('cdr', 'qr', 'ar'),
|
|
467
|
+
'6.2.2.6': ('cdr', 'qr', 'ar'),
|
|
468
|
+
'6.2.2.7': ('pdr', 'cdr'),
|
|
469
|
+
'6.2.2.10': ('pdr', 'cdr', 'qr', 'ar'),
|
|
470
|
+
'6.2.10.1': ('pdr', 'cdr', 'qr', 'ar')},
|
|
471
|
+
'design-justification': {'7.2.2.3': ('pdr', 'cdr')},
|
|
472
|
+
'design-standards': {'6.2.8.4': ('srr', 'pdr'), '6.3.3.2': ('srr', 'pdr')},
|
|
473
|
+
'isvv-plan': {'6.2.6.13': ('srr', 'pdr'), '6.3.5.28': ('srr', 'pdr')},
|
|
474
|
+
'isvv-report': {'6.2.6.13': ('pdr', 'cdr', 'qr', 'ar'),
|
|
475
|
+
'6.3.5.28': ('pdr', 'cdr', 'qr', 'ar')},
|
|
476
|
+
'maintenance': {'6.3.9.1': ('qr', 'ar', 'orr'),
|
|
477
|
+
'6.3.9.2': ('qr', 'ar', 'orr'),
|
|
478
|
+
'6.3.9.4': ('qr', 'ar', 'orr'),
|
|
479
|
+
'6.3.9.5': ('qr', 'ar', 'orr'),
|
|
480
|
+
'6.3.9.6': (),
|
|
481
|
+
'6.3.9.7': ()},
|
|
482
|
+
'nonconformance-reports': {'5.2.6.1': ('srr',), '6.3.7.5': ('ar',)},
|
|
483
|
+
'operations': {'6.3.8.1': ('orr',),
|
|
484
|
+
'6.3.8.2': ('orr',),
|
|
485
|
+
'7.4.1': ('srr', 'pdr'),
|
|
486
|
+
'7.4.2': ('srr', 'pdr'),
|
|
487
|
+
'7.4.3': ('srr', 'pdr'),
|
|
488
|
+
'7.4.4': ('srr', 'pdr')},
|
|
489
|
+
'problem-reports': {'5.2.5.1': ('pdr',),
|
|
490
|
+
'5.2.5.2': ('pdr',),
|
|
491
|
+
'5.2.5.3': ('pdr',),
|
|
492
|
+
'6.2.6.4': ('srr', 'pdr', 'cdr', 'qr', 'ar', 'orr'),
|
|
493
|
+
'6.3.5.6': ('cdr', 'qr', 'ar', 'orr'),
|
|
494
|
+
'6.3.5.8': ('srr', 'pdr', 'cdr', 'qr', 'ar', 'orr')},
|
|
495
|
+
'procedures-standards': {'6.2.1.6': ('pdr',), '6.2.1.7': ('pdr',)},
|
|
496
|
+
'process-assessment': {'5.7.1': (),
|
|
497
|
+
'5.7.2.1': (),
|
|
498
|
+
'5.7.2.2': (),
|
|
499
|
+
'5.7.2.3': (),
|
|
500
|
+
'5.7.2.4': (),
|
|
501
|
+
'5.7.3.1': (),
|
|
502
|
+
'5.7.3.2': (),
|
|
503
|
+
'5.7.3.3': ()},
|
|
504
|
+
'procurement': {'5.5.3': ('srr', 'pdr'),
|
|
505
|
+
'5.5.5': ('pdr', 'cdr', 'qr'),
|
|
506
|
+
'7.4.1': ('srr', 'pdr')},
|
|
507
|
+
'requirements-baseline': {'7.1.1': ('srr',),
|
|
508
|
+
'7.1.2': ('srr',),
|
|
509
|
+
'7.2.1.1': ('srr',),
|
|
510
|
+
'7.2.1.3': ('srr',)},
|
|
511
|
+
'review-inspection-records': {'6.2.6.9': (), '6.2.6.10': (), '6.2.6.11': ()},
|
|
512
|
+
'scm-plan': {'6.2.4.2': ('srr', 'pdr'),
|
|
513
|
+
'6.2.4.5': ('srr', 'pdr'),
|
|
514
|
+
'6.2.4.12': ('srr', 'pdr'),
|
|
515
|
+
'7.3.5': ('srr', 'pdr')},
|
|
516
|
+
'sdp': {'5.5.2': ('srr', 'pdr'),
|
|
517
|
+
'5.6.2.1': ('srr', 'pdr'),
|
|
518
|
+
'5.6.2.2': ('srr', 'pdr'),
|
|
519
|
+
'6.2.1.1': (),
|
|
520
|
+
'6.2.1.2': (),
|
|
521
|
+
'6.2.1.3': (),
|
|
522
|
+
'6.3.4.5': ('pdr',)},
|
|
523
|
+
'security-analysis': {'5.4.5': ('srr',),
|
|
524
|
+
'6.2.9.2': ('pdr',),
|
|
525
|
+
'6.2.9.5': ('cdr', 'qr', 'ar'),
|
|
526
|
+
'6.2.9.6': ('cdr', 'qr', 'ar'),
|
|
527
|
+
'6.2.9.7': ('pdr', 'cdr'),
|
|
528
|
+
'6.2.10.1': ('pdr', 'cdr', 'qr', 'ar')},
|
|
529
|
+
'security-management-plan': {'6.2.4.8': ('srr', 'pdr'),
|
|
530
|
+
'6.2.4.9': ('srr',),
|
|
531
|
+
'6.2.4.11': ('srr',),
|
|
532
|
+
'6.2.10.2': ('pdr', 'cdr', 'qr', 'ar'),
|
|
533
|
+
'6.2.10.3': ('pdr', 'cdr'),
|
|
534
|
+
'6.2.10.4': ('pdr', 'cdr')},
|
|
535
|
+
'software-configuration-file': {'6.2.4.3': (),
|
|
536
|
+
'6.2.4.4': ('cdr', 'qr', 'ar', 'orr'),
|
|
537
|
+
'6.2.4.5': ('cdr', 'qr', 'ar', 'orr'),
|
|
538
|
+
'6.2.4.6': ('cdr', 'qr', 'ar', 'orr'),
|
|
539
|
+
'6.2.4.8': ('cdr', 'qr', 'ar', 'orr'),
|
|
540
|
+
'6.2.4.10': (),
|
|
541
|
+
'6.2.4.11': (),
|
|
542
|
+
'6.3.6.2': ('ar',)},
|
|
543
|
+
'software-reuse-file': {'6.2.7.2': ('srr', 'pdr'),
|
|
544
|
+
'6.2.7.3': ('srr', 'pdr'),
|
|
545
|
+
'6.2.7.4': ('srr', 'pdr'),
|
|
546
|
+
'6.2.7.5': ('srr', 'pdr'),
|
|
547
|
+
'6.2.7.6': ('srr', 'pdr'),
|
|
548
|
+
'6.2.7.7': ('srr', 'pdr'),
|
|
549
|
+
'6.2.7.8': ('srr', 'pdr'),
|
|
550
|
+
'6.2.7.9': ('cdr', 'qr', 'ar'),
|
|
551
|
+
'6.2.7.11': ('srr', 'pdr', 'cdr', 'qr', 'ar'),
|
|
552
|
+
'7.3.6': ('cdr',),
|
|
553
|
+
'7.3.7': ('cdr',)},
|
|
554
|
+
'software-verification-plan': {'6.2.6.1': ('srr', 'pdr')},
|
|
555
|
+
'software-verification-report': {'6.2.6.5': ('cdr', 'qr', 'ar'),
|
|
556
|
+
'6.2.6.6': ('cdr', 'qr', 'ar'),
|
|
557
|
+
'7.1.7': ('pdr', 'cdr', 'qr'),
|
|
558
|
+
'7.2.3.6': ('cdr', 'qr', 'ar')},
|
|
559
|
+
'spa-reports': {'5.2.2.1': (),
|
|
560
|
+
'5.2.2.2': (),
|
|
561
|
+
'5.6.1.3': (),
|
|
562
|
+
'6.2.5.4': (),
|
|
563
|
+
'6.2.5.5': (),
|
|
564
|
+
'6.2.6.2': (),
|
|
565
|
+
'6.2.6.3': (),
|
|
566
|
+
'6.2.6.7': (),
|
|
567
|
+
'6.2.8.5': (),
|
|
568
|
+
'6.3.3.4': (),
|
|
569
|
+
'6.3.3.6': (),
|
|
570
|
+
'6.3.3.7': (),
|
|
571
|
+
'6.3.4.7': (),
|
|
572
|
+
'6.3.5.3': (),
|
|
573
|
+
'6.3.5.5': (),
|
|
574
|
+
'6.3.5.12': (),
|
|
575
|
+
'7.1.6': (),
|
|
576
|
+
'7.1.8': ()},
|
|
577
|
+
'spamr': {'5.2.2.3': ('srr', 'pdr', 'cdr', 'qr', 'ar', 'orr'),
|
|
578
|
+
'5.6.1.2': ('srr', 'pdr'),
|
|
579
|
+
'6.2.3.3': ('pdr', 'cdr', 'qr', 'ar'),
|
|
580
|
+
'6.2.6.12': ('srr', 'pdr', 'cdr', 'qr', 'ar', 'orr')},
|
|
581
|
+
'spap': {'5.1.2.1': ('srr',),
|
|
582
|
+
'5.1.2.2': ('srr',),
|
|
583
|
+
'5.1.2.3': ('srr',),
|
|
584
|
+
'5.1.3.1': ('srr',),
|
|
585
|
+
'5.1.4.1': ('srr',),
|
|
586
|
+
'5.2.1.1': ('srr', 'pdr'),
|
|
587
|
+
'5.2.1.3': ('cdr', 'qr', 'ar', 'orr'),
|
|
588
|
+
'5.2.1.4': ('ar',),
|
|
589
|
+
'5.2.1.5': ('srr', 'pdr'),
|
|
590
|
+
'5.2.6.1': ('srr',),
|
|
591
|
+
'5.2.6.2': ('srr', 'pdr'),
|
|
592
|
+
'5.2.7.1': ('pdr',),
|
|
593
|
+
'5.2.7.2': ('pdr',),
|
|
594
|
+
'5.4.3.3': ('pdr',),
|
|
595
|
+
'5.4.3.4': ('pdr',),
|
|
596
|
+
'5.6.1.1': ('srr', 'pdr'),
|
|
597
|
+
'6.1.1': ('srr', 'pdr'),
|
|
598
|
+
'6.2.1.4': ('srr', 'pdr'),
|
|
599
|
+
'6.2.2.10': ('pdr', 'cdr'),
|
|
600
|
+
'6.2.3.2': ('pdr', 'cdr'),
|
|
601
|
+
'6.2.3.4': ('pdr', 'cdr'),
|
|
602
|
+
'6.2.3.5': ('pdr', 'cdr'),
|
|
603
|
+
'6.2.4.8': ('srr', 'pdr'),
|
|
604
|
+
'6.2.4.9': ('srr', 'pdr'),
|
|
605
|
+
'6.2.4.11': ('srr', 'pdr'),
|
|
606
|
+
'6.2.5.1': ('srr', 'pdr'),
|
|
607
|
+
'6.2.5.2': ('srr', 'pdr'),
|
|
608
|
+
'6.2.7.2': ('srr', 'pdr'),
|
|
609
|
+
'6.2.7.3': ('srr', 'pdr'),
|
|
610
|
+
'6.2.7.4': ('srr', 'pdr'),
|
|
611
|
+
'6.2.7.5': ('srr', 'pdr'),
|
|
612
|
+
'6.2.9.1': ('srr', 'pdr', 'cdr'),
|
|
613
|
+
'6.2.10.3': ('pdr', 'cdr'),
|
|
614
|
+
'6.2.10.4': ('pdr', 'cdr'),
|
|
615
|
+
'6.3.3.3': ('pdr',),
|
|
616
|
+
'6.3.3.5': ('pdr',),
|
|
617
|
+
'6.3.3.7': ('pdr',),
|
|
618
|
+
'6.3.4.3': ('pdr',),
|
|
619
|
+
'6.3.4.6': ('pdr',),
|
|
620
|
+
'6.3.5.1': ('pdr', 'cdr'),
|
|
621
|
+
'6.3.5.2': ('pdr', 'cdr'),
|
|
622
|
+
'6.3.8.3': ('orr',),
|
|
623
|
+
'7.1.3': ('srr', 'pdr'),
|
|
624
|
+
'7.1.4': ('srr', 'pdr'),
|
|
625
|
+
'7.1.5': ('srr', 'pdr'),
|
|
626
|
+
'7.2.2.3': ('srr', 'pdr'),
|
|
627
|
+
'7.5.1': ('pdr',),
|
|
628
|
+
'7.5.2': ('pdr',)},
|
|
629
|
+
'supplier-control': {'5.4.1.1': (),
|
|
630
|
+
'5.4.2.1': ('srr',),
|
|
631
|
+
'5.4.2.2': ('srr',),
|
|
632
|
+
'5.4.4': ('srr',)},
|
|
633
|
+
'technical-specification': {'6.3.2.4': ('pdr',),
|
|
634
|
+
'7.1.1': ('pdr',),
|
|
635
|
+
'7.1.2': ('pdr',),
|
|
636
|
+
'7.2.1.1': ('pdr',),
|
|
637
|
+
'7.2.1.3': ('pdr',),
|
|
638
|
+
'7.3.4': ('pdr',)},
|
|
639
|
+
'test-readiness': {'6.1.5': ('trr',),
|
|
640
|
+
'6.3.5.4': ('trr',),
|
|
641
|
+
'6.3.5.7': ('cdr', 'qr', 'ar', 'orr'),
|
|
642
|
+
'6.3.5.11': ('cdr', 'qr', 'ar', 'orr')},
|
|
643
|
+
'training': {'5.1.5.1': ('srr',), '5.1.5.2': ()},
|
|
644
|
+
'validation-documentation': {'6.2.8.2': ('pdr', 'cdr', 'qr', 'ar'),
|
|
645
|
+
'6.2.8.7': ('pdr', 'cdr', 'qr', 'ar'),
|
|
646
|
+
'6.3.5.13': ('cdr', 'qr', 'ar', 'orr'),
|
|
647
|
+
'6.3.5.16': ('cdr', 'qr', 'ar', 'orr'),
|
|
648
|
+
'6.3.5.17': ('cdr', 'qr', 'ar', 'orr'),
|
|
649
|
+
'6.3.5.18': ('cdr', 'qr', 'ar', 'orr'),
|
|
650
|
+
'6.3.5.22': ('pdr', 'cdr'),
|
|
651
|
+
'6.3.5.23': ('pdr', 'cdr'),
|
|
652
|
+
'6.3.5.24': ('pdr', 'cdr'),
|
|
653
|
+
'6.3.5.25': ('pdr', 'cdr', 'qr', 'ar'),
|
|
654
|
+
'6.3.5.27': ('ar',),
|
|
655
|
+
'6.3.5.29': ('pdr', 'cdr', 'qr', 'ar'),
|
|
656
|
+
'6.3.5.30': ('cdr', 'qr', 'ar'),
|
|
657
|
+
'6.3.5.31': ('cdr', 'qr', 'ar'),
|
|
658
|
+
'6.3.5.32': ('cdr', 'qr', 'ar'),
|
|
659
|
+
'6.3.5.33': ('cdr', 'qr', 'ar')}}
|
|
660
|
+
|
|
661
|
+
SPAMR_SECTIONS = (('1', 'purpose and scope of this milestone report', None),
|
|
662
|
+
('2', 'applicable and reference documents', None),
|
|
663
|
+
('3', 'terms and abbreviations', None),
|
|
664
|
+
('4',
|
|
665
|
+
'verification activities performed by product assurance',
|
|
666
|
+
'verification_activities'),
|
|
667
|
+
('5', 'suitability of methods and tools', 'methods_tools'),
|
|
668
|
+
('6', 'adherence to design and coding standards', 'standards_adherence'),
|
|
669
|
+
('7', 'product and process metrics against their targets', 'metrics'),
|
|
670
|
+
('8', 'testing and validation status and coverage', 'testing'),
|
|
671
|
+
('9', 'status of software problem reports and nonconformances', 'problems'),
|
|
672
|
+
('10', 'references to progress reports', 'progress_reports'))
|
|
673
|
+
|
|
674
|
+
METRICS = {'cyclomatic_complexity': ('product', 'per function', 'max', '7.1.5'),
|
|
675
|
+
'nesting_depth': ('product', 'levels', 'max', '7.1.4'),
|
|
676
|
+
'function_size_loc': ('product', 'lines', 'max', '7.1.4'),
|
|
677
|
+
'comment_density': ('product', 'ratio', 'min', '7.1.4'),
|
|
678
|
+
'requirement_coverage': ('product', 'ratio', 'min', '7.2.1'),
|
|
679
|
+
'requirement_test_coverage': ('product', 'ratio', 'min', '6.3.5.2'),
|
|
680
|
+
'statement_coverage': ('product', 'ratio', 'min', '6.3.5.2'),
|
|
681
|
+
'decision_coverage': ('product', 'ratio', 'min', '6.3.5.2'),
|
|
682
|
+
'mcdc_coverage': ('product', 'ratio', 'min', '6.3.5.2'),
|
|
683
|
+
'open_major_nonconformances': ('process', 'count', 'max', '5.2.6'),
|
|
684
|
+
'open_problem_reports': ('process', 'count', 'max', '5.2.5'),
|
|
685
|
+
'coding_standard_violations': ('product', 'count', 'max', '6.3.4')}
|
|
686
|
+
|
|
687
|
+
DEFAULT_THRESHOLDS = {'A': {'cyclomatic_complexity': 10,
|
|
688
|
+
'nesting_depth': 4,
|
|
689
|
+
'function_size_loc': 60,
|
|
690
|
+
'comment_density': 0.25,
|
|
691
|
+
'requirement_coverage': 1.0,
|
|
692
|
+
'requirement_test_coverage': 1.0,
|
|
693
|
+
'statement_coverage': 1.0,
|
|
694
|
+
'decision_coverage': 1.0,
|
|
695
|
+
'mcdc_coverage': 1.0,
|
|
696
|
+
'open_major_nonconformances': 0,
|
|
697
|
+
'open_problem_reports': None,
|
|
698
|
+
'coding_standard_violations': 0},
|
|
699
|
+
'B': {'cyclomatic_complexity': 10,
|
|
700
|
+
'nesting_depth': 4,
|
|
701
|
+
'function_size_loc': 80,
|
|
702
|
+
'comment_density': 0.2,
|
|
703
|
+
'requirement_coverage': 1.0,
|
|
704
|
+
'requirement_test_coverage': 1.0,
|
|
705
|
+
'statement_coverage': 1.0,
|
|
706
|
+
'decision_coverage': 1.0,
|
|
707
|
+
'mcdc_coverage': None,
|
|
708
|
+
'open_major_nonconformances': 0,
|
|
709
|
+
'open_problem_reports': None,
|
|
710
|
+
'coding_standard_violations': 0},
|
|
711
|
+
'C': {'cyclomatic_complexity': 15,
|
|
712
|
+
'nesting_depth': 5,
|
|
713
|
+
'function_size_loc': 100,
|
|
714
|
+
'comment_density': 0.15,
|
|
715
|
+
'requirement_coverage': 1.0,
|
|
716
|
+
'requirement_test_coverage': 1.0,
|
|
717
|
+
'statement_coverage': None,
|
|
718
|
+
'decision_coverage': None,
|
|
719
|
+
'mcdc_coverage': None,
|
|
720
|
+
'open_major_nonconformances': 0,
|
|
721
|
+
'open_problem_reports': None,
|
|
722
|
+
'coding_standard_violations': None},
|
|
723
|
+
'D': {'cyclomatic_complexity': 20,
|
|
724
|
+
'nesting_depth': 6,
|
|
725
|
+
'function_size_loc': 150,
|
|
726
|
+
'comment_density': 0.1,
|
|
727
|
+
'requirement_coverage': 1.0,
|
|
728
|
+
'requirement_test_coverage': 0.9,
|
|
729
|
+
'statement_coverage': None,
|
|
730
|
+
'decision_coverage': None,
|
|
731
|
+
'mcdc_coverage': None,
|
|
732
|
+
'open_major_nonconformances': 0,
|
|
733
|
+
'open_problem_reports': None,
|
|
734
|
+
'coding_standard_violations': None}}
|
|
735
|
+
|
|
736
|
+
_REQUIREMENT_SET = frozenset(REQUIREMENT_IDS)
|
|
737
|
+
|
|
738
|
+
# Worst first: several evidence rows on one clause combine to the weakest.
|
|
739
|
+
_SEVERITY_RANK = {"not-compliant": 0, "partially-compliant": 1,
|
|
740
|
+
"compliant": 2}
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# ---------------------------------------------------------------------------
|
|
744
|
+
# Normalisation
|
|
745
|
+
# ---------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
def normalise_category(value):
|
|
748
|
+
"""Return the category letter A to D ('b', 'cat-B', ' B ' accepted)."""
|
|
749
|
+
text = str(value if value is not None else "").strip().upper()
|
|
750
|
+
for prefix in ("CATEGORY", "CAT-", "CAT"):
|
|
751
|
+
if text.startswith(prefix):
|
|
752
|
+
text = text[len(prefix):].strip(" -")
|
|
753
|
+
if text not in CATEGORIES:
|
|
754
|
+
raise ValueError("unknown software criticality category %r "
|
|
755
|
+
"(use A, B, C or D)" % (value,))
|
|
756
|
+
return text
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def normalise_severity(value):
|
|
760
|
+
"""Return the function severity I to IV (numeral, digit or name)."""
|
|
761
|
+
key = str(value if value is not None else "").strip()
|
|
762
|
+
if key.upper() in SEVERITIES:
|
|
763
|
+
return key.upper()
|
|
764
|
+
if key.lower() in _SEVERITY_ALIASES:
|
|
765
|
+
return _SEVERITY_ALIASES[key.lower()]
|
|
766
|
+
raise ValueError("unknown function severity %r (use I, II, III, IV or "
|
|
767
|
+
"catastrophic, critical, major, minor)" % (value,))
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def normalise_provision(value):
|
|
771
|
+
key = str(value).strip().lower()
|
|
772
|
+
if key not in _PROVISION_ALIASES:
|
|
773
|
+
raise ValueError("unknown compensating provision %r (use hardware, "
|
|
774
|
+
"software or operational-procedure)" % (value,))
|
|
775
|
+
return _PROVISION_ALIASES[key]
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def normalise_clause_id(value):
|
|
779
|
+
"""Return a clause id such as '6.2.3.4'.
|
|
780
|
+
|
|
781
|
+
A 'clause' / 'cl.' / section-sign prefix, a trailing dot and a trailing
|
|
782
|
+
item letter are cut: '6.2.3.4a', '6.2.3.4.a' and 'cl. 6.2.3.4' all give
|
|
783
|
+
'6.2.3.4'. Leading zeros in a part are dropped.
|
|
784
|
+
"""
|
|
785
|
+
if not isinstance(value, str):
|
|
786
|
+
raise ValueError("clause id must be text, got %r" % (value,))
|
|
787
|
+
text = value.strip().rstrip(".")
|
|
788
|
+
low = text.lower()
|
|
789
|
+
for prefix in ("clause", "cl.", "§"):
|
|
790
|
+
if low.startswith(prefix):
|
|
791
|
+
text = text[len(prefix):].strip()
|
|
792
|
+
low = text.lower()
|
|
793
|
+
while text and text[-1].isalpha():
|
|
794
|
+
text = text[:-1].rstrip(".")
|
|
795
|
+
parts = text.split(".")
|
|
796
|
+
if not text or not all(p.isdigit() for p in parts):
|
|
797
|
+
raise ValueError("not a clause identifier: %r" % (value,))
|
|
798
|
+
return ".".join(str(int(p)) for p in parts)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def normalise_status(value):
|
|
802
|
+
"""Map a status spelling to one of the four statuses; refuse the rest."""
|
|
803
|
+
key = str(value or "").strip().lower().replace("_", " ")
|
|
804
|
+
if key in STATUS_ALIASES:
|
|
805
|
+
return STATUS_ALIASES[key]
|
|
806
|
+
dashed = key.replace(" ", "-")
|
|
807
|
+
if dashed in STATUS_ALIASES:
|
|
808
|
+
return STATUS_ALIASES[dashed]
|
|
809
|
+
raise ValueError("unknown compliance status %r (use compliant, "
|
|
810
|
+
"partially-compliant, not-compliant or not-applicable)"
|
|
811
|
+
% (value,))
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def normalise_review(value):
|
|
815
|
+
"""Return the short review name (srr, pdr, cdr, trr, qr, ar, orr)."""
|
|
816
|
+
key = str(value).strip().lower().replace(" ", "-")
|
|
817
|
+
key = _REVIEW_ALIASES.get(key, key)
|
|
818
|
+
if key not in REVIEWS:
|
|
819
|
+
raise ValueError("unknown review %r (use SRR, PDR, CDR, TRR, QR, AR "
|
|
820
|
+
"or ORR)" % (value,))
|
|
821
|
+
return key
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def normalise_scope(flags):
|
|
825
|
+
"""Return the scope flags as a sorted tuple; refuse unknown flags."""
|
|
826
|
+
out = set()
|
|
827
|
+
for flag in flags or ():
|
|
828
|
+
key = str(flag).strip().lower()
|
|
829
|
+
if not key:
|
|
830
|
+
continue
|
|
831
|
+
if key not in SCOPE_FLAGS:
|
|
832
|
+
raise ValueError("unknown scope flag %r (use %s)"
|
|
833
|
+
% (flag, ", ".join(SCOPE_FLAGS)))
|
|
834
|
+
out.add(key)
|
|
835
|
+
return tuple(sorted(out))
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _clause_key(cid):
|
|
839
|
+
return [int(p) for p in cid.split(".")]
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def topic_of(clause):
|
|
843
|
+
"""Our topic label for the heading a clause sits under, or ''."""
|
|
844
|
+
parts = normalise_clause_id(clause).split(".")
|
|
845
|
+
for depth in range(min(3, len(parts)), 0, -1):
|
|
846
|
+
key = ".".join(parts[:depth])
|
|
847
|
+
if key in TOPICS:
|
|
848
|
+
return TOPICS[key]
|
|
849
|
+
return ""
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _in_group(cid, group):
|
|
853
|
+
return cid == group or cid.startswith(group + ".")
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# ---------------------------------------------------------------------------
|
|
857
|
+
# Stage 1-2: category and tailoring
|
|
858
|
+
# ---------------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
def derive_category(severity, provisions=(), provides_provision_for=None):
|
|
861
|
+
"""Derive the software criticality category.
|
|
862
|
+
|
|
863
|
+
The base category follows the highest severity of the functions the
|
|
864
|
+
software takes part in (I -> A ... IV -> D). A compensating provision
|
|
865
|
+
at system level (hardware, software, operational procedure) lowers it
|
|
866
|
+
by one step. Software that IS the compensating provision for functions
|
|
867
|
+
of some severity takes that severity's category with no credit.
|
|
868
|
+
|
|
869
|
+
Returns category, base_category, lowered_by_provision, rationale lines
|
|
870
|
+
and the constraints the assignment places on other items.
|
|
871
|
+
"""
|
|
872
|
+
sev = normalise_severity(severity)
|
|
873
|
+
base = CATEGORIES[SEVERITIES.index(sev)]
|
|
874
|
+
rationale = ["highest function severity %s gives base category %s"
|
|
875
|
+
% (sev, base)]
|
|
876
|
+
constraints = []
|
|
877
|
+
if provides_provision_for:
|
|
878
|
+
prov = normalise_severity(provides_provision_for)
|
|
879
|
+
prov_base = CATEGORIES[SEVERITIES.index(prov)]
|
|
880
|
+
category = min(base, prov_base) # 'A' sorts before 'B': stricter
|
|
881
|
+
rationale.append("the software is itself a compensating provision "
|
|
882
|
+
"for severity %s functions, so it takes category "
|
|
883
|
+
"%s without credit" % (prov, prov_base))
|
|
884
|
+
return {"severity": sev, "category": category, "base_category": base,
|
|
885
|
+
"lowered_by_provision": False, "provisions": [],
|
|
886
|
+
"rationale": rationale, "constraints": constraints}
|
|
887
|
+
kinds = sorted(set(normalise_provision(k) for k in (provisions or ())))
|
|
888
|
+
lowered = bool(kinds) and base != "D"
|
|
889
|
+
if lowered:
|
|
890
|
+
category = CATEGORIES[CATEGORIES.index(base) + 1]
|
|
891
|
+
rationale.append("compensating provision(s) %s lower %s to %s"
|
|
892
|
+
% (", ".join(kinds), base, category))
|
|
893
|
+
if "software" in kinds:
|
|
894
|
+
constraints.append("the software implementing the provision must "
|
|
895
|
+
"itself be category %s" % base)
|
|
896
|
+
constraints.append("each provision must meet the system dependability "
|
|
897
|
+
"and safety requirements before the lower category "
|
|
898
|
+
"is claimed")
|
|
899
|
+
else:
|
|
900
|
+
category = base
|
|
901
|
+
if kinds:
|
|
902
|
+
rationale.append("severity IV software is already category D; "
|
|
903
|
+
"no credit to take")
|
|
904
|
+
return {"severity": sev, "category": category, "base_category": base,
|
|
905
|
+
"lowered_by_provision": lowered, "provisions": kinds,
|
|
906
|
+
"rationale": rationale, "constraints": constraints}
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def tailored_status(clause, category, security_sensitive=False):
|
|
910
|
+
"""'applicable', 'reduced', 'not-applicable', or 'unknown'.
|
|
911
|
+
|
|
912
|
+
'unknown' = the id is not a requirement of this revision (a heading,
|
|
913
|
+
a typo, or an id from an older list); the matrix treats it as
|
|
914
|
+
applicable and says so.
|
|
915
|
+
"""
|
|
916
|
+
cid = normalise_clause_id(clause)
|
|
917
|
+
if cid not in _REQUIREMENT_SET:
|
|
918
|
+
return "unknown"
|
|
919
|
+
code = APPLICABILITY.get(cid, "YYYY")[
|
|
920
|
+
CATEGORIES.index(normalise_category(category))]
|
|
921
|
+
if code == "S":
|
|
922
|
+
return "applicable" if security_sensitive else "not-applicable"
|
|
923
|
+
return {"Y": "applicable", "R": "reduced", "N": "not-applicable"}[code]
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def tailoring_summary(category, security_sensitive=False):
|
|
927
|
+
"""Tailoring for one category, per clause group and in total."""
|
|
928
|
+
cat = normalise_category(category)
|
|
929
|
+
groups, order = {}, []
|
|
930
|
+
totals = {"applicable": 0, "reduced": 0, "not-applicable": 0}
|
|
931
|
+
reduced = []
|
|
932
|
+
for rid in REQUIREMENT_IDS:
|
|
933
|
+
status = tailored_status(rid, cat, security_sensitive)
|
|
934
|
+
group = ".".join(rid.split(".")[:2])
|
|
935
|
+
if group not in groups:
|
|
936
|
+
groups[group] = {"group": group, "topic": TOPICS.get(group, ""),
|
|
937
|
+
"applicable": 0, "reduced": 0,
|
|
938
|
+
"not-applicable": 0}
|
|
939
|
+
order.append(group)
|
|
940
|
+
groups[group][status] += 1
|
|
941
|
+
totals[status] += 1
|
|
942
|
+
if status == "reduced":
|
|
943
|
+
reduced.append({"clause": rid,
|
|
944
|
+
"note": REDUCTION_NOTES.get(
|
|
945
|
+
rid, "reduced scope for this category")})
|
|
946
|
+
return {"category": cat, "security_sensitive": bool(security_sensitive),
|
|
947
|
+
"groups": [groups[g] for g in order], "totals": totals,
|
|
948
|
+
"reduced": reduced}
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def default_clauses():
|
|
952
|
+
"""The requirement list of the standard as (id, topic) pairs."""
|
|
953
|
+
return [(rid, topic_of(rid)) for rid in REQUIREMENT_IDS]
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def parse_clause_list(text):
|
|
957
|
+
"""Parse a customer clause list: one id per line, or CSV with id[,title].
|
|
958
|
+
|
|
959
|
+
Blank lines and lines starting with '#' are skipped; a header row whose
|
|
960
|
+
first cell is not a clause id is skipped. Duplicates are refused.
|
|
961
|
+
"""
|
|
962
|
+
out, seen = [], set()
|
|
963
|
+
for n, raw in enumerate(csv.reader(io.StringIO(str(text))), 1):
|
|
964
|
+
if not raw or not raw[0].strip() or raw[0].strip().startswith("#"):
|
|
965
|
+
continue
|
|
966
|
+
try:
|
|
967
|
+
cid = normalise_clause_id(raw[0])
|
|
968
|
+
except ValueError:
|
|
969
|
+
if n == 1:
|
|
970
|
+
continue # header row
|
|
971
|
+
raise
|
|
972
|
+
if cid in seen:
|
|
973
|
+
raise ValueError("clause %s listed twice" % cid)
|
|
974
|
+
seen.add(cid)
|
|
975
|
+
title = raw[1].strip() if len(raw) > 1 and raw[1].strip() else ""
|
|
976
|
+
out.append((cid, title or topic_of(cid)))
|
|
977
|
+
if not out:
|
|
978
|
+
raise ValueError("clause list is empty")
|
|
979
|
+
return out
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
# ---------------------------------------------------------------------------
|
|
983
|
+
# Stage 3: evidence index and compliance matrix
|
|
984
|
+
# ---------------------------------------------------------------------------
|
|
985
|
+
|
|
986
|
+
_FIELD_ALIASES = {
|
|
987
|
+
"clause": "clause", "clause_id": "clause", "requirement": "clause",
|
|
988
|
+
"id": "clause",
|
|
989
|
+
"document": "document", "doc": "document", "evidence": "document",
|
|
990
|
+
"section": "section", "sect": "section", "paragraph": "section",
|
|
991
|
+
"status": "status", "compliance": "status",
|
|
992
|
+
"justification": "justification", "comment": "justification",
|
|
993
|
+
"rationale": "justification",
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def parse_evidence_csv(text):
|
|
998
|
+
"""Parse the evidence index from CSV text.
|
|
999
|
+
|
|
1000
|
+
Needs a clause column and a status column; document, section and
|
|
1001
|
+
justification are optional. Header spellings are matched loosely
|
|
1002
|
+
(clause/requirement/id, document/doc/evidence, section/paragraph,
|
|
1003
|
+
justification/comment/rationale). Blank rows are skipped.
|
|
1004
|
+
"""
|
|
1005
|
+
reader = csv.DictReader(io.StringIO(str(text)))
|
|
1006
|
+
if not reader.fieldnames:
|
|
1007
|
+
raise ValueError("evidence CSV has no header row")
|
|
1008
|
+
mapping = {}
|
|
1009
|
+
for name in reader.fieldnames:
|
|
1010
|
+
key = _FIELD_ALIASES.get(str(name).strip().lower())
|
|
1011
|
+
if key and key not in mapping.values():
|
|
1012
|
+
mapping[name] = key
|
|
1013
|
+
if "clause" not in mapping.values() or "status" not in mapping.values():
|
|
1014
|
+
raise ValueError("evidence CSV needs a clause column and a status "
|
|
1015
|
+
"column (optional: document, section, "
|
|
1016
|
+
"justification)")
|
|
1017
|
+
rows = []
|
|
1018
|
+
for raw in reader:
|
|
1019
|
+
row = {mapping[k]: (v or "").strip() for k, v in raw.items()
|
|
1020
|
+
if k in mapping}
|
|
1021
|
+
if not any(row.values()):
|
|
1022
|
+
continue
|
|
1023
|
+
rows.append(row)
|
|
1024
|
+
return rows
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def index_evidence(rows):
|
|
1028
|
+
"""{clause id: [entries]} with normalised status and stripped fields."""
|
|
1029
|
+
index = {}
|
|
1030
|
+
for n, row in enumerate(rows, 1):
|
|
1031
|
+
if not str(row.get("clause") or "").strip():
|
|
1032
|
+
raise ValueError("evidence row %d has no clause" % n)
|
|
1033
|
+
cid = normalise_clause_id(str(row["clause"]))
|
|
1034
|
+
try:
|
|
1035
|
+
status = normalise_status(row.get("status"))
|
|
1036
|
+
except ValueError as exc:
|
|
1037
|
+
raise ValueError("evidence row %d (clause %s): %s" % (n, cid, exc))
|
|
1038
|
+
index.setdefault(cid, []).append({
|
|
1039
|
+
"status": status,
|
|
1040
|
+
"document": str(row.get("document") or "").strip(),
|
|
1041
|
+
"section": str(row.get("section") or "").strip(),
|
|
1042
|
+
"justification": str(row.get("justification") or "").strip(),
|
|
1043
|
+
})
|
|
1044
|
+
return index
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def route_gap(kind, clause):
|
|
1048
|
+
"""The bound skill that resolves a gap of this kind on this clause."""
|
|
1049
|
+
if kind in GAP_ROUTES_BY_KIND:
|
|
1050
|
+
return GAP_ROUTES_BY_KIND[kind]
|
|
1051
|
+
for prefix, leaf in GAP_ROUTES_BY_CLAUSE:
|
|
1052
|
+
if clause.startswith(prefix):
|
|
1053
|
+
return leaf
|
|
1054
|
+
return _LEAF + "q80-compliance-matrix"
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _matrix_row(cid, title, entries, category, security_sensitive):
|
|
1058
|
+
gaps = []
|
|
1059
|
+
tailoring = (tailored_status(cid, category, security_sensitive)
|
|
1060
|
+
if category else "unknown")
|
|
1061
|
+
refs, docs = [], []
|
|
1062
|
+
for e in entries:
|
|
1063
|
+
if e["document"]:
|
|
1064
|
+
refs.append(e["document"] + (" " + e["section"]
|
|
1065
|
+
if e["section"] else ""))
|
|
1066
|
+
if e["document"] not in docs:
|
|
1067
|
+
docs.append(e["document"])
|
|
1068
|
+
justification = "; ".join(e["justification"] for e in entries
|
|
1069
|
+
if e["justification"])
|
|
1070
|
+
if not entries:
|
|
1071
|
+
if tailoring == "not-applicable":
|
|
1072
|
+
status = "not-applicable"
|
|
1073
|
+
justification = ("tailored out for software category %s"
|
|
1074
|
+
% category)
|
|
1075
|
+
else:
|
|
1076
|
+
status = "not-compliant"
|
|
1077
|
+
gaps.append("no-evidence")
|
|
1078
|
+
justification = "no evidence mapped; open until assessed"
|
|
1079
|
+
else:
|
|
1080
|
+
stated = [e["status"] for e in entries]
|
|
1081
|
+
graded = [s for s in stated if s != "not-applicable"]
|
|
1082
|
+
if graded:
|
|
1083
|
+
status = min(graded, key=_SEVERITY_RANK.get)
|
|
1084
|
+
if "not-applicable" in stated:
|
|
1085
|
+
gaps.append("conflicting-status")
|
|
1086
|
+
else:
|
|
1087
|
+
status = "not-applicable"
|
|
1088
|
+
if status == "not-applicable":
|
|
1089
|
+
if tailoring in ("applicable", "reduced"):
|
|
1090
|
+
gaps.append("na-conflicts-with-tailoring")
|
|
1091
|
+
if not justification:
|
|
1092
|
+
gaps.append("na-without-justification")
|
|
1093
|
+
else:
|
|
1094
|
+
if not refs:
|
|
1095
|
+
gaps.append("no-evidence-reference")
|
|
1096
|
+
if status == "compliant":
|
|
1097
|
+
status = "partially-compliant"
|
|
1098
|
+
elif any(e["document"] and not e["section"] for e in entries):
|
|
1099
|
+
gaps.append("reference-without-section")
|
|
1100
|
+
if (status in ("partially-compliant", "not-compliant")
|
|
1101
|
+
and not justification):
|
|
1102
|
+
gaps.append("deviation-without-justification")
|
|
1103
|
+
if tailoring == "not-applicable":
|
|
1104
|
+
gaps.append("evidence-for-tailored-out-clause")
|
|
1105
|
+
return {"clause": cid, "title": title, "status": status,
|
|
1106
|
+
"tailoring": tailoring, "justification": justification,
|
|
1107
|
+
"evidence": refs, "documents": docs,
|
|
1108
|
+
"evidence_rows": len(entries), "gaps": gaps, "notes": [],
|
|
1109
|
+
"trace": None}
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def build_matrix(clauses, evidence_rows, category, security_sensitive=False):
|
|
1113
|
+
"""Build the compliance matrix. The result is ALWAYS a draft.
|
|
1114
|
+
|
|
1115
|
+
clauses: list of (id, topic) pairs (default_clauses() for the whole
|
|
1116
|
+
standard) or plain ids.
|
|
1117
|
+
evidence_rows: rows from parse_evidence_csv (or dicts of that shape).
|
|
1118
|
+
category: software criticality category A to D.
|
|
1119
|
+
"""
|
|
1120
|
+
cat = normalise_category(category)
|
|
1121
|
+
index = index_evidence(evidence_rows)
|
|
1122
|
+
listed, seen = [], set()
|
|
1123
|
+
for item in clauses:
|
|
1124
|
+
if isinstance(item, (tuple, list)):
|
|
1125
|
+
cid, title = normalise_clause_id(str(item[0])), str(item[1] or "")
|
|
1126
|
+
else:
|
|
1127
|
+
cid, title = normalise_clause_id(str(item)), ""
|
|
1128
|
+
if cid in seen:
|
|
1129
|
+
raise ValueError("clause %s listed twice" % cid)
|
|
1130
|
+
seen.add(cid)
|
|
1131
|
+
listed.append((cid, title or topic_of(cid)))
|
|
1132
|
+
rows = [_matrix_row(cid, title, index.get(cid, []), cat,
|
|
1133
|
+
security_sensitive) for cid, title in listed]
|
|
1134
|
+
orphans = sorted((cid for cid in index if cid not in seen),
|
|
1135
|
+
key=_clause_key)
|
|
1136
|
+
matrix = {"standard": STANDARD, "category": cat, "status": "DRAFT",
|
|
1137
|
+
"requires_human_sign_off": True, "rows": rows,
|
|
1138
|
+
"orphan_evidence": orphans,
|
|
1139
|
+
"orphan_documents": {cid: [e["document"] for e in index[cid]
|
|
1140
|
+
if e["document"]]
|
|
1141
|
+
for cid in orphans},
|
|
1142
|
+
"trace": {"ran": False}}
|
|
1143
|
+
_refresh(matrix)
|
|
1144
|
+
return matrix
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def coverage_summary(rows, trace_ran=False):
|
|
1148
|
+
"""Counts per status and the three coverage fractions.
|
|
1149
|
+
|
|
1150
|
+
compliant fraction = compliant / applicable; evidenced fraction =
|
|
1151
|
+
applicable clauses carrying a document reference / applicable; traced
|
|
1152
|
+
fraction = applicable clauses with at least one reference that resolves
|
|
1153
|
+
to a real file / applicable (None when no document folder was given).
|
|
1154
|
+
"""
|
|
1155
|
+
counts = {s: 0 for s in STATUSES}
|
|
1156
|
+
with_ref = traced = gapped = 0
|
|
1157
|
+
for row in rows:
|
|
1158
|
+
counts[row["status"]] += 1
|
|
1159
|
+
applicable = row["status"] != "not-applicable"
|
|
1160
|
+
if applicable and row["evidence"]:
|
|
1161
|
+
with_ref += 1
|
|
1162
|
+
if applicable and row.get("trace") and row["trace"]["found"]:
|
|
1163
|
+
traced += 1
|
|
1164
|
+
if row["gaps"]:
|
|
1165
|
+
gapped += 1
|
|
1166
|
+
applicable = len(rows) - counts["not-applicable"]
|
|
1167
|
+
|
|
1168
|
+
def frac(n):
|
|
1169
|
+
return round(n / applicable, 4) if applicable else 1.0
|
|
1170
|
+
return {"clauses": len(rows), "counts": counts, "applicable": applicable,
|
|
1171
|
+
"compliant_fraction": frac(counts["compliant"]),
|
|
1172
|
+
"evidenced_fraction": frac(with_ref),
|
|
1173
|
+
"traced_fraction": frac(traced) if trace_ran else None,
|
|
1174
|
+
"rows_with_gaps": gapped}
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def find_gaps(matrix):
|
|
1178
|
+
"""One entry per (clause, gap kind), with the skill that resolves it.
|
|
1179
|
+
|
|
1180
|
+
Every row gap; every clause not fully compliant (unless it already
|
|
1181
|
+
carries no-evidence); one entry per clause that has evidence but is not
|
|
1182
|
+
in the clause list.
|
|
1183
|
+
"""
|
|
1184
|
+
out = []
|
|
1185
|
+
for row in matrix["rows"]:
|
|
1186
|
+
for kind in row["gaps"]:
|
|
1187
|
+
out.append({"clause": row["clause"], "kind": kind,
|
|
1188
|
+
"status": row["status"],
|
|
1189
|
+
"next_skill": route_gap(kind, row["clause"])})
|
|
1190
|
+
if (row["status"] in ("partially-compliant", "not-compliant")
|
|
1191
|
+
and "no-evidence" not in row["gaps"]):
|
|
1192
|
+
out.append({"clause": row["clause"], "kind": row["status"],
|
|
1193
|
+
"status": row["status"],
|
|
1194
|
+
"next_skill": route_gap(row["status"],
|
|
1195
|
+
row["clause"])})
|
|
1196
|
+
for cid in matrix.get("orphan_evidence", []):
|
|
1197
|
+
out.append({"clause": cid, "kind": "evidence-for-unlisted-clause",
|
|
1198
|
+
"status": None,
|
|
1199
|
+
"next_skill": route_gap("evidence-for-unlisted-clause",
|
|
1200
|
+
cid)})
|
|
1201
|
+
return out
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _refresh(matrix):
|
|
1205
|
+
matrix["coverage"] = coverage_summary(matrix["rows"],
|
|
1206
|
+
matrix["trace"]["ran"])
|
|
1207
|
+
matrix["gaps"] = find_gaps(matrix)
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
# ---------------------------------------------------------------------------
|
|
1211
|
+
# Stage 4: document trace
|
|
1212
|
+
# ---------------------------------------------------------------------------
|
|
1213
|
+
|
|
1214
|
+
def list_documents(folder):
|
|
1215
|
+
"""Every file under a folder, as sorted relative paths ('/' separated)."""
|
|
1216
|
+
if not os.path.isdir(folder):
|
|
1217
|
+
raise ValueError("document folder not found: %s" % folder)
|
|
1218
|
+
out = []
|
|
1219
|
+
for root, dirs, files in os.walk(folder):
|
|
1220
|
+
dirs[:] = sorted(d for d in dirs if not d.startswith("."))
|
|
1221
|
+
for f in sorted(files):
|
|
1222
|
+
if f.startswith("."):
|
|
1223
|
+
continue
|
|
1224
|
+
rel = os.path.relpath(os.path.join(root, f), folder)
|
|
1225
|
+
out.append(rel.replace(os.sep, "/"))
|
|
1226
|
+
return sorted(out)
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def _doc_keys(name):
|
|
1230
|
+
base = str(name).replace("\\", "/").rsplit("/", 1)[-1].strip().lower()
|
|
1231
|
+
stem = base.rsplit(".", 1)[0] if "." in base else base
|
|
1232
|
+
return base, stem
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def resolve_document(document, files):
|
|
1236
|
+
"""The file a cited document resolves to, or None.
|
|
1237
|
+
|
|
1238
|
+
A citation resolves when a file in the folder has the same name, or the
|
|
1239
|
+
same name without its extension ('EX-SPAP-001' -> 'EX-SPAP-001.md').
|
|
1240
|
+
Matching ignores case and the folder a file sits in.
|
|
1241
|
+
"""
|
|
1242
|
+
base, stem = _doc_keys(document)
|
|
1243
|
+
for rel in files:
|
|
1244
|
+
fbase, fstem = _doc_keys(rel)
|
|
1245
|
+
if base in (fbase, fstem) or stem == fbase:
|
|
1246
|
+
return rel
|
|
1247
|
+
return None
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def apply_document_trace(matrix, files, folder_label="docs/"):
|
|
1251
|
+
"""Trace every cited document to a file; raise gaps for the misses.
|
|
1252
|
+
|
|
1253
|
+
A clause whose cited document is missing gets a document-not-found
|
|
1254
|
+
gap; a compliant clause whose every citation is missing is downgraded
|
|
1255
|
+
to partially compliant (a claim resting on no file is not evidence).
|
|
1256
|
+
"""
|
|
1257
|
+
files = list(files)
|
|
1258
|
+
cited, missing = {}, {}
|
|
1259
|
+
for row in matrix["rows"]:
|
|
1260
|
+
found, lost = [], []
|
|
1261
|
+
for doc in row["documents"]:
|
|
1262
|
+
rel = resolve_document(doc, files)
|
|
1263
|
+
cited.setdefault(doc, rel)
|
|
1264
|
+
if rel is None:
|
|
1265
|
+
lost.append(doc)
|
|
1266
|
+
missing.setdefault(doc, []).append(row["clause"])
|
|
1267
|
+
else:
|
|
1268
|
+
found.append(doc)
|
|
1269
|
+
row["trace"] = {"found": found, "missing": lost}
|
|
1270
|
+
if lost and "document-not-found" not in row["gaps"]:
|
|
1271
|
+
row["gaps"].append("document-not-found")
|
|
1272
|
+
if row["status"] == "compliant" and row["documents"] and not found:
|
|
1273
|
+
row["status"] = "partially-compliant"
|
|
1274
|
+
row["notes"].append("downgraded from compliant: no cited "
|
|
1275
|
+
"document was found in the folder")
|
|
1276
|
+
for cid, docs in matrix.get("orphan_documents", {}).items():
|
|
1277
|
+
for doc in docs:
|
|
1278
|
+
rel = resolve_document(doc, files)
|
|
1279
|
+
cited.setdefault(doc, rel)
|
|
1280
|
+
if rel is None:
|
|
1281
|
+
missing.setdefault(doc, []).append(cid)
|
|
1282
|
+
used = set(r for r in cited.values() if r)
|
|
1283
|
+
matrix["trace"] = {
|
|
1284
|
+
"ran": True, "folder": folder_label, "files": len(files),
|
|
1285
|
+
"cited": len(cited),
|
|
1286
|
+
"found": sorted(d for d, r in cited.items() if r),
|
|
1287
|
+
"resolved": {d: r for d, r in sorted(cited.items()) if r},
|
|
1288
|
+
"missing": {d: sorted(set(c), key=_clause_key)
|
|
1289
|
+
for d, c in sorted(missing.items())},
|
|
1290
|
+
"uncited_files": [f for f in files if f not in used],
|
|
1291
|
+
}
|
|
1292
|
+
_refresh(matrix)
|
|
1293
|
+
return matrix
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
# ---------------------------------------------------------------------------
|
|
1297
|
+
# Stage 5: milestone evidence
|
|
1298
|
+
# ---------------------------------------------------------------------------
|
|
1299
|
+
|
|
1300
|
+
def clause_applies(clause, category, security_sensitive=False):
|
|
1301
|
+
"""True when a clause applies (fully or reduced) for a category."""
|
|
1302
|
+
return tailored_status(clause, category, security_sensitive) in (
|
|
1303
|
+
"applicable", "reduced")
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def evidence_due(review, category, scope=()):
|
|
1307
|
+
"""Documents a review owes for a category and scope, with their clauses.
|
|
1308
|
+
|
|
1309
|
+
A document kind is owed when at least one of its driving clauses
|
|
1310
|
+
expects it at this review and applies to the category; kinds tied to a
|
|
1311
|
+
scope (suppliers, procured items, reuse, security, operations) are owed
|
|
1312
|
+
only when the project has that scope.
|
|
1313
|
+
"""
|
|
1314
|
+
rev = normalise_review(review)
|
|
1315
|
+
cat = normalise_category(category)
|
|
1316
|
+
flags = set(normalise_scope(scope))
|
|
1317
|
+
out = []
|
|
1318
|
+
for doc in sorted(DOCUMENT_CLAUSE_REVIEWS):
|
|
1319
|
+
cond = DOCUMENT_CONDITIONS.get(doc)
|
|
1320
|
+
if cond is not None and cond not in flags:
|
|
1321
|
+
continue
|
|
1322
|
+
clauses = [c for c, revs in DOCUMENT_CLAUSE_REVIEWS[doc].items()
|
|
1323
|
+
if rev in revs and clause_applies(c, cat,
|
|
1324
|
+
"security" in flags)]
|
|
1325
|
+
if clauses:
|
|
1326
|
+
out.append({"document": doc, "title": DOCUMENT_TITLES[doc],
|
|
1327
|
+
"clauses": clauses})
|
|
1328
|
+
return out
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def parse_pack_csv(text):
|
|
1332
|
+
"""Parse a review pack: CSV with document (a document kind) + maturity."""
|
|
1333
|
+
reader = csv.DictReader(io.StringIO(str(text)))
|
|
1334
|
+
fields = [str(f).strip().lower() for f in (reader.fieldnames or [])]
|
|
1335
|
+
if "document" not in fields or "maturity" not in fields:
|
|
1336
|
+
raise ValueError("review pack CSV needs 'document' and 'maturity' "
|
|
1337
|
+
"columns (document = a document kind such as spap, "
|
|
1338
|
+
"sdp, scm-plan)")
|
|
1339
|
+
out = []
|
|
1340
|
+
for raw in reader:
|
|
1341
|
+
row = {str(k).strip().lower(): (v or "").strip()
|
|
1342
|
+
for k, v in raw.items() if k is not None}
|
|
1343
|
+
if not row.get("document"):
|
|
1344
|
+
continue
|
|
1345
|
+
out.append({"document": row["document"],
|
|
1346
|
+
"maturity": row.get("maturity", ""),
|
|
1347
|
+
"file": row.get("file", "")})
|
|
1348
|
+
return out
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def assess_review_pack(review, category, submitted, scope=(),
|
|
1352
|
+
required_maturity="issued"):
|
|
1353
|
+
"""Grade a submitted pack against what the review owes.
|
|
1354
|
+
|
|
1355
|
+
Returns owed, missing, immature (below required maturity), unplanned
|
|
1356
|
+
(submitted but not owed at this review) and complete.
|
|
1357
|
+
"""
|
|
1358
|
+
if required_maturity not in _MATURITY:
|
|
1359
|
+
raise ValueError("unknown maturity %r" % (required_maturity,))
|
|
1360
|
+
owed = [d["document"] for d in evidence_due(review, category, scope)]
|
|
1361
|
+
got = {}
|
|
1362
|
+
for item in submitted:
|
|
1363
|
+
doc = str(item.get("document") or "").strip()
|
|
1364
|
+
if doc not in DOCUMENT_TITLES:
|
|
1365
|
+
raise ValueError("unknown document kind %r in the review pack "
|
|
1366
|
+
"(known kinds: %s)"
|
|
1367
|
+
% (doc, ", ".join(sorted(DOCUMENT_TITLES))))
|
|
1368
|
+
if doc in got:
|
|
1369
|
+
raise ValueError("document %s submitted twice" % doc)
|
|
1370
|
+
mat = str(item.get("maturity") or "").strip().lower()
|
|
1371
|
+
if mat not in _MATURITY:
|
|
1372
|
+
raise ValueError("unknown maturity %r for %s (use draft, issued "
|
|
1373
|
+
"or approved)" % (mat, doc))
|
|
1374
|
+
got[doc] = mat
|
|
1375
|
+
missing = [d for d in owed if d not in got]
|
|
1376
|
+
immature = [d for d in owed if d in got
|
|
1377
|
+
and _MATURITY[got[d]] < _MATURITY[required_maturity]]
|
|
1378
|
+
unplanned = sorted(d for d in got if d not in owed)
|
|
1379
|
+
return {"review": normalise_review(review), "owed": owed,
|
|
1380
|
+
"missing": missing, "immature": immature,
|
|
1381
|
+
"unplanned": unplanned,
|
|
1382
|
+
"complete": not missing and not immature}
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
def milestone_check(matrix, review, category, scope=(), pack=None):
|
|
1386
|
+
"""What the review owes, traced to the matrix rows that evidence it."""
|
|
1387
|
+
rev = normalise_review(review)
|
|
1388
|
+
by_clause = {r["clause"]: r for r in matrix["rows"]}
|
|
1389
|
+
trace_ran = matrix["trace"]["ran"]
|
|
1390
|
+
owed = []
|
|
1391
|
+
for item in evidence_due(rev, category, scope):
|
|
1392
|
+
rows = [by_clause.get(c) for c in item["clauses"]]
|
|
1393
|
+
listed = [r for r in rows if r is not None]
|
|
1394
|
+
docs = []
|
|
1395
|
+
for r in listed:
|
|
1396
|
+
for d in (r["trace"]["found"] if trace_ran and r["trace"]
|
|
1397
|
+
else r["documents"]):
|
|
1398
|
+
if d not in docs:
|
|
1399
|
+
docs.append(d)
|
|
1400
|
+
if not listed:
|
|
1401
|
+
state = "not-in-clause-list"
|
|
1402
|
+
elif len(listed) == len(rows) and all(
|
|
1403
|
+
r["status"] == "compliant"
|
|
1404
|
+
and (not trace_ran or r["trace"]["found"]) for r in listed):
|
|
1405
|
+
state = "evidenced"
|
|
1406
|
+
elif any(r["evidence"] for r in listed):
|
|
1407
|
+
state = "partial"
|
|
1408
|
+
else:
|
|
1409
|
+
state = "no-evidence"
|
|
1410
|
+
owed.append(dict(item, state=state, cited=docs))
|
|
1411
|
+
return {"review": rev, "review_name": REVIEW_NAMES[rev],
|
|
1412
|
+
"spap_maturity_due": PLAN_MATURITY_BY_REVIEW.get(rev),
|
|
1413
|
+
"owed": owed,
|
|
1414
|
+
"pack": (assess_review_pack(rev, category, pack, scope)
|
|
1415
|
+
if pack is not None else None)}
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def spamr_inputs(matrix, metrics=None):
|
|
1419
|
+
"""Which milestone-report inputs the evidence can supply, and from what.
|
|
1420
|
+
|
|
1421
|
+
An input is available when an applicable clause of its groups carries
|
|
1422
|
+
evidence that resolves to a real document (or, with no folder given,
|
|
1423
|
+
that cites one). The metrics input is also fed by a graded measurement
|
|
1424
|
+
set.
|
|
1425
|
+
"""
|
|
1426
|
+
trace_ran = matrix["trace"]["ran"]
|
|
1427
|
+
out = {}
|
|
1428
|
+
for key, groups in SPAMR_INPUT_CLAUSES.items():
|
|
1429
|
+
docs = []
|
|
1430
|
+
for r in matrix["rows"]:
|
|
1431
|
+
if r["status"] == "not-applicable":
|
|
1432
|
+
continue
|
|
1433
|
+
if not any(_in_group(r["clause"], g) for g in groups):
|
|
1434
|
+
continue
|
|
1435
|
+
for d in (r["trace"]["found"] if trace_ran and r["trace"]
|
|
1436
|
+
else r["documents"]):
|
|
1437
|
+
if d not in docs:
|
|
1438
|
+
docs.append(d)
|
|
1439
|
+
if key == "metrics" and metrics is not None:
|
|
1440
|
+
docs.append("graded measurement set")
|
|
1441
|
+
out[key] = sorted(docs)
|
|
1442
|
+
return out
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def spamr_skeleton(review, inputs):
|
|
1446
|
+
"""Milestone report (SPAMR) skeleton: section status per available input.
|
|
1447
|
+
|
|
1448
|
+
Status is 'boilerplate' (no project input needed), 'filled' (input
|
|
1449
|
+
available) or 'missing-input'. Always a draft for human review.
|
|
1450
|
+
"""
|
|
1451
|
+
rev = normalise_review(review)
|
|
1452
|
+
sections, missing = [], []
|
|
1453
|
+
for sid, topic, key in SPAMR_SECTIONS:
|
|
1454
|
+
if key is None:
|
|
1455
|
+
status = "boilerplate"
|
|
1456
|
+
elif inputs.get(key):
|
|
1457
|
+
status = "filled"
|
|
1458
|
+
else:
|
|
1459
|
+
status = "missing-input"
|
|
1460
|
+
missing.append(key)
|
|
1461
|
+
sections.append({"id": sid, "topic": topic, "status": status,
|
|
1462
|
+
"input": key,
|
|
1463
|
+
"draws_on": list(inputs.get(key) or [])
|
|
1464
|
+
if key else []})
|
|
1465
|
+
return {"review": rev, "sections": sections, "missing_inputs": missing,
|
|
1466
|
+
"status": "DRAFT - requires human review and sign-off"}
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
# ---------------------------------------------------------------------------
|
|
1470
|
+
# Stage 6: product quality metrics
|
|
1471
|
+
# ---------------------------------------------------------------------------
|
|
1472
|
+
|
|
1473
|
+
def parse_metrics_csv(text):
|
|
1474
|
+
"""Parse a measurement set: CSV with metric,value (blank = not measured)."""
|
|
1475
|
+
reader = csv.DictReader(io.StringIO(str(text)))
|
|
1476
|
+
fields = [str(f).strip().lower() for f in (reader.fieldnames or [])]
|
|
1477
|
+
if "metric" not in fields or "value" not in fields:
|
|
1478
|
+
raise ValueError("metrics CSV needs 'metric' and 'value' columns")
|
|
1479
|
+
out = {}
|
|
1480
|
+
for raw in reader:
|
|
1481
|
+
row = {str(k).strip().lower(): (v or "").strip()
|
|
1482
|
+
for k, v in raw.items() if k is not None}
|
|
1483
|
+
name = row.get("metric", "")
|
|
1484
|
+
if not name:
|
|
1485
|
+
continue
|
|
1486
|
+
value = row.get("value", "")
|
|
1487
|
+
try:
|
|
1488
|
+
out[name] = float(value) if value != "" else None
|
|
1489
|
+
except ValueError:
|
|
1490
|
+
raise ValueError("metric %s has a non-numeric value %r"
|
|
1491
|
+
% (name, value))
|
|
1492
|
+
return out
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def thresholds_for(category, overrides=None):
|
|
1496
|
+
"""{metric: (threshold, source)}; source 'project' for an override."""
|
|
1497
|
+
cat = normalise_category(category)
|
|
1498
|
+
out = {n: (v, "default") for n, v in DEFAULT_THRESHOLDS[cat].items()
|
|
1499
|
+
if v is not None}
|
|
1500
|
+
for name, value in dict(overrides or {}).items():
|
|
1501
|
+
if name not in METRICS:
|
|
1502
|
+
raise ValueError("unknown metric %r" % (name,))
|
|
1503
|
+
if value is None:
|
|
1504
|
+
out.pop(name, None)
|
|
1505
|
+
else:
|
|
1506
|
+
out[name] = (value, "project")
|
|
1507
|
+
return out
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
def evaluate_metrics(measurements, category, overrides=None):
|
|
1511
|
+
"""Grade a measurement set: pass / fail / missing, with the margin."""
|
|
1512
|
+
limits = thresholds_for(category, overrides)
|
|
1513
|
+
unknown = sorted(n for n in measurements if n not in METRICS)
|
|
1514
|
+
rows, failing, missing = [], [], []
|
|
1515
|
+
for name in sorted(limits):
|
|
1516
|
+
threshold, source = limits[name]
|
|
1517
|
+
direction = METRICS[name][2]
|
|
1518
|
+
value = measurements.get(name)
|
|
1519
|
+
if value is None:
|
|
1520
|
+
rows.append({"metric": name, "value": None,
|
|
1521
|
+
"threshold": threshold, "source": source,
|
|
1522
|
+
"direction": direction, "status": "missing",
|
|
1523
|
+
"margin": None})
|
|
1524
|
+
missing.append(name)
|
|
1525
|
+
continue
|
|
1526
|
+
if direction == "max":
|
|
1527
|
+
ok, margin = value <= threshold + TOLERANCE, threshold - value
|
|
1528
|
+
else:
|
|
1529
|
+
ok, margin = value + TOLERANCE >= threshold, value - threshold
|
|
1530
|
+
rows.append({"metric": name, "value": value, "threshold": threshold,
|
|
1531
|
+
"source": source, "direction": direction,
|
|
1532
|
+
"status": "pass" if ok else "fail",
|
|
1533
|
+
"margin": round(margin, 4)})
|
|
1534
|
+
if not ok:
|
|
1535
|
+
failing.append(name)
|
|
1536
|
+
return {"category": normalise_category(category), "rows": rows,
|
|
1537
|
+
"failing": failing, "missing": missing, "unknown": unknown,
|
|
1538
|
+
"verdict": "pass" if not failing and not missing else "fail"}
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
# ---------------------------------------------------------------------------
|
|
1542
|
+
# The deliverable
|
|
1543
|
+
# ---------------------------------------------------------------------------
|
|
1544
|
+
|
|
1545
|
+
class ProjectInputs(object):
|
|
1546
|
+
"""Everything the role is given for one project."""
|
|
1547
|
+
|
|
1548
|
+
def __init__(self, project="", category=None, severity=None,
|
|
1549
|
+
provisions=(), provides_provision_for=None,
|
|
1550
|
+
security_sensitive=False, scope=(), review=None,
|
|
1551
|
+
clauses=None, clause_label="", evidence_text="",
|
|
1552
|
+
evidence_label="evidence.csv", docs_files=None,
|
|
1553
|
+
docs_label="", pack_text=None, pack_label="",
|
|
1554
|
+
metrics_text=None, metrics_label="", example=False):
|
|
1555
|
+
self.project = project
|
|
1556
|
+
self.category = category
|
|
1557
|
+
self.severity = severity
|
|
1558
|
+
self.provisions = list(provisions or ())
|
|
1559
|
+
self.provides_provision_for = provides_provision_for
|
|
1560
|
+
self.security_sensitive = bool(security_sensitive)
|
|
1561
|
+
self.scope = list(scope or ())
|
|
1562
|
+
self.review = review
|
|
1563
|
+
self.clauses = clauses
|
|
1564
|
+
self.clause_label = clause_label
|
|
1565
|
+
self.evidence_text = evidence_text
|
|
1566
|
+
self.evidence_label = evidence_label
|
|
1567
|
+
self.docs_files = docs_files
|
|
1568
|
+
self.docs_label = docs_label
|
|
1569
|
+
self.pack_text = pack_text
|
|
1570
|
+
self.pack_label = pack_label
|
|
1571
|
+
self.metrics_text = metrics_text
|
|
1572
|
+
self.metrics_label = metrics_label
|
|
1573
|
+
self.example = example
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
EXAMPLE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
1577
|
+
"..", "example")
|
|
1578
|
+
EXAMPLE_PROJECT = ("EXAMPLE-SAT on-board software (worked example, "
|
|
1579
|
+
"not a real project)")
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _read(path):
|
|
1583
|
+
with open(path, encoding="utf-8") as f:
|
|
1584
|
+
return f.read()
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def example_item(example_dir=None):
|
|
1588
|
+
"""The bundled worked example: a category-B on-board software at PDR.
|
|
1589
|
+
|
|
1590
|
+
Severity I functions with a hardware compensating provision (so the
|
|
1591
|
+
category derives to B), reuse in scope, an evidence index, a document
|
|
1592
|
+
folder of stub files (one cited document deliberately absent), a PDR
|
|
1593
|
+
review pack and a measurement set.
|
|
1594
|
+
"""
|
|
1595
|
+
d = os.path.normpath(example_dir or EXAMPLE_DIR)
|
|
1596
|
+
return ProjectInputs(
|
|
1597
|
+
project=EXAMPLE_PROJECT, severity="I", provisions=["hardware"],
|
|
1598
|
+
scope=["reuse"], review="pdr",
|
|
1599
|
+
evidence_text=_read(os.path.join(d, "evidence.csv")),
|
|
1600
|
+
evidence_label="example/evidence.csv",
|
|
1601
|
+
docs_files=list_documents(os.path.join(d, "docs")),
|
|
1602
|
+
docs_label="example/docs/",
|
|
1603
|
+
pack_text=_read(os.path.join(d, "pdr-pack.csv")),
|
|
1604
|
+
pack_label="example/pdr-pack.csv",
|
|
1605
|
+
metrics_text=_read(os.path.join(d, "metrics.csv")),
|
|
1606
|
+
metrics_label="example/metrics.csv", example=True)
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def resolve_category(item):
|
|
1610
|
+
"""(category, derivation or None, notes) from the inputs."""
|
|
1611
|
+
notes = []
|
|
1612
|
+
derivation = None
|
|
1613
|
+
if item.severity:
|
|
1614
|
+
derivation = derive_category(item.severity, item.provisions,
|
|
1615
|
+
item.provides_provision_for)
|
|
1616
|
+
if item.category:
|
|
1617
|
+
cat = normalise_category(item.category)
|
|
1618
|
+
if derivation and derivation["category"] != cat:
|
|
1619
|
+
notes.append("the category given (%s) differs from the one the "
|
|
1620
|
+
"severity derives (%s): the human must settle it "
|
|
1621
|
+
"before the matrix is used" % (
|
|
1622
|
+
cat, derivation["category"]))
|
|
1623
|
+
return cat, derivation, notes
|
|
1624
|
+
if derivation:
|
|
1625
|
+
return derivation["category"], derivation, notes
|
|
1626
|
+
raise ValueError("give the software criticality category (--category "
|
|
1627
|
+
"A-D) or the function severity to derive it from "
|
|
1628
|
+
"(--severity I-IV)")
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def build_report(item):
|
|
1632
|
+
"""Build the full deliverable model from the project inputs."""
|
|
1633
|
+
cat, derivation, notes = resolve_category(item)
|
|
1634
|
+
scope = set(normalise_scope(item.scope))
|
|
1635
|
+
security = bool(item.security_sensitive) or "security" in scope
|
|
1636
|
+
if security:
|
|
1637
|
+
scope.add("security")
|
|
1638
|
+
scope = tuple(sorted(scope))
|
|
1639
|
+
clauses = item.clauses if item.clauses is not None else default_clauses()
|
|
1640
|
+
evidence_rows = parse_evidence_csv(item.evidence_text)
|
|
1641
|
+
matrix = build_matrix(clauses, evidence_rows, cat, security)
|
|
1642
|
+
if item.docs_files is not None:
|
|
1643
|
+
apply_document_trace(matrix, item.docs_files,
|
|
1644
|
+
item.docs_label or "docs/")
|
|
1645
|
+
metrics = None
|
|
1646
|
+
if item.metrics_text is not None:
|
|
1647
|
+
metrics = evaluate_metrics(parse_metrics_csv(item.metrics_text), cat)
|
|
1648
|
+
review = normalise_review(item.review) if item.review else None
|
|
1649
|
+
milestone = spamr = None
|
|
1650
|
+
if review:
|
|
1651
|
+
pack = (parse_pack_csv(item.pack_text)
|
|
1652
|
+
if item.pack_text is not None else None)
|
|
1653
|
+
milestone = milestone_check(matrix, review, cat, scope, pack)
|
|
1654
|
+
spamr = spamr_skeleton(review, spamr_inputs(matrix, metrics))
|
|
1655
|
+
return {
|
|
1656
|
+
"role": "software-product-assurance-engineer",
|
|
1657
|
+
"title": TITLE, "standard": STANDARD,
|
|
1658
|
+
"project": item.project or "unnamed project",
|
|
1659
|
+
"example": bool(item.example),
|
|
1660
|
+
"status": "DRAFT", "requires_human_sign_off": True,
|
|
1661
|
+
"banner": DRAFT_BANNER, "stop_line": STOP_LINE,
|
|
1662
|
+
"category": cat, "category_derivation": derivation,
|
|
1663
|
+
"category_notes": notes,
|
|
1664
|
+
"security_sensitive": security, "scope": list(scope),
|
|
1665
|
+
"review": review,
|
|
1666
|
+
"inputs": {
|
|
1667
|
+
"clause_list": item.clause_label or (
|
|
1668
|
+
"the full requirement list of the standard"
|
|
1669
|
+
if item.clauses is None else "customer clause list"),
|
|
1670
|
+
"evidence": item.evidence_label,
|
|
1671
|
+
"evidence_rows": len(evidence_rows),
|
|
1672
|
+
"docs": item.docs_label if item.docs_files is not None else None,
|
|
1673
|
+
"pack": item.pack_label if item.pack_text is not None else None,
|
|
1674
|
+
"metrics": (item.metrics_label
|
|
1675
|
+
if item.metrics_text is not None else None),
|
|
1676
|
+
},
|
|
1677
|
+
"matrix": matrix,
|
|
1678
|
+
"tailoring": tailoring_summary(cat, security),
|
|
1679
|
+
"milestone": milestone, "spamr": spamr, "metrics": metrics,
|
|
1680
|
+
"sign_off": {"signed": False, "signatory": None, "role": None,
|
|
1681
|
+
"date": None, "decision": None},
|
|
1682
|
+
"disclaimer": ("DRAFT for human review. Not an approval, not a "
|
|
1683
|
+
"certification, not a statement of compliance."),
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
# ---------------------------------------------------------------------------
|
|
1688
|
+
# Rendering
|
|
1689
|
+
# ---------------------------------------------------------------------------
|
|
1690
|
+
|
|
1691
|
+
def _cell(text):
|
|
1692
|
+
return str(text).replace("|", "/").replace("\n", " ").strip()
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
def _frac(x):
|
|
1696
|
+
return "not run" if x is None else "%.4f" % x
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
def coverage_line(cov):
|
|
1700
|
+
c = cov["counts"]
|
|
1701
|
+
return ("Coverage: %d clauses, %d applicable; compliant %d, partially %d, "
|
|
1702
|
+
"not compliant %d, not applicable %d; compliant fraction %s; "
|
|
1703
|
+
"evidenced fraction %s; traced fraction %s." % (
|
|
1704
|
+
cov["clauses"], cov["applicable"], c["compliant"],
|
|
1705
|
+
c["partially-compliant"], c["not-compliant"],
|
|
1706
|
+
c["not-applicable"], _frac(cov["compliant_fraction"]),
|
|
1707
|
+
_frac(cov["evidenced_fraction"]),
|
|
1708
|
+
_frac(cov["traced_fraction"])))
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
def render_report_markdown(model):
|
|
1712
|
+
"""Render the deliverable: banner, scope, coverage, matrix, gaps, trace,
|
|
1713
|
+
tailoring, milestone, SPAMR skeleton, metrics, boundaries, stop line."""
|
|
1714
|
+
m = model["matrix"]
|
|
1715
|
+
cov = m["coverage"]
|
|
1716
|
+
L = ["# " + model["title"], "", "> " + model["banner"], "",
|
|
1717
|
+
"Status: %s. Human sign-off required." % model["status"], ""]
|
|
1718
|
+
|
|
1719
|
+
# 1. scope
|
|
1720
|
+
L += ["## 1. Scope and basis", ""]
|
|
1721
|
+
L.append("- Project: %s" % model["project"])
|
|
1722
|
+
L.append("- Standard: %s, referenced summary-only (clause identifiers "
|
|
1723
|
+
"with topic labels of our own wording)" % model["standard"])
|
|
1724
|
+
L.append("- Software criticality category: **%s**" % model["category"])
|
|
1725
|
+
der = model["category_derivation"]
|
|
1726
|
+
if der:
|
|
1727
|
+
L.append("- Category derivation: %s" % "; ".join(der["rationale"]))
|
|
1728
|
+
for c in der["constraints"]:
|
|
1729
|
+
L.append("- Constraint: %s" % c)
|
|
1730
|
+
for n in model["category_notes"]:
|
|
1731
|
+
L.append("- Open point: %s" % n)
|
|
1732
|
+
L.append("- Security sensitive: %s" % (
|
|
1733
|
+
"yes" if model["security_sensitive"] else "no"))
|
|
1734
|
+
L.append("- Project scope: %s" % (", ".join(model["scope"]) or
|
|
1735
|
+
"no conditional scope"))
|
|
1736
|
+
L.append("- Target review: %s" % (
|
|
1737
|
+
REVIEW_NAMES[model["review"]] if model["review"] else "none given"))
|
|
1738
|
+
inp = model["inputs"]
|
|
1739
|
+
L.append("- Clause list: %s (%d clauses)" % (inp["clause_list"],
|
|
1740
|
+
cov["clauses"]))
|
|
1741
|
+
L.append("- Evidence index: %s (%d rows)" % (inp["evidence"],
|
|
1742
|
+
inp["evidence_rows"]))
|
|
1743
|
+
L.append("- Document folder: %s" % (
|
|
1744
|
+
"%s (%d files)" % (inp["docs"], m["trace"]["files"])
|
|
1745
|
+
if inp["docs"] else "not given, so no citation was traced to a file"))
|
|
1746
|
+
L.append("- Review pack: %s" % (inp["pack"] or "not given"))
|
|
1747
|
+
L.append("- Measurement set: %s" % (inp["metrics"] or "not given"))
|
|
1748
|
+
L.append("")
|
|
1749
|
+
|
|
1750
|
+
# 2. coverage
|
|
1751
|
+
c = cov["counts"]
|
|
1752
|
+
L += ["## 2. Coverage summary", "", coverage_line(cov), "",
|
|
1753
|
+
"| Measure | Value |", "|---|---|",
|
|
1754
|
+
"| Clauses in the matrix | %d |" % cov["clauses"],
|
|
1755
|
+
"| Applicable clauses | %d |" % cov["applicable"],
|
|
1756
|
+
"| Compliant | %d |" % c["compliant"],
|
|
1757
|
+
"| Partially compliant | %d |" % c["partially-compliant"],
|
|
1758
|
+
"| Not compliant (including no evidence) | %d |" % c["not-compliant"],
|
|
1759
|
+
"| Not applicable | %d |" % c["not-applicable"],
|
|
1760
|
+
"| Compliant fraction of applicable clauses | %s |"
|
|
1761
|
+
% _frac(cov["compliant_fraction"]),
|
|
1762
|
+
"| Applicable clauses with a document reference | %s |"
|
|
1763
|
+
% _frac(cov["evidenced_fraction"]),
|
|
1764
|
+
"| Applicable clauses traced to a real file | %s |"
|
|
1765
|
+
% _frac(cov["traced_fraction"]),
|
|
1766
|
+
"| Rows with at least one gap | %d |" % cov["rows_with_gaps"],
|
|
1767
|
+
"", "Open gaps: %d." % len(m["gaps"]), ""]
|
|
1768
|
+
|
|
1769
|
+
# 3. matrix
|
|
1770
|
+
L += ["## 3. Compliance matrix", "",
|
|
1771
|
+
"| Clause | Topic | Tailoring | Status | Justification | Evidence "
|
|
1772
|
+
"| Gaps |", "|---|---|---|---|---|---|---|"]
|
|
1773
|
+
for r in m["rows"]:
|
|
1774
|
+
just = r["justification"]
|
|
1775
|
+
if r["notes"]:
|
|
1776
|
+
just = "; ".join([just] + r["notes"]) if just else "; ".join(
|
|
1777
|
+
r["notes"])
|
|
1778
|
+
L.append("| %s | %s | %s | %s | %s | %s | %s |" % (
|
|
1779
|
+
r["clause"], _cell(r["title"]), r["tailoring"], r["status"],
|
|
1780
|
+
_cell(just), _cell("; ".join(r["evidence"])),
|
|
1781
|
+
_cell(", ".join(r["gaps"]))))
|
|
1782
|
+
L.append("")
|
|
1783
|
+
|
|
1784
|
+
# 4. gaps
|
|
1785
|
+
L += ["## 4. Gaps", ""]
|
|
1786
|
+
if m["gaps"]:
|
|
1787
|
+
L += ["Each gap names the bound skill that resolves it.", "",
|
|
1788
|
+
"| # | Clause | Gap | Status | Next skill |",
|
|
1789
|
+
"|---|---|---|---|---|"]
|
|
1790
|
+
for i, g in enumerate(m["gaps"], 1):
|
|
1791
|
+
L.append("| %d | %s | %s | %s | %s |" % (
|
|
1792
|
+
i, g["clause"], g["kind"], g["status"] or "not in list",
|
|
1793
|
+
g["next_skill"].rsplit("/", 1)[-1]))
|
|
1794
|
+
else:
|
|
1795
|
+
L.append("No open gaps.")
|
|
1796
|
+
if m["orphan_evidence"]:
|
|
1797
|
+
L += ["", "Evidence was given for clauses that are not in the clause "
|
|
1798
|
+
"list (a mistyped id, or an id from an older revision): %s."
|
|
1799
|
+
% ", ".join(m["orphan_evidence"])]
|
|
1800
|
+
L.append("")
|
|
1801
|
+
|
|
1802
|
+
# 5. trace
|
|
1803
|
+
L += ["## 5. Document trace", ""]
|
|
1804
|
+
t = m["trace"]
|
|
1805
|
+
if t["ran"]:
|
|
1806
|
+
L.append("Folder %s holds %d files; the evidence cites %d documents, "
|
|
1807
|
+
"%d of which resolve to a file." % (
|
|
1808
|
+
t["folder"], t["files"], t["cited"], len(t["found"])))
|
|
1809
|
+
L.append("")
|
|
1810
|
+
if t["missing"]:
|
|
1811
|
+
L += ["| Cited document not found | Cited by clauses |",
|
|
1812
|
+
"|---|---|"]
|
|
1813
|
+
for doc, cl in t["missing"].items():
|
|
1814
|
+
L.append("| %s | %s |" % (_cell(doc), ", ".join(cl)))
|
|
1815
|
+
L.append("")
|
|
1816
|
+
else:
|
|
1817
|
+
L += ["Every cited document resolves to a file.", ""]
|
|
1818
|
+
if t["resolved"]:
|
|
1819
|
+
L += ["| Cited document | File |", "|---|---|"]
|
|
1820
|
+
for doc, rel in t["resolved"].items():
|
|
1821
|
+
L.append("| %s | %s |" % (_cell(doc), _cell(rel)))
|
|
1822
|
+
L.append("")
|
|
1823
|
+
if t["uncited_files"]:
|
|
1824
|
+
L += ["Files no evidence row cites: %s." % ", ".join(
|
|
1825
|
+
t["uncited_files"]), ""]
|
|
1826
|
+
else:
|
|
1827
|
+
L += ["Not run: no document folder was given, so the references in "
|
|
1828
|
+
"the matrix are claims that have not been traced to files. "
|
|
1829
|
+
"Run the build again with the project's document folder.", ""]
|
|
1830
|
+
|
|
1831
|
+
# 6. tailoring
|
|
1832
|
+
tl = model["tailoring"]
|
|
1833
|
+
L += ["## 6. Tailoring for category %s" % tl["category"], "",
|
|
1834
|
+
"| Group | Topic | Applicable | Reduced | Not applicable |",
|
|
1835
|
+
"|---|---|---|---|---|"]
|
|
1836
|
+
for g in tl["groups"]:
|
|
1837
|
+
L.append("| %s | %s | %d | %d | %d |" % (
|
|
1838
|
+
g["group"], _cell(g["topic"]), g["applicable"], g["reduced"],
|
|
1839
|
+
g["not-applicable"]))
|
|
1840
|
+
tt = tl["totals"]
|
|
1841
|
+
L += ["", "Total: %d applicable, %d reduced, %d not applicable. The "
|
|
1842
|
+
"security clauses follow the security sensitivity (%s), not the "
|
|
1843
|
+
"category." % (tt["applicable"], tt["reduced"],
|
|
1844
|
+
tt["not-applicable"],
|
|
1845
|
+
"yes" if tl["security_sensitive"] else "no"), ""]
|
|
1846
|
+
if tl["reduced"]:
|
|
1847
|
+
L += ["Reduced for this category:", ""]
|
|
1848
|
+
for r in tl["reduced"]:
|
|
1849
|
+
L.append("- %s: %s" % (r["clause"], r["note"]))
|
|
1850
|
+
L.append("")
|
|
1851
|
+
|
|
1852
|
+
# 7. milestone
|
|
1853
|
+
ms = model["milestone"]
|
|
1854
|
+
L += ["## 7. Milestone evidence check", ""]
|
|
1855
|
+
if ms:
|
|
1856
|
+
L.append("Review: %s. Software product assurance plan (SPAP) "
|
|
1857
|
+
"maturity owed: %s." % (
|
|
1858
|
+
ms["review_name"],
|
|
1859
|
+
ms["spap_maturity_due"] or "not set for this review"))
|
|
1860
|
+
L += ["", "| Document owed | Driving clauses | Evidence state | "
|
|
1861
|
+
"Documents cited |", "|---|---|---|---|"]
|
|
1862
|
+
for o in ms["owed"]:
|
|
1863
|
+
L.append("| %s | %s | %s | %s |" % (
|
|
1864
|
+
_cell(o["title"]), ", ".join(o["clauses"]), o["state"],
|
|
1865
|
+
_cell(", ".join(o["cited"]) or "none")))
|
|
1866
|
+
L.append("")
|
|
1867
|
+
pk = ms["pack"]
|
|
1868
|
+
if pk:
|
|
1869
|
+
L += ["Review pack against what is owed: %d owed; missing: %s; "
|
|
1870
|
+
"below issued maturity: %s; submitted but not owed at this "
|
|
1871
|
+
"review: %s; pack complete: %s." % (
|
|
1872
|
+
len(pk["owed"]), ", ".join(pk["missing"]) or "none",
|
|
1873
|
+
", ".join(pk["immature"]) or "none",
|
|
1874
|
+
", ".join(pk["unplanned"]) or "none",
|
|
1875
|
+
"yes" if pk["complete"] else "no"), ""]
|
|
1876
|
+
else:
|
|
1877
|
+
L += ["No review pack was given, so only the clause evidence was "
|
|
1878
|
+
"checked.", ""]
|
|
1879
|
+
else:
|
|
1880
|
+
L += ["No target review was given.", ""]
|
|
1881
|
+
|
|
1882
|
+
# 8. SPAMR
|
|
1883
|
+
sp = model["spamr"]
|
|
1884
|
+
L += ["## 8. Milestone report skeleton (SPAMR)", ""]
|
|
1885
|
+
if sp:
|
|
1886
|
+
L += ["Software product assurance milestone report (SPAMR) for the "
|
|
1887
|
+
"%s, %s." % (REVIEW_NAMES[sp["review"]], sp["status"]), "",
|
|
1888
|
+
"| Section | Topic | Status | Draws on |", "|---|---|---|---|"]
|
|
1889
|
+
for s in sp["sections"]:
|
|
1890
|
+
L.append("| %s | %s | %s | %s |" % (
|
|
1891
|
+
s["id"], _cell(s["topic"]), s["status"],
|
|
1892
|
+
_cell(", ".join(s["draws_on"])) or "-"))
|
|
1893
|
+
L += ["", "Inputs still missing: %s." % (
|
|
1894
|
+
", ".join(sp["missing_inputs"]) or "none"), ""]
|
|
1895
|
+
else:
|
|
1896
|
+
L += ["No target review was given.", ""]
|
|
1897
|
+
|
|
1898
|
+
# 9. metrics
|
|
1899
|
+
mt = model["metrics"]
|
|
1900
|
+
L += ["## 9. Product quality metrics", ""]
|
|
1901
|
+
if mt:
|
|
1902
|
+
L += ["| Metric | Value | Threshold | Source | Direction | Status | "
|
|
1903
|
+
"Margin |", "|---|---|---|---|---|---|---|"]
|
|
1904
|
+
for r in mt["rows"]:
|
|
1905
|
+
L.append("| %s | %s | %s | %s | %s | %s | %s |" % (
|
|
1906
|
+
r["metric"], "-" if r["value"] is None else "%g" % r["value"],
|
|
1907
|
+
"%g" % r["threshold"], r["source"], r["direction"],
|
|
1908
|
+
r["status"],
|
|
1909
|
+
"-" if r["margin"] is None else "%g" % r["margin"]))
|
|
1910
|
+
L += ["", "Verdict: %s (failing: %s; not measured: %s). The standard "
|
|
1911
|
+
"fixes no threshold values: the defaults are illustrative and "
|
|
1912
|
+
"the values agreed in the contract and the plan replace them."
|
|
1913
|
+
% (mt["verdict"], ", ".join(mt["failing"]) or "none",
|
|
1914
|
+
", ".join(mt["missing"]) or "none"), ""]
|
|
1915
|
+
if mt["unknown"]:
|
|
1916
|
+
L += ["Metrics not in the catalogue (not graded): %s."
|
|
1917
|
+
% ", ".join(mt["unknown"]), ""]
|
|
1918
|
+
else:
|
|
1919
|
+
L += ["No measurement set was given.", ""]
|
|
1920
|
+
|
|
1921
|
+
# 10. boundaries
|
|
1922
|
+
L += ["## 10. Limitations and boundaries", "",
|
|
1923
|
+
"- This matrix is a DRAFT prepared by an agent. It is not an "
|
|
1924
|
+
"approval, not a certification and not a statement of compliance.",
|
|
1925
|
+
"- Statuses are those the evidence index claims, combined to the "
|
|
1926
|
+
"weakest per clause and downgraded where a reference is missing. "
|
|
1927
|
+
"The role checks that each cited document exists; it does not "
|
|
1928
|
+
"judge what the document says.",
|
|
1929
|
+
"- A not-applicable claim on a clause the tailoring keeps is a "
|
|
1930
|
+
"deviation and needs the customer's agreement.",
|
|
1931
|
+
"- The standard is referenced, never reproduced: clause "
|
|
1932
|
+
"identifiers are facts, topic labels are our own wording."]
|
|
1933
|
+
if model["example"]:
|
|
1934
|
+
L += ["- Worked example: the project, documents and evidence are "
|
|
1935
|
+
"invented to show the deliverable. Nothing here describes a "
|
|
1936
|
+
"real mission."]
|
|
1937
|
+
L.append("")
|
|
1938
|
+
|
|
1939
|
+
# 11. sign-off
|
|
1940
|
+
L += ["## 11. Human sign-off", "", NOT_SIGNED,
|
|
1941
|
+
"The responsible human records the decision (see ROLE.md, "
|
|
1942
|
+
"Human sign-off). Approving over open gaps has to list them as "
|
|
1943
|
+
"accepted.", "", model["stop_line"]]
|
|
1944
|
+
return "\n".join(L) + "\n"
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
def render_matrix_csv(model):
|
|
1948
|
+
"""The matrix as CSV: DRAFT on every row, the stop line as the last row."""
|
|
1949
|
+
buf = io.StringIO()
|
|
1950
|
+
w = csv.writer(buf, lineterminator="\n")
|
|
1951
|
+
w.writerow(["clause", "topic", "tailoring", "status", "justification",
|
|
1952
|
+
"evidence", "documents_found", "documents_missing", "gaps",
|
|
1953
|
+
"next_skill", "matrix_status"])
|
|
1954
|
+
for r in model["matrix"]["rows"]:
|
|
1955
|
+
tr = r["trace"] or {}
|
|
1956
|
+
just = "; ".join(([r["justification"]] if r["justification"] else [])
|
|
1957
|
+
+ r["notes"])
|
|
1958
|
+
w.writerow([r["clause"], r["title"], r["tailoring"], r["status"],
|
|
1959
|
+
just, "; ".join(r["evidence"]),
|
|
1960
|
+
"; ".join(tr.get("found", [])),
|
|
1961
|
+
"; ".join(tr.get("missing", [])),
|
|
1962
|
+
", ".join(r["gaps"]),
|
|
1963
|
+
route_gap(r["gaps"][0], r["clause"]).rsplit("/", 1)[-1]
|
|
1964
|
+
if r["gaps"] else "",
|
|
1965
|
+
model["status"]])
|
|
1966
|
+
w.writerow(["STOP", model["stop_line"], "", "", "", "", "", "", "", "",
|
|
1967
|
+
model["status"]])
|
|
1968
|
+
return buf.getvalue()
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
def example_report_markdown():
|
|
1972
|
+
return render_report_markdown(build_report(example_item()))
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
# ---------------------------------------------------------------------------
|
|
1976
|
+
# Gates
|
|
1977
|
+
# ---------------------------------------------------------------------------
|
|
1978
|
+
|
|
1979
|
+
def check_report(model):
|
|
1980
|
+
"""Evidence gates on the content model. True = pass."""
|
|
1981
|
+
m = model["matrix"]
|
|
1982
|
+
rows = m["rows"]
|
|
1983
|
+
ids = [r["clause"] for r in rows]
|
|
1984
|
+
gates = {}
|
|
1985
|
+
gates["draft_status"] = (model["status"] == "DRAFT"
|
|
1986
|
+
and model["requires_human_sign_off"] is True
|
|
1987
|
+
and m["status"] == "DRAFT"
|
|
1988
|
+
and not model["sign_off"]["signed"])
|
|
1989
|
+
gates["category_resolved"] = model["category"] in CATEGORIES
|
|
1990
|
+
gates["one_row_per_clause"] = (bool(rows) and len(set(ids)) == len(ids)
|
|
1991
|
+
and all(r["status"] in STATUSES
|
|
1992
|
+
for r in rows))
|
|
1993
|
+
gates["no_silent_compliance"] = all(
|
|
1994
|
+
r["evidence_rows"] > 0
|
|
1995
|
+
or (r["status"] == "not-applicable"
|
|
1996
|
+
and r["tailoring"] == "not-applicable")
|
|
1997
|
+
or (r["status"] == "not-compliant" and "no-evidence" in r["gaps"])
|
|
1998
|
+
for r in rows)
|
|
1999
|
+
gates["compliant_rows_referenced"] = all(
|
|
2000
|
+
r["evidence"] and (not m["trace"]["ran"] or r["trace"]["found"])
|
|
2001
|
+
for r in rows if r["status"] == "compliant")
|
|
2002
|
+
gates["coverage_consistent"] = (
|
|
2003
|
+
m["coverage"] == coverage_summary(rows, m["trace"]["ran"]))
|
|
2004
|
+
gates["gaps_consistent"] = m["gaps"] == find_gaps(m)
|
|
2005
|
+
if m["trace"]["ran"]:
|
|
2006
|
+
flagged = set(c for r in rows if "document-not-found" in r["gaps"]
|
|
2007
|
+
for c in [r["clause"]])
|
|
2008
|
+
gates["trace_honest"] = all(
|
|
2009
|
+
c in flagged or c in m["orphan_evidence"]
|
|
2010
|
+
for cl in m["trace"]["missing"].values() for c in cl)
|
|
2011
|
+
else:
|
|
2012
|
+
gates["trace_honest"] = m["coverage"]["traced_fraction"] is None
|
|
2013
|
+
ms = model["milestone"]
|
|
2014
|
+
gates["milestone_consistent"] = (ms is None) or (
|
|
2015
|
+
bool(ms["owed"]) and all(o["state"] in (
|
|
2016
|
+
"evidenced", "partial", "no-evidence", "not-in-clause-list")
|
|
2017
|
+
for o in ms["owed"]))
|
|
2018
|
+
gates["all_pass"] = all(gates.values())
|
|
2019
|
+
return gates
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
_COVERAGE_RE = re.compile(
|
|
2023
|
+
r"^Coverage: (\d+) clauses, (\d+) applicable; compliant (\d+), "
|
|
2024
|
+
r"partially (\d+), not compliant (\d+), not applicable (\d+);", re.M)
|
|
2025
|
+
_ROW_RE = re.compile(r"^\| (\d+(?:\.\d+)*) \| [^|]* \| ([a-z-]+) \| "
|
|
2026
|
+
r"([a-z-]+) \|", re.M)
|
|
2027
|
+
_ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
def read_sign_off(md):
|
|
2031
|
+
"""Parse the human sign-off block, if one was appended.
|
|
2032
|
+
|
|
2033
|
+
Returns {'state': 'unsigned'} or {'state': 'signed', fields..., 'errors'}.
|
|
2034
|
+
"""
|
|
2035
|
+
found = {}
|
|
2036
|
+
for line in md.splitlines():
|
|
2037
|
+
for key in SIGN_OFF_KEYS:
|
|
2038
|
+
if line.startswith(key + ":"):
|
|
2039
|
+
found[key] = line[len(key) + 1:].strip()
|
|
2040
|
+
if not found:
|
|
2041
|
+
return {"state": "unsigned"}
|
|
2042
|
+
errors = []
|
|
2043
|
+
for key in SIGN_OFF_KEYS[:4]:
|
|
2044
|
+
if not found.get(key):
|
|
2045
|
+
errors.append("%s is missing or empty" % key)
|
|
2046
|
+
if found.get("Sign-off-date") and not _ISO_DATE.match(
|
|
2047
|
+
found["Sign-off-date"]):
|
|
2048
|
+
errors.append("Sign-off-date must be YYYY-MM-DD")
|
|
2049
|
+
decision = found.get("Sign-off-decision", "").lower()
|
|
2050
|
+
if decision and decision not in ("approved", "rejected"):
|
|
2051
|
+
errors.append("Sign-off-decision must be approved or rejected")
|
|
2052
|
+
return {"state": "signed", "signatory": found.get("Signed-off-by"),
|
|
2053
|
+
"role": found.get("Sign-off-role"),
|
|
2054
|
+
"date": found.get("Sign-off-date"), "decision": decision,
|
|
2055
|
+
"accepted_open_gaps": found.get("Accepted-open-gaps"),
|
|
2056
|
+
"errors": errors}
|
|
2057
|
+
|
|
2058
|
+
|
|
2059
|
+
def _section(md, heading):
|
|
2060
|
+
"""The text of one '## ' section of the Markdown, or ''."""
|
|
2061
|
+
if heading not in md:
|
|
2062
|
+
return ""
|
|
2063
|
+
return md.split(heading, 1)[1].split("\n## ", 1)[0]
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
def check_report_markdown(md):
|
|
2067
|
+
"""Gate-check a rendered (or human-signed) matrix. True = pass."""
|
|
2068
|
+
gates = {}
|
|
2069
|
+
low = md.lower()
|
|
2070
|
+
gates["title_present"] = TITLE in md
|
|
2071
|
+
gates["draft_marked"] = DRAFT_BANNER in md and "not an approval" in low
|
|
2072
|
+
gates["category_stated"] = bool(re.search(
|
|
2073
|
+
r"Software criticality category: \*\*[ABCD]\*\*", md))
|
|
2074
|
+
rows = _ROW_RE.findall(_section(md, "## 3. Compliance matrix"))
|
|
2075
|
+
gates["matrix_present"] = bool(rows) and all(
|
|
2076
|
+
s in STATUSES for _, _, s in rows)
|
|
2077
|
+
counts = {s: 0 for s in STATUSES}
|
|
2078
|
+
for _, _, s in rows:
|
|
2079
|
+
if s in counts:
|
|
2080
|
+
counts[s] += 1
|
|
2081
|
+
cov = _COVERAGE_RE.search(md)
|
|
2082
|
+
gates["coverage_matches_rows"] = bool(cov) and [
|
|
2083
|
+
int(x) for x in cov.groups()] == [
|
|
2084
|
+
len(rows), len(rows) - counts["not-applicable"], counts["compliant"],
|
|
2085
|
+
counts["partially-compliant"], counts["not-compliant"],
|
|
2086
|
+
counts["not-applicable"]]
|
|
2087
|
+
gap_sec = _section(md, "## 4. Gaps")
|
|
2088
|
+
gap_rows = len(re.findall(r"^\| \d+ \| ", gap_sec, re.M))
|
|
2089
|
+
og = re.search(r"^Open gaps: (\d+)\.", md, re.M)
|
|
2090
|
+
open_gaps = int(og.group(1)) if og else None
|
|
2091
|
+
gates["open_gaps_match"] = og is not None and open_gaps == gap_rows
|
|
2092
|
+
so = read_sign_off(md)
|
|
2093
|
+
if so["state"] == "unsigned":
|
|
2094
|
+
gates["sign_off_honest"] = (STOP_LINE in md and NOT_SIGNED in md
|
|
2095
|
+
and "Status: DRAFT." in md)
|
|
2096
|
+
else:
|
|
2097
|
+
ok = not so["errors"]
|
|
2098
|
+
if ok and so["decision"] == "approved" and open_gaps:
|
|
2099
|
+
acc = so.get("accepted_open_gaps") or ""
|
|
2100
|
+
ok = acc.isdigit() and int(acc) == open_gaps
|
|
2101
|
+
if not ok:
|
|
2102
|
+
so["errors"].append(
|
|
2103
|
+
"approved over %d open gaps without Accepted-open-gaps: "
|
|
2104
|
+
"%d" % (open_gaps, open_gaps))
|
|
2105
|
+
gates["sign_off_honest"] = ok
|
|
2106
|
+
gates["all_pass"] = all(gates.values())
|
|
2107
|
+
gates["sign_off"] = so
|
|
2108
|
+
return gates
|