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,542 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cli.py - run the Software Product Assurance Engineer role.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 cli.py build --out matrix.md
|
|
6
|
+
# the bundled worked example (example/): category B on-board
|
|
7
|
+
# software at PDR, evidence index, stub documents, review pack,
|
|
8
|
+
# measurement set
|
|
9
|
+
python3 cli.py build --out matrix.md --evidence evidence.csv \\
|
|
10
|
+
--category B [--docs DIR] [--review PDR] [--pack pack.csv] \\
|
|
11
|
+
[--metrics metrics.csv] [--scope reuse,suppliers] \\
|
|
12
|
+
[--security-sensitive] [--clauses customer-list.csv] [--bundle]
|
|
13
|
+
# a real project; --severity I --provision hardware derives the
|
|
14
|
+
# category instead of --category
|
|
15
|
+
python3 cli.py check --file matrix.md
|
|
16
|
+
# gate-check a matrix (a draft, or one a human has signed)
|
|
17
|
+
|
|
18
|
+
Every output is a DRAFT that ends at the stop line: human sign-off
|
|
19
|
+
required. The role never marks a matrix approved.
|
|
20
|
+
|
|
21
|
+
The role ENGINE (core/software_product_assurance_core.py) does the work
|
|
22
|
+
standalone. When AEROSKILLS_DEV points at an aero-agent-skills checkout or
|
|
23
|
+
installed package, the CLI also dispatches the bound q80 leaves and
|
|
24
|
+
cross-checks the engine against them (recorded in provenance.json).
|
|
25
|
+
|
|
26
|
+
Author: ashfordeOU
|
|
27
|
+
"""
|
|
28
|
+
import argparse
|
|
29
|
+
import importlib.util
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
34
|
+
"core"))
|
|
35
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
|
36
|
+
"..", "..", "scripts"))
|
|
37
|
+
import software_product_assurance_core as core # noqa: E402
|
|
38
|
+
import evidence # noqa: E402
|
|
39
|
+
|
|
40
|
+
ROLE_SLUG = "software-product-assurance-engineer"
|
|
41
|
+
DELIVERABLE = "ECSS-Q-ST-80C Rev.2 software product assurance compliance matrix"
|
|
42
|
+
ECSS = "space-systems/ecss/"
|
|
43
|
+
Q80_LEAVES = (
|
|
44
|
+
"q80-compliance-matrix",
|
|
45
|
+
"q80-software-criticality-tailoring",
|
|
46
|
+
"q80-milestone-assurance-evidence",
|
|
47
|
+
"q80-software-product-assurance-plan",
|
|
48
|
+
"q80-software-product-quality-metrics",
|
|
49
|
+
"q80-software-process-assurance",
|
|
50
|
+
)
|
|
51
|
+
HOW_TO_POINT = (
|
|
52
|
+
"To cross-check against the bound skills, point AEROSKILLS_DEV at an "
|
|
53
|
+
"aero-agent-skills checkout, or at an installed package that carries "
|
|
54
|
+
"the q80 leaves:\n"
|
|
55
|
+
" npm install aero-agent-skills\n"
|
|
56
|
+
" export AEROSKILLS_DEV=\"$(npm root)/aero-agent-skills\"\n"
|
|
57
|
+
" (a global install: export AEROSKILLS_DEV=\"$(npm root -g)/"
|
|
58
|
+
"aero-agent-skills\")")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class InputError(Exception):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# inputs
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def _read(path, what):
|
|
70
|
+
if not os.path.isfile(path):
|
|
71
|
+
raise InputError("%s not found: %s" % (what, path))
|
|
72
|
+
with open(path, encoding="utf-8-sig") as f:
|
|
73
|
+
return f.read()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _label(path, folder=False):
|
|
77
|
+
"""A basename label: absolute paths never reach the deliverable."""
|
|
78
|
+
base = os.path.basename(os.path.normpath(path))
|
|
79
|
+
return base + "/" if folder else base
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _flags(value):
|
|
83
|
+
out = []
|
|
84
|
+
for part in (value or "").replace(";", ",").split(","):
|
|
85
|
+
if part.strip():
|
|
86
|
+
out.append(part.strip())
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def gather(args):
|
|
91
|
+
"""Build the ProjectInputs from the command line (or the example)."""
|
|
92
|
+
if not args.evidence:
|
|
93
|
+
if args.docs or args.pack or args.metrics or args.clauses:
|
|
94
|
+
raise InputError("--docs, --pack, --metrics and --clauses need "
|
|
95
|
+
"--evidence (without --evidence the bundled "
|
|
96
|
+
"worked example is used)")
|
|
97
|
+
item = core.example_item()
|
|
98
|
+
if args.category:
|
|
99
|
+
item.category, item.severity = args.category, None
|
|
100
|
+
if args.severity:
|
|
101
|
+
item.severity, item.category = args.severity, None
|
|
102
|
+
item.provisions = args.provision or []
|
|
103
|
+
if args.review:
|
|
104
|
+
item.review = args.review
|
|
105
|
+
if args.scope:
|
|
106
|
+
item.scope = _flags(args.scope)
|
|
107
|
+
if args.security_sensitive:
|
|
108
|
+
item.security_sensitive = True
|
|
109
|
+
return item
|
|
110
|
+
if not (args.category or args.severity):
|
|
111
|
+
raise InputError("give the software criticality category "
|
|
112
|
+
"(--category A-D) or the function severity to "
|
|
113
|
+
"derive it (--severity I-IV [--provision hardware])")
|
|
114
|
+
item = core.ProjectInputs(
|
|
115
|
+
project=args.project or "",
|
|
116
|
+
category=args.category or None, severity=args.severity or None,
|
|
117
|
+
provisions=args.provision or [],
|
|
118
|
+
provides_provision_for=args.provides_provision_for or None,
|
|
119
|
+
security_sensitive=args.security_sensitive,
|
|
120
|
+
scope=_flags(args.scope), review=args.review or None,
|
|
121
|
+
evidence_text=_read(args.evidence, "evidence CSV"),
|
|
122
|
+
evidence_label=_label(args.evidence))
|
|
123
|
+
if args.clauses:
|
|
124
|
+
try:
|
|
125
|
+
item.clauses = core.parse_clause_list(
|
|
126
|
+
_read(args.clauses, "clause list"))
|
|
127
|
+
except ValueError as e:
|
|
128
|
+
raise InputError("clause list %s: %s" % (args.clauses, e))
|
|
129
|
+
item.clause_label = "customer clause list %s" % _label(args.clauses)
|
|
130
|
+
if args.docs:
|
|
131
|
+
if not os.path.isdir(args.docs):
|
|
132
|
+
raise InputError("document folder not found: %s" % args.docs)
|
|
133
|
+
item.docs_files = core.list_documents(args.docs)
|
|
134
|
+
item.docs_label = _label(args.docs, folder=True)
|
|
135
|
+
if args.pack:
|
|
136
|
+
item.pack_text = _read(args.pack, "review pack CSV")
|
|
137
|
+
item.pack_label = _label(args.pack)
|
|
138
|
+
if not item.review:
|
|
139
|
+
raise InputError("--pack needs --review (the review it is for)")
|
|
140
|
+
if args.metrics:
|
|
141
|
+
item.metrics_text = _read(args.metrics, "metrics CSV")
|
|
142
|
+
item.metrics_label = _label(args.metrics)
|
|
143
|
+
return item
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# cross-check against the bound q80 leaves (optional)
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def _skills_root():
|
|
151
|
+
return os.environ.get("AEROSKILLS_DEV", "").strip()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _load_leaf(root, leaf):
|
|
155
|
+
"""Import a leaf's *_logic.py module, or None."""
|
|
156
|
+
logic_dir = os.path.join(root, "skills", ECSS + leaf, "scripts")
|
|
157
|
+
if not os.path.isdir(logic_dir):
|
|
158
|
+
return None
|
|
159
|
+
for f in sorted(os.listdir(logic_dir)):
|
|
160
|
+
if f.endswith("_logic.py") and not f.startswith("test_"):
|
|
161
|
+
spec = importlib.util.spec_from_file_location(
|
|
162
|
+
"spa_dispatch_" + leaf.replace("-", "_"),
|
|
163
|
+
os.path.join(logic_dir, f))
|
|
164
|
+
mod = importlib.util.module_from_spec(spec)
|
|
165
|
+
try:
|
|
166
|
+
spec.loader.exec_module(mod)
|
|
167
|
+
except Exception:
|
|
168
|
+
return None
|
|
169
|
+
return mod
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def skills_status():
|
|
174
|
+
"""(root or None, loaded modules, reason when nothing can be loaded)."""
|
|
175
|
+
root = _skills_root()
|
|
176
|
+
if not root:
|
|
177
|
+
return None, {}, "AEROSKILLS_DEV is not set"
|
|
178
|
+
if not os.path.isdir(os.path.join(root, "skills")):
|
|
179
|
+
return None, {}, "AEROSKILLS_DEV has no skills/ folder: %s" % root
|
|
180
|
+
mods = {leaf: _load_leaf(root, leaf) for leaf in Q80_LEAVES}
|
|
181
|
+
if not any(mods.values()):
|
|
182
|
+
return None, {}, ("the skills at AEROSKILLS_DEV carry no q80 leaves "
|
|
183
|
+
"(an older release?)")
|
|
184
|
+
return root, mods, ""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _row(rows, leaf, function, core_val, skill_val, detail=""):
|
|
188
|
+
rows.append({"leaf": ECSS + leaf, "function": function,
|
|
189
|
+
"dispatched": True, "core_value": core_val,
|
|
190
|
+
"skill_value": skill_val, "agrees": core_val == skill_val,
|
|
191
|
+
"detail": detail})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _guard(rows, leaf, function, fn):
|
|
195
|
+
try:
|
|
196
|
+
fn()
|
|
197
|
+
except Exception as e: # a leaf that throws is a disagreement, not a crash
|
|
198
|
+
rows.append({"leaf": ECSS + leaf, "function": function,
|
|
199
|
+
"dispatched": True, "error": str(e)[:200],
|
|
200
|
+
"agrees": False})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def dispatch_rows(item, model, mods):
|
|
204
|
+
"""Run the same computation in the bound leaf and compare.
|
|
205
|
+
|
|
206
|
+
Every comparison is made on the engine's output BEFORE the role's own
|
|
207
|
+
additions (the document trace), because that layer has no counterpart
|
|
208
|
+
in the leaves.
|
|
209
|
+
"""
|
|
210
|
+
rows = []
|
|
211
|
+
cat, sec = model["category"], model["security_sensitive"]
|
|
212
|
+
scope = tuple(model["scope"])
|
|
213
|
+
review = model["review"]
|
|
214
|
+
ev_text = item.evidence_text
|
|
215
|
+
clauses = (item.clauses if item.clauses is not None
|
|
216
|
+
else core.default_clauses())
|
|
217
|
+
|
|
218
|
+
m = mods.get("q80-compliance-matrix")
|
|
219
|
+
if m is not None:
|
|
220
|
+
def matrix():
|
|
221
|
+
plain = core.build_matrix(clauses, core.parse_evidence_csv(ev_text),
|
|
222
|
+
cat, sec)
|
|
223
|
+
sk = m.build_matrix([{"id": c, "title": t} for c, t in clauses],
|
|
224
|
+
m.index_evidence(m.parse_evidence_csv(ev_text)),
|
|
225
|
+
category=cat, security_sensitive=sec)
|
|
226
|
+
mine = [(r["clause"], r["status"], r["tailoring"], r["gaps"])
|
|
227
|
+
for r in plain["rows"]]
|
|
228
|
+
theirs = [(r["clause"], r["status"], r["tailoring"], r["gaps"])
|
|
229
|
+
for r in sk["rows"]]
|
|
230
|
+
differ = sum(1 for a, b in zip(mine, theirs) if a != b) + abs(
|
|
231
|
+
len(mine) - len(theirs))
|
|
232
|
+
_row(rows, "q80-compliance-matrix", "build_matrix.rows",
|
|
233
|
+
"%d rows" % len(mine),
|
|
234
|
+
"%d rows" % len(theirs) if not differ else
|
|
235
|
+
"%d rows, %d differ" % (len(theirs), differ))
|
|
236
|
+
keys = ("clauses", "counts", "applicable", "compliant_fraction",
|
|
237
|
+
"evidenced_fraction", "rows_with_gaps")
|
|
238
|
+
_row(rows, "q80-compliance-matrix", "coverage_summary",
|
|
239
|
+
{k: plain["coverage"][k] for k in keys},
|
|
240
|
+
{k: sk["coverage"][k] for k in keys})
|
|
241
|
+
_row(rows, "q80-compliance-matrix", "find_gaps",
|
|
242
|
+
[(g["clause"], g["kind"]) for g in plain["gaps"]],
|
|
243
|
+
[(g["clause"], g["kind"]) for g in sk["gaps"]],
|
|
244
|
+
"%d gaps" % len(plain["gaps"]))
|
|
245
|
+
_row(rows, "q80-compliance-matrix", "orphan_evidence",
|
|
246
|
+
plain["orphan_evidence"], sk["orphan_evidence"])
|
|
247
|
+
_guard(rows, "q80-compliance-matrix", "build_matrix", matrix)
|
|
248
|
+
|
|
249
|
+
t = mods.get("q80-software-criticality-tailoring")
|
|
250
|
+
if t is not None:
|
|
251
|
+
def tailoring():
|
|
252
|
+
der = model["category_derivation"]
|
|
253
|
+
if der:
|
|
254
|
+
sk = t.assign_category(der["severity"], der["provisions"],
|
|
255
|
+
item.provides_provision_for or None)
|
|
256
|
+
_row(rows, "q80-software-criticality-tailoring",
|
|
257
|
+
"assign_category", der["category"], sk["category"])
|
|
258
|
+
counts = {"applicable": 0, "reduced": 0, "not-applicable": 0}
|
|
259
|
+
for r in t.tailoring_matrix(cat, sec):
|
|
260
|
+
counts[r["status"]] += 1
|
|
261
|
+
_row(rows, "q80-software-criticality-tailoring",
|
|
262
|
+
"tailoring_matrix", model["tailoring"]["totals"], counts)
|
|
263
|
+
_guard(rows, "q80-software-criticality-tailoring", "tailoring",
|
|
264
|
+
tailoring)
|
|
265
|
+
|
|
266
|
+
ms = mods.get("q80-milestone-assurance-evidence")
|
|
267
|
+
if ms is not None and review:
|
|
268
|
+
def milestone():
|
|
269
|
+
mine = [(d["document"], d["clauses"])
|
|
270
|
+
for d in core.evidence_due(review, cat, scope)]
|
|
271
|
+
sk = [(d["document"], d["clauses"])
|
|
272
|
+
for d in ms.evidence_due(review, cat, scope)]
|
|
273
|
+
_row(rows, "q80-milestone-assurance-evidence", "evidence_due",
|
|
274
|
+
mine, sk, "%d documents owed" % len(mine))
|
|
275
|
+
pk = model["milestone"]["pack"]
|
|
276
|
+
if pk is not None:
|
|
277
|
+
sub = [{"document": p["document"], "maturity": p["maturity"]}
|
|
278
|
+
for p in core.parse_pack_csv(item.pack_text)]
|
|
279
|
+
_row(rows, "q80-milestone-assurance-evidence",
|
|
280
|
+
"assess_review_pack", pk,
|
|
281
|
+
ms.assess_review_pack(review, cat, sub, scope))
|
|
282
|
+
inputs = core.spamr_inputs(model["matrix"], model["metrics"])
|
|
283
|
+
sk_sp = ms.spamr_skeleton(review, inputs)
|
|
284
|
+
_row(rows, "q80-milestone-assurance-evidence", "spamr_skeleton",
|
|
285
|
+
[(s["id"], s["status"]) for s in model["spamr"]["sections"]],
|
|
286
|
+
[(s["id"], s["status"]) for s in sk_sp["sections"]])
|
|
287
|
+
_guard(rows, "q80-milestone-assurance-evidence", "milestone",
|
|
288
|
+
milestone)
|
|
289
|
+
|
|
290
|
+
p = mods.get("q80-software-product-assurance-plan")
|
|
291
|
+
if p is not None and review and review in core.PLAN_MATURITY_BY_REVIEW:
|
|
292
|
+
_guard(rows, "q80-software-product-assurance-plan",
|
|
293
|
+
"plan_maturity_due", lambda: _row(
|
|
294
|
+
rows, "q80-software-product-assurance-plan",
|
|
295
|
+
"plan_maturity_due",
|
|
296
|
+
model["milestone"]["spap_maturity_due"],
|
|
297
|
+
p.plan_maturity_due(review)))
|
|
298
|
+
|
|
299
|
+
q = mods.get("q80-software-product-quality-metrics")
|
|
300
|
+
if q is not None and model["metrics"] is not None:
|
|
301
|
+
def metrics():
|
|
302
|
+
meas = core.parse_metrics_csv(item.metrics_text)
|
|
303
|
+
sk = q.evaluate_metrics(meas, cat)
|
|
304
|
+
key = ("metric", "value", "threshold", "status", "margin")
|
|
305
|
+
_row(rows, "q80-software-product-quality-metrics",
|
|
306
|
+
"evaluate_metrics",
|
|
307
|
+
[tuple(r[k] for k in key) for r in model["metrics"]["rows"]]
|
|
308
|
+
+ [model["metrics"]["verdict"]],
|
|
309
|
+
[tuple(r[k] for k in key) for r in sk["rows"]]
|
|
310
|
+
+ [sk["verdict"]])
|
|
311
|
+
_guard(rows, "q80-software-product-quality-metrics",
|
|
312
|
+
"evaluate_metrics", metrics)
|
|
313
|
+
return rows
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _provenance(rows, reason):
|
|
317
|
+
return {
|
|
318
|
+
"role": ROLE_SLUG,
|
|
319
|
+
"generator": "core/software_product_assurance_core.py",
|
|
320
|
+
"core": {
|
|
321
|
+
"file": "core/software_product_assurance_core.py",
|
|
322
|
+
"functions": ["derive_category", "tailoring_summary",
|
|
323
|
+
"build_matrix", "apply_document_trace",
|
|
324
|
+
"milestone_check", "assess_review_pack",
|
|
325
|
+
"spamr_skeleton", "evaluate_metrics",
|
|
326
|
+
"build_report", "check_report"],
|
|
327
|
+
"version": core.__version__,
|
|
328
|
+
},
|
|
329
|
+
"skills": rows,
|
|
330
|
+
"skills_not_dispatched_reason": reason,
|
|
331
|
+
"cross_checked": bool(rows) and all(r.get("agrees") for r in rows),
|
|
332
|
+
"disclaimer": "DRAFT for human review. Not an approval, not a "
|
|
333
|
+
"certification, not a statement of compliance.",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# commands
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
def cmd_build(args):
|
|
342
|
+
try:
|
|
343
|
+
item = gather(args)
|
|
344
|
+
model = core.build_report(item)
|
|
345
|
+
except (InputError, ValueError) as e:
|
|
346
|
+
print("error: %s" % e, file=sys.stderr)
|
|
347
|
+
return 2
|
|
348
|
+
|
|
349
|
+
root, mods, reason = (None, {}, "--no-dispatch given") \
|
|
350
|
+
if args.no_dispatch else skills_status()
|
|
351
|
+
if args.require_cross_check and not mods:
|
|
352
|
+
print("error: cross-check required but not possible: %s.\n%s"
|
|
353
|
+
% (reason, HOW_TO_POINT), file=sys.stderr)
|
|
354
|
+
return 2
|
|
355
|
+
|
|
356
|
+
md = core.render_report_markdown(model)
|
|
357
|
+
profile = None
|
|
358
|
+
try:
|
|
359
|
+
profile = evidence.load_profile(args.profile)
|
|
360
|
+
except ValueError as e:
|
|
361
|
+
print("error: bad profile: %s" % e, file=sys.stderr)
|
|
362
|
+
return 2
|
|
363
|
+
if profile:
|
|
364
|
+
model["profile"] = {k: profile.get(k) for k in (
|
|
365
|
+
"customer", "program", "basis", "authority", "der",
|
|
366
|
+
"document_prefix", "revision")}
|
|
367
|
+
md = evidence.profile_header_block(profile) + md
|
|
368
|
+
|
|
369
|
+
gates = core.check_report(model)
|
|
370
|
+
rows = dispatch_rows(item, model, mods) if mods else []
|
|
371
|
+
|
|
372
|
+
if not args.out:
|
|
373
|
+
sys.stdout.write(md)
|
|
374
|
+
return 0 if gates["all_pass"] else 1
|
|
375
|
+
|
|
376
|
+
out_dir = os.path.dirname(os.path.abspath(args.out))
|
|
377
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
378
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
379
|
+
f.write(md)
|
|
380
|
+
csv_path = args.csv or os.path.splitext(args.out)[0] + ".csv"
|
|
381
|
+
with open(csv_path, "w", encoding="utf-8", newline="") as f:
|
|
382
|
+
f.write(core.render_matrix_csv(model))
|
|
383
|
+
|
|
384
|
+
mx = model["matrix"]
|
|
385
|
+
if item.example:
|
|
386
|
+
print("using the bundled worked example (example/): invented "
|
|
387
|
+
"project, not a real mission")
|
|
388
|
+
print("built compliance matrix (DRAFT) -> %s" % args.out)
|
|
389
|
+
print(" csv -> %s" % csv_path)
|
|
390
|
+
if profile:
|
|
391
|
+
print(" profile: %s / %s" % (profile.get("customer"),
|
|
392
|
+
profile.get("program")))
|
|
393
|
+
print(" standard: %s; software category %s%s" % (
|
|
394
|
+
core.STANDARD, model["category"],
|
|
395
|
+
" (derived from severity %s)" % model["category_derivation"][
|
|
396
|
+
"severity"] if model["category_derivation"] else ""))
|
|
397
|
+
for n in model["category_notes"]:
|
|
398
|
+
print(" open point: %s" % n)
|
|
399
|
+
print(" " + core.coverage_line(mx["coverage"]))
|
|
400
|
+
print(" open gaps: %d" % len(mx["gaps"]))
|
|
401
|
+
tr = mx["trace"]
|
|
402
|
+
if tr["ran"]:
|
|
403
|
+
print(" document trace: %d cited, %d found, %d missing%s" % (
|
|
404
|
+
tr["cited"], len(tr["found"]), len(tr["missing"]),
|
|
405
|
+
(" (" + ", ".join(tr["missing"]) + ")") if tr["missing"] else ""))
|
|
406
|
+
else:
|
|
407
|
+
print(" document trace: not run (no --docs folder given)")
|
|
408
|
+
ms = model["milestone"]
|
|
409
|
+
if ms:
|
|
410
|
+
line = " %s: %d documents owed" % (ms["review"].upper(),
|
|
411
|
+
len(ms["owed"]))
|
|
412
|
+
if ms["pack"]:
|
|
413
|
+
pk = ms["pack"]
|
|
414
|
+
line += "; pack missing %d, immature %d, complete %s" % (
|
|
415
|
+
len(pk["missing"]), len(pk["immature"]),
|
|
416
|
+
"yes" if pk["complete"] else "no")
|
|
417
|
+
print(line)
|
|
418
|
+
if model["metrics"]:
|
|
419
|
+
mt = model["metrics"]
|
|
420
|
+
print(" metrics: %s (failing %d, not measured %d)" % (
|
|
421
|
+
mt["verdict"], len(mt["failing"]), len(mt["missing"])))
|
|
422
|
+
print(" gates: all_pass=%s" % gates["all_pass"])
|
|
423
|
+
if not gates["all_pass"]:
|
|
424
|
+
print(" failing gates: %s" % ", ".join(
|
|
425
|
+
k for k, v in gates.items() if k != "all_pass" and not v))
|
|
426
|
+
|
|
427
|
+
disagree = [r for r in rows if not r.get("agrees")]
|
|
428
|
+
if rows:
|
|
429
|
+
print(" dispatch cross-check: %d/%d agree against the bound q80 "
|
|
430
|
+
"leaf logic" % (len(rows) - len(disagree), len(rows)))
|
|
431
|
+
for r in disagree:
|
|
432
|
+
print(" DISAGREE %s %s: %s" % (
|
|
433
|
+
r["leaf"].rsplit("/", 1)[-1], r["function"],
|
|
434
|
+
r.get("error") or "core and skill differ"))
|
|
435
|
+
else:
|
|
436
|
+
print(" dispatch cross-check: not dispatched (%s). The matrix is "
|
|
437
|
+
"complete without it." % reason)
|
|
438
|
+
if not args.no_dispatch:
|
|
439
|
+
for line in HOW_TO_POINT.splitlines():
|
|
440
|
+
print(" " + line)
|
|
441
|
+
if args.bundle:
|
|
442
|
+
paths = evidence.write_bundle(args.out, ROLE_SLUG, DELIVERABLE,
|
|
443
|
+
model, gates,
|
|
444
|
+
_provenance(rows, reason))
|
|
445
|
+
print(" bundle: model=%s" % paths["model"])
|
|
446
|
+
print(" gates=%s" % paths["gates"])
|
|
447
|
+
print(" provenance=%s" % paths["provenance"])
|
|
448
|
+
print(core.STOP_LINE)
|
|
449
|
+
if disagree:
|
|
450
|
+
return 1
|
|
451
|
+
return 0 if gates["all_pass"] else 1
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def cmd_check(args):
|
|
455
|
+
if not os.path.exists(args.file):
|
|
456
|
+
print("file not found: %s" % args.file)
|
|
457
|
+
return 1
|
|
458
|
+
with open(args.file, encoding="utf-8") as f:
|
|
459
|
+
md = f.read()
|
|
460
|
+
gates = core.check_report_markdown(md)
|
|
461
|
+
for name, ok in gates.items():
|
|
462
|
+
if name in ("all_pass", "sign_off"):
|
|
463
|
+
continue
|
|
464
|
+
print("%s %s" % ("PASS" if ok else "FAIL", name))
|
|
465
|
+
so = gates["sign_off"]
|
|
466
|
+
if so["state"] == "unsigned":
|
|
467
|
+
print("SIGN-OFF: none recorded. " + core.STOP_LINE)
|
|
468
|
+
else:
|
|
469
|
+
print("SIGN-OFF: %s by %s (%s) on %s" % (
|
|
470
|
+
so["decision"] or "?", so["signatory"] or "?", so["role"] or "?",
|
|
471
|
+
so["date"] or "?"))
|
|
472
|
+
for e in so["errors"]:
|
|
473
|
+
print(" sign-off problem: %s" % e)
|
|
474
|
+
print("RESULT: %s" % ("PASS" if gates["all_pass"] else "FAIL"))
|
|
475
|
+
return 0 if gates["all_pass"] else 1
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main():
|
|
479
|
+
p = argparse.ArgumentParser(
|
|
480
|
+
description="Software Product Assurance Engineer role "
|
|
481
|
+
"(ECSS-Q-ST-80C Rev.2 compliance matrix)")
|
|
482
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
483
|
+
|
|
484
|
+
b = sub.add_parser("build", help="build the compliance matrix (DRAFT)")
|
|
485
|
+
b.add_argument("--out", default="",
|
|
486
|
+
help="Markdown output (default: stdout); the CSV goes "
|
|
487
|
+
"beside it")
|
|
488
|
+
b.add_argument("--csv", default="", help="CSV output path")
|
|
489
|
+
b.add_argument("--evidence", default="",
|
|
490
|
+
help="evidence CSV: clause,document,section,status,"
|
|
491
|
+
"justification (omit for the bundled example)")
|
|
492
|
+
b.add_argument("--category", default="",
|
|
493
|
+
help="software criticality category A-D")
|
|
494
|
+
b.add_argument("--severity", default="",
|
|
495
|
+
help="highest function severity I-IV (derives the "
|
|
496
|
+
"category)")
|
|
497
|
+
b.add_argument("--provision", action="append", default=[],
|
|
498
|
+
help="compensating provision at system level: hardware, "
|
|
499
|
+
"software or operational-procedure (repeatable)")
|
|
500
|
+
b.add_argument("--provides-provision-for", default="",
|
|
501
|
+
help="severity of the functions this software is itself "
|
|
502
|
+
"the compensating provision for")
|
|
503
|
+
b.add_argument("--security-sensitive", action="store_true",
|
|
504
|
+
help="the software is security sensitive")
|
|
505
|
+
b.add_argument("--scope", default="",
|
|
506
|
+
help="comma list: suppliers, procured, reuse, security, "
|
|
507
|
+
"operations")
|
|
508
|
+
b.add_argument("--review", default="",
|
|
509
|
+
help="target review: SRR, PDR, CDR, TRR, QR, AR or ORR")
|
|
510
|
+
b.add_argument("--docs", default="",
|
|
511
|
+
help="the project's document folder: every cited "
|
|
512
|
+
"document must resolve to a file in it")
|
|
513
|
+
b.add_argument("--pack", default="",
|
|
514
|
+
help="review pack CSV: document (kind),maturity")
|
|
515
|
+
b.add_argument("--metrics", default="",
|
|
516
|
+
help="measurement set CSV: metric,value")
|
|
517
|
+
b.add_argument("--clauses", default="",
|
|
518
|
+
help="customer clause list (one id per line or id,title "
|
|
519
|
+
"CSV); default: every requirement of the standard")
|
|
520
|
+
b.add_argument("--project", default="", help="project name")
|
|
521
|
+
b.add_argument("--bundle", action="store_true",
|
|
522
|
+
help="emit evidence/{model,gates,provenance}.json")
|
|
523
|
+
b.add_argument("--profile", default="",
|
|
524
|
+
help="program profile JSON (docs/PROFILE-SCHEMA.md)")
|
|
525
|
+
b.add_argument("--no-dispatch", action="store_true",
|
|
526
|
+
help="skip the cross-check against the bound skills")
|
|
527
|
+
b.add_argument("--require-cross-check", action="store_true",
|
|
528
|
+
help="fail (exit 2) when the bound skills cannot be "
|
|
529
|
+
"dispatched")
|
|
530
|
+
b.set_defaults(fn=cmd_build)
|
|
531
|
+
|
|
532
|
+
c = sub.add_parser("check", help="gate-check a matrix file")
|
|
533
|
+
c.add_argument("--file", required=True,
|
|
534
|
+
help="compliance matrix Markdown to gate-check")
|
|
535
|
+
c.set_defaults(fn=cmd_check)
|
|
536
|
+
|
|
537
|
+
args = p.parse_args()
|
|
538
|
+
sys.exit(args.fn(args))
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
if __name__ == "__main__":
|
|
542
|
+
main()
|