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.
Files changed (27) hide show
  1. package/manifest.json +17 -3
  2. package/package.json +1 -1
  3. package/roles/software-product-assurance-engineer/ROLE.md +243 -0
  4. package/roles/software-product-assurance-engineer/SOURCES.md +40 -0
  5. package/roles/software-product-assurance-engineer/cli.py +542 -0
  6. package/roles/software-product-assurance-engineer/core/software_product_assurance_core.py +2108 -0
  7. package/roles/software-product-assurance-engineer/example/docs/EX-AUDIT-2026-01.md +7 -0
  8. package/roles/software-product-assurance-engineer/example/docs/EX-CSTD-001.md +7 -0
  9. package/roles/software-product-assurance-engineer/example/docs/EX-DSA-001.md +7 -0
  10. package/roles/software-product-assurance-engineer/example/docs/EX-DSTD-001.md +7 -0
  11. package/roles/software-product-assurance-engineer/example/docs/EX-RB-001.md +7 -0
  12. package/roles/software-product-assurance-engineer/example/docs/EX-SCMP-001.md +7 -0
  13. package/roles/software-product-assurance-engineer/example/docs/EX-SDP-001.md +7 -0
  14. package/roles/software-product-assurance-engineer/example/docs/EX-SPAP-001.md +7 -0
  15. package/roles/software-product-assurance-engineer/example/docs/EX-SPAR-2026-Q3.md +7 -0
  16. package/roles/software-product-assurance-engineer/example/docs/EX-SPR-LOG.md +7 -0
  17. package/roles/software-product-assurance-engineer/example/docs/EX-SRF-001.md +7 -0
  18. package/roles/software-product-assurance-engineer/example/docs/EX-SVALP-001.md +7 -0
  19. package/roles/software-product-assurance-engineer/example/docs/EX-SVERP-001.md +7 -0
  20. package/roles/software-product-assurance-engineer/example/docs/EX-TS-001.md +7 -0
  21. package/roles/software-product-assurance-engineer/example/evidence.csv +251 -0
  22. package/roles/software-product-assurance-engineer/example/metrics.csv +11 -0
  23. package/roles/software-product-assurance-engineer/example/pdr-pack.csv +17 -0
  24. package/roles/software-product-assurance-engineer/templates/q80-compliance-matrix-template.md +572 -0
  25. package/roles/software-product-assurance-engineer/tests/test_role_software_product_assurance_engineer.py +202 -0
  26. package/roles/software-product-assurance-engineer/tests/test_software_product_assurance_cli.py +271 -0
  27. 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()