A demonstration of bidirectional requirements traceability for a satellite Attitude Determination and Control System (ADCS). The demo traces the full lifecycle from SysMLv2 structural specification through symbolic analysis (SymPy), numerical simulation (scipy), and human expert attestation — with all traceability stored as RDF in a named-graph quadstore and all artifacts version-controlled in git.
Evidence does not verify requirements; evidence supports a human judgment that requirements are satisfied. Models are imperfect representations of physical systems. The engineer judges model adequacy and evidence sufficiency. Only human attestation connects evidence to requirement satisfaction.
This split is the Hawkins–Habli Assurance Claim Point categorization:
adequacy is a gsn:Assumption, sufficiency is a gsn:Justification. The
project's rtm: namespace introduces NO novel epistemic vocabulary.
rtm: is a thin application/integration ontology. Convenience classes and
properties are subclasses / subproperties of established standards; the
project's contribution is the assembly, not new terms.
| Namespace | Role |
|---|---|
prov: |
Provenance spine |
sysml: ↔ omg-sysml: |
SysMLv2; local prefix aliased to OMG SysMLv2 OWL |
earl: |
Assertion + outcome lattice + mode |
gsn: (OntoGSN) |
Goal / Strategy / Solution / Assumption / Justification |
p-plan: |
Process model (one Step per pipeline stage) |
oslc_rm: / oslc_qm: |
Tool-interop aliases for RM tools |
sh: |
SHACL closure-rule suite |
The runtime is Dataset(default_union=True) so SPARQL queries match across
named graphs without GRAPH clauses. Each layer lives in its own graph,
sized to match Flexo MMS branch conventions:
<rtm:ontology> TBox + shapes + individuals
<rtm:plan> P-PLAN process model
<adcs:structural> SysMLv2 instance data
<adcs:context> Stable gsn:Context / gsn:Assumption individuals
<adcs:evidence> rtm:Evidence artifacts (incl. rtm:DockerImage under --compute=docker, WP3)
<adcs:attestations> rtm:Attestation events
<adcs:plan-execution> p-plan:Activity instances (one per stage)
<adcs:audit> Forward/backward/bidirectional audit summary
IRIs and the NAMED_GRAPHS dict are in ontology/prefixes.py.
The demo runs against three remotes + a fourth service. See ARCHITECTURE.md for the full picture; one-paragraph summary:
- git holds code, ontology, Dockerfile.
- Flexo holds RDF named graphs (incl.
rtm:DockerImagerecords). - local Docker is the runtime: image (static, content-addressed) vs container (transient, per-run) vs host (the machine).
- transaction-log store (CouchDB; opt-in via
ADCS_TXNLOG_ENABLED=1) holds the wire-log JSON for each service invocation.
URI scheme (canonical shapes):
- Image:
urn:adcs:docker-image:<digest>(rtm:DockerImage) - Container:
urn:adcs:docker-container:<container-id>(rtm:DockerContainer) - Host:
urn:adcs:location:docker:<host>(prov:Location) - Executor:
urn:adcs:executor:<id>(prov:SoftwareAgent) - Operating org:
urn:adcs:org:local-operatordefault (prov:Organization) - Flexo record:
urn:adcs:flexo:<org>/<repo>/<branch> - Flexo service:
urn:adcs:service:flexo-mms - Txnlog service:
urn:adcs:service:transaction-log-store
Standard PROV edges link them: <container> prov:wasDerivedFrom <image>, <activity> prov:used <container>, <executor> prov:actedOnBehalfOf <operating-org>. Auspices are per substrate:
each substrate's location/service node carries rtm:operatedBy
(subPropertyOf prov:wasAttributedTo) pointing at its OWN hosting org —
the compute host gets ADCS_HOSTING_ORG_* (defaults to the operating
org), the Flexo service node gets ADCS_FLEXO_HOSTING_ORG_*, the
txnlog service node gets ADCS_TXNLOG_HOSTING_ORG_* (defaults to the
compute hosting org). FLEXO_ORG is the Flexo MMS org slug (REST path
segment), not an auspices IRI. The image carries rtm:gitRef +
rtm:flexoRecord for cross-remote linking; the record links
prov:atLocation to the service node.
EARL-wrapped verification outcomes sit beside human attestation:
rtm:ClosureRuleAssertion (Stage 6.5 SHACL outcome),
rtm:DigestMatchAssertion (compute.reproduce rebuild outcome), and
rtm:BehaviorOracleAssertion (analysis.oracle metric-vs-criterion
outcome) are all earl:Assertion subclasses with earl:mode = earl:automatic. The behavior oracle verifies a model-level claim (a
computed metric against a requirement's machine-readable acceptance
criterion); it never asserts physical requirement satisfaction — that
remains human attestation only. Accordingly it links the requirement via
rtm:evaluatesAgainst (subPropertyOf prov:used), never rtm:attests.
Preflight gate probes every configured backend before Stage 0 and fails fast on any unreachable remote. Matches WP2's ROBOT-default discipline — no silent degrade.
Seven trust queries in traceability/queries.py answer "how can I
trust this?" — technical_provenance, auspices_chain,
reproducibility_witnesses, closure_witnesses,
service_invocations_for, service_auspices, trust_summary.
ontology/—rtm-edit.ttl(source),rtm.ttl(built artifact),rtm_shapes.ttl(closure-rule suite),rtm_individuals.ttl,sysml_term_map.csv,assembly_manifest.json,imports/(vendored upstreams)structural/— SysMLv2 RDF modelanalysis/— SymPy symbolic analysis + scipy numerical simulation + traceable behavior-model oracle (oracle.py)compute/—LocalCompute(in-process) +DockerCompute(containerized remote-emulation with PROV provenance capture) +Dockerfileevidence/— content hashing + RDF evidence binding (with execution metadata)traceability/— RTM assembly, SPARQL queries, attestation (GSN-based), closure-rule verification (verification.py), behavior-oracle assertion emitter (oracle_assertion.py), audit modulepipeline/— stage orchestrator (PipelineState+ per-stage free functions inrunner.py;state.pydefines the typed result records),dataset.py(named-graph helpers incl.query_named_graph),stage0_assembly.py,plan.ttl,backends/(Local / Flexo / Fuseki)flexo/— Flexo MMS provisioning scripts + integration docsinterrogate/— explain / reproduce / visualize / rerundocuments/— compiled document views over the dataset (design_description.pybuilds DDVS-001; deterministic, never hand-edited)scripts/—fetch_imports.py,build_ontology.pytests/— 309 tests: 306 in the default run + 3 live-Flexo opt-in (alignment, named graphs, shape suite, audit, backends, compute, behavior oracle, document compiler)
Stage 0 narrates the ontology assembly; Stage 6.5 runs the closure-rule
suite; Stage 7a runs the audit. Every stage emits a p-plan:Activity
into <adcs:plan-execution> so the construction process is itself
queryable.
The orchestrator threads a PipelineState object
through per-stage free functions (run_stage_<N>_<name>(state) -> <StageResult> in pipeline/runner.py). Each
stage returns a frozen dataclass (StructuralResult,
SymbolicResult, NumericalResult, …) that the next stage reads via
state.<prior>.<field>. The runner's job is narration + the ordered
call sequence; stage bodies stand alone and are unit-testable.
PipelineState.activity_to_stage maps p-plan step IRI fragments
(STEP_NAMES in
traceability/plan_execution.py)
to stage numbers; interrogate/rerun.py
keeps a parallel ACTIVITY_TO_STAGE table, cross-checked by a unit
test against STEP_NAMES.
Every CLI in this repo is a Typer app (Click + Rich transitively).
Pattern per module: app = typer.Typer(...) + @app.command()
function + if __name__ == "__main__": app(). Existing uv run python -m <module> invocations and the [project.scripts] adcs-pipeline
console script work unchanged. Choice-validated options use Enum
subclasses so Typer matches the prior argparse choices= semantics.
CLIs today:
pipeline.runner— runs the full lifecycle (flags:--auto,--no-attest,--engineer,--rebuild,--backend,--compute).interrogate.rerun— translates a verification report into the pipeline stages that must re-run (flags:--input,--requirement,--format).documents.design_description— compiles DDVS-001, a deterministic Markdown design-description/VCRM view overoutput/rtm.trig(flags:--input,--output,--requirement,--check,--stdout). Byte-identical rebuilds: document date =MAX(prov:generatedAtTime)(never wall clock), fingerprint = sha256 of the raw input file bytes (bnode relabeling rules out re-serialization hashing);--checkis the drift gate.
interrogate.explain, interrogate.reproduce, interrogate.visualize
are library-only (no CLI entry points). Tests use
typer.testing.CliRunner in tests/test_cli.py.
A top-level adcs aggregator (adcs pipeline run, adcs interrogate rerun, etc.) is tracked as a follow-up — see
issue #5.
Strict semantic split across module names, function names, doc strings, RDF property labels, log/banner strings, and commit messages:
- Verification = automated check whose computation result is
fully specified (SHACL conformance, ROBOT/ELK consistency,
content-hash matching, completeness checks, HTTP-connectivity
probes, triple-count budgets, behavior-oracle metric-vs-criterion
comparison in
analysis.oracle). - Validation = human judgement with expertise, additional context, and/or interpretation (engineer attestation, adequacy assumption, sufficiency justification).
This breaks with pyshacl's validate() convention deliberately —
upstream APIs keep their names, the demo's own wrappers use the
discipline. SHACL conformance is wrapped by traceability.verification.verify;
human judgement lives in traceability.attestation.request_attestation.
The split pairs with the longer-standing rule that evidence does not
verify requirements; only human attestation does: evidence and
verification are both system-internal, attestation is the human
boundary.
One IRI fragment is intentionally out-of-sync with the discipline:
<plan/step/ValidateShapes> in pipeline/plan.ttl is preserved so
already-persisted <adcs:plan-execution> and <adcs:audit> graphs
stay valid. The rdfs:label says "Verify"; the IRI rename is tracked
separately for a future Flexo migration.
- Python 3.12+, managed by
uv— required rdflibfor RDF/SPARQL,sympyfor symbolic math,scipyfor ODE integrationpyshaclfor closure-rule verification (the demo's own wrapper is namedverify; pyshacl's upstream API isvalidateand is wrapped, not renamed),httpxfor backend HTTP,typerfor CLI surfaces- ProofScript/ProofBuilder pattern reimplemented from gds-proof
- Docker — optional, for
--compute=docker - OBO ROBOT (Java) — required for default
make ontology; no-Java users invoke the explicitmake ontology-pythontarget instead. CI installs Java 17 + a cachedrobot.jarand verifies every push.
uv run python -m pipeline.runner --auto # local backend, local compute
uv run python -m pipeline.runner --auto --backend=flexo # push to Flexo MMS
uv run python -m pipeline.runner --auto --compute=docker # remote-compute emulationThe runner runs against the committed rtm.ttl; it does NOT need
Java/obo-robot. Only rebuilding the ontology does.
uv run pytest # default: skips live + network markers
uv run pytest -m live # opt-in: live Flexo MMS round-trip (needs FLEXO_TOKEN)
uv run pytest -m network # opt-in: reserved for W3C-vocab fetchesMarker filtering is set in pyproject.toml via addopts = "-m 'not live and not network'". tests/test_flexo_live.py carries the
live marker; tests there fail loudly when -m live is requested
without credentials rather than silently skipping (skip-on-opt-in
would hide infra breakage).
make ontology # canonical: Python assembly + ROBOT/ELK verification (requires Java + obo-robot)
make ontology-python # no-Java path: Python assembly only, ROBOT verification skipped
make ontology-robot # just the ROBOT step (no rtm.ttl rewrite)make ontology fails fast on missing Java/obo-robot. The no-Java
escape is the explicit ontology-python target — invoking it is an
intentional opt-out, not a flag. The manifest's robot_used field +
Stage 0 banner record which path produced the artifact. The build
also enforces a triple-count budget (TRIPLE_BUDGET=356 in
scripts/build_ontology.py) so the integration ontology can't quietly
grow novel epistemic vocabulary.
rtm.ttl is a built artifact. Edit rtm-edit.ttl and rebuild.
Commit rtm-edit.ttl BEFORE you rebuild rtm.ttl. rtm.ttl's # Built <time> stamp comes from _reproducible_build_time(), which reads the git
commit time of rtm-edit.ttl. Rebuilding while that edit is still uncommitted
stamps the previous commit's time, so the committed rtm.ttl won't match CI's
rebuild and the CI git diff --exit-code -- ontology/rtm.ttl check fails
(ROBOT/ELK itself still passes). Order: commit rtm-edit.ttl → make ontology
→ commit rtm.ttl + assembly_manifest.json.
Local toolchain (macOS): the brew JDK + obo-robot aren't found by
/usr/bin/java or /usr/libexec/java_home, so point JAVA_HOME at the
Homebrew openjdk and put obo-robot on PATH:
JAVA_HOME=/opt/homebrew/opt/openjdk PATH="/opt/homebrew/bin:$PATH" make ontology # Apple SiliconThe Makefile now auto-detects /opt/homebrew vs /usr/local for JAVA_HOME.
Captured in the plan file at /Users/z/.claude/plans/i-want-to-look-hidden-balloon.md:
- Cryptographic envelopes & signatures — W3C VC Data Integrity (eddsa-rdfc-2022) + in-toto/SLSA + sigstore/Rekor. Today's bare SHA-256 gives content identity, not authenticity.
- Formal authority/credential model — FOAF + Org Ontology +
schema:hasCredential+ W3C Verifiable Credentials on top ofprov:Agent. - OntoGSN confidence arguments — reify per-ACP confidence.
- Defeaters & revocation (SACM-style).
- Multi-attestation aggregation policy — sign-off gates as SHACL.
- Production Flexo deployment — multi-user auth, PR branches for RTM evolution, federation.
- OSLC connector for DOORS Next / Jama / RQM.
- Federated SPARQL for cross-program traceability.
- Continuous re-verification in CI + live traceability dashboard.
Notebook Act 9–10 demonstrate the capture side of remote compute provenance. Production replay against a pulled image is a future addition that pairs with item #1.