quantbom 0.6.0__py3-none-any.whl

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 (45) hide show
  1. quantbom/__init__.py +497 -0
  2. quantbom/_bitorder.py +121 -0
  3. quantbom/_circuit.py +77 -0
  4. quantbom/_collect.py +661 -0
  5. quantbom/_drift.py +333 -0
  6. quantbom/_fidelity.py +280 -0
  7. quantbom/_merge.py +187 -0
  8. quantbom/_qir.py +224 -0
  9. quantbom/_scan.py +118 -0
  10. quantbom/_sign.py +284 -0
  11. quantbom/_stats.py +125 -0
  12. quantbom/_verify.py +530 -0
  13. quantbom/actions/__init__.py +27 -0
  14. quantbom/actions/base.py +68 -0
  15. quantbom/actions/benchmark.py +53 -0
  16. quantbom/actions/bind_backend.py +71 -0
  17. quantbom/actions/capture.py +106 -0
  18. quantbom/actions/check_qir.py +44 -0
  19. quantbom/actions/check_results.py +75 -0
  20. quantbom/actions/compare_native.py +88 -0
  21. quantbom/actions/disclose_error_handling.py +66 -0
  22. quantbom/actions/execute.py +85 -0
  23. quantbom/actions/inventory.py +24 -0
  24. quantbom/actions/layout.py +118 -0
  25. quantbom/actions/prove_equivalence.py +225 -0
  26. quantbom/actions/record_provenance.py +49 -0
  27. quantbom/actions/verify_conversion.py +53 -0
  28. quantbom/attest.py +363 -0
  29. quantbom/cli.py +378 -0
  30. quantbom/frameworks/__init__.py +67 -0
  31. quantbom/frameworks/base.py +155 -0
  32. quantbom/frameworks/braket.py +132 -0
  33. quantbom/frameworks/cirq.py +106 -0
  34. quantbom/frameworks/cudaq.py +223 -0
  35. quantbom/frameworks/pennylane.py +116 -0
  36. quantbom/frameworks/qiskit.py +67 -0
  37. quantbom/frameworks/qrisp.py +267 -0
  38. quantbom/harness.json +1730 -0
  39. quantbom/harness.py +88 -0
  40. quantbom-0.6.0.dist-info/METADATA +207 -0
  41. quantbom-0.6.0.dist-info/RECORD +45 -0
  42. quantbom-0.6.0.dist-info/WHEEL +5 -0
  43. quantbom-0.6.0.dist-info/entry_points.txt +2 -0
  44. quantbom-0.6.0.dist-info/licenses/LICENSE +202 -0
  45. quantbom-0.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,118 @@
1
+ """The QBOM property layout — the order properties appear in `metadata.properties`.
2
+
3
+ Actions emit properties in whatever order suits them; the collector places them
4
+ by this table, so the SBOM is byte-stable and adding a property is an explicit
5
+ decision. A property missing from this table is an error, not an afterthought.
6
+ """
7
+ PROPERTY_ORDER = (
8
+ "secqdevops:provenance-ref",
9
+ "secqdevops:signature-ref",
10
+ "secqdevops:provenance-artifact-ref",
11
+ "secqdevops:signature-artifact-ref",
12
+ "secqdevops:quantum-stack",
13
+ "secqdevops:normalized-ir",
14
+ "secqdevops:source-format",
15
+ "secqdevops:circuit-format",
16
+ "secqdevops:circuit-artifact-ref",
17
+ "secqdevops:circuit-sha256",
18
+ "secqdevops:transpiled-circuit-ref",
19
+ "secqdevops:transpiled-circuit-sha256",
20
+ "secqdevops:qasm-program-ref",
21
+ "secqdevops:qasm-program-sha256",
22
+ "secqdevops:qir-program-ref",
23
+ "secqdevops:qir-program-sha256",
24
+ "secqdevops:qir-profile",
25
+ "secqdevops:transpile-report-ref",
26
+ "secqdevops:backend-provider",
27
+ "secqdevops:backend-name",
28
+ "secqdevops:execution-mode",
29
+ "secqdevops:execution-target",
30
+ "secqdevops:transpile-profile-hash",
31
+ "secqdevops:backend-target-ref",
32
+ "secqdevops:backend-target-sha256",
33
+ "secqdevops:calibration-ref",
34
+ "secqdevops:calibration-sha256",
35
+ "secqdevops:calibration-captured-at",
36
+ "secqdevops:calibration-age-hours",
37
+ "secqdevops:error-handling-ref",
38
+ "secqdevops:error-handling-sha256",
39
+ "secqdevops:error-mitigation",
40
+ "secqdevops:runtime-job-id",
41
+ "secqdevops:runtime-job-ref",
42
+ "secqdevops:runtime-job-sha256",
43
+ "secqdevops:runtime-job-status",
44
+ "secqdevops:result-summary-ref",
45
+ "secqdevops:result-summary-sha256",
46
+ "secqdevops:qubit-count",
47
+ "secqdevops:circuit-depth",
48
+ "secqdevops:two-qubit-depth",
49
+ "secqdevops:t-depth",
50
+ "secqdevops:gate-count",
51
+ "secqdevops:benchmark-ref",
52
+ "secqdevops:benchmark-sha256",
53
+ "secqdevops:layer-model-ref",
54
+ "secqdevops:vocabulary-ref",
55
+ "secqdevops:cyclonedx-edition",
56
+ "secqdevops:qbom-profile-version",
57
+ "secqdevops:circuit-layer",
58
+ "secqdevops:circuit-dynamic",
59
+ "secqdevops:transpiled-circuit-layer",
60
+ "secqdevops:backend-target-layer",
61
+ "secqdevops:runtime-job-layer",
62
+ "secqdevops:equivalence-ref",
63
+ "secqdevops:equivalence-sha256",
64
+ "secqdevops:equivalence-verdict",
65
+ "secqdevops:equivalence-tool",
66
+ "secqdevops:equivalence-kind",
67
+ "secqdevops:expected-distribution-ref",
68
+ "secqdevops:expected-distribution-sha256",
69
+ "secqdevops:result-tvd",
70
+ "secqdevops:result-tolerance",
71
+ "secqdevops:result-fidelity-verdict",
72
+ "secqdevops:result-confidence",
73
+ "secqdevops:result-sampling-error",
74
+ "secqdevops:result-shots-required",
75
+ "secqdevops:conversion-fidelity-ref",
76
+ "secqdevops:conversion-fidelity-sha256",
77
+ "secqdevops:conversion-fidelity-verdict",
78
+ "secqdevops:conversion-fidelity-method",
79
+ "secqdevops:native-program-ref",
80
+ "secqdevops:native-program-sha256",
81
+ "secqdevops:native-program-format",
82
+ "secqdevops:native-result-ref",
83
+ "secqdevops:native-result-sha256",
84
+ "secqdevops:native-result-verdict",
85
+ "secqdevops:native-result-tvd",
86
+ "secqdevops:qir-consistency-ref",
87
+ "secqdevops:qir-consistency-sha256",
88
+ "secqdevops:qir-consistency-verdict",
89
+ "secqdevops:runtime-session-id",
90
+ "secqdevops:provider-job-id",
91
+ "secqdevops:cnot-depth",
92
+ "secqdevops:ir-standard",
93
+ "secqdevops:harness-version",
94
+ "secqdevops:harness-log-ref",
95
+ "secqdevops:harness-log-sha256",
96
+ )
97
+
98
+
99
+ def assemble(results) -> list[dict[str, str]]:
100
+ """Merge every action's properties into the canonical layout.
101
+
102
+ Raises if two actions emit the same property (a collision like the one that
103
+ once silently dropped result-fidelity evidence) or if a property has no
104
+ place in the layout.
105
+ """
106
+ owner: dict[str, str] = {}
107
+ values: dict[str, str] = {}
108
+ for result in results:
109
+ for prop in result.properties:
110
+ name = prop["name"]
111
+ if name in owner:
112
+ raise RuntimeError(f"{name} emitted by both {owner[name]!r} and {result.action!r}")
113
+ owner[name] = result.action
114
+ values[name] = prop["value"]
115
+ unplaced = sorted(set(values) - set(PROPERTY_ORDER))
116
+ if unplaced:
117
+ raise RuntimeError(f"properties with no place in the QBOM layout: {unplaced}")
118
+ return [{"name": n, "value": values[n]} for n in PROPERTY_ORDER if n in values]
@@ -0,0 +1,225 @@
1
+ """prove_equivalence — the compiled circuit computes the same function as the source (feeds QNT-05).
2
+
3
+ The collector only RECORDS the checker's verdict, including `not_equivalent`
4
+ (honesty over convenience); the gate judges it.
5
+
6
+ Kinds of equivalence (decision 7 Oct 2026, `secqdevops:equivalence-kind`):
7
+ unitary the compiled circuit implements the same unitary (up to global phase)
8
+ observational the unitaries differ, but by nothing any measurement can observe —
9
+ identical measurement statistics (QCEC partial equivalence). A
10
+ measurement-aware optimiser (Qiskit level 3) legitimately produces
11
+ this; the full check alone would call a correct compilation wrong.
12
+ Only attempted for circuits whose measurements are all final.
13
+ dynamic a dynamic circuit, compared under QCEC's deferred-measurement
14
+ transformation (see verify_dynamic)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+
20
+ from .base import NOT_REQUESTED, PRODUCED, UNSUPPORTED, ActionResult, Context
21
+
22
+ ID = "prove_equivalence"
23
+
24
+
25
+ def run(ctx: Context) -> ActionResult:
26
+ ctx.state["equivalence_verdict"] = None
27
+ if not ctx.equivalence_check:
28
+ return ActionResult(ID, NOT_REQUESTED, reason="equivalence proof not requested")
29
+ try:
30
+ from importlib.metadata import version as _pkg_version
31
+
32
+ from mqt import qcec
33
+ except ImportError:
34
+ return ActionResult(ID, UNSUPPORTED, reason="mqt.qcec is not installed (pip install 'quantbom[verify]')")
35
+ from .. import _collect as C
36
+
37
+ dynamic = bool(ctx.circuit_info.get("dynamic"))
38
+ dynamic_detail = None
39
+ partial_detail = None
40
+ kind = "dynamic" if dynamic else "unitary"
41
+ try:
42
+ if dynamic:
43
+ # dynamic circuits: QCEC defers measurements and turns classical
44
+ # control into quantum control before comparing (decision 2 Oct 2026)
45
+ result, dynamic_detail = verify_dynamic(qcec, ctx.qiskit_circuit, ctx.state["transpiled"])
46
+ else:
47
+ result = qcec.verify(ctx.qiskit_circuit, ctx.state["transpiled"])
48
+ if _criterion(result) == "not_equivalent" and final_measurements(ctx.qiskit_circuit):
49
+ # the unitaries differ: do the differences reach any measurement?
50
+ result, kind, partial_detail = verify_observational(qcec, ctx.qiskit_circuit, ctx.state["transpiled"], result)
51
+ except Exception as err: # contained: one undecidable proof must never abort the collection
52
+ return ActionResult(ID, UNSUPPORTED,
53
+ reason=f"mqt.qcec could not check this circuit: {type(err).__name__}: {str(err)[:200]}")
54
+ criterion = _criterion(result)
55
+ if result.considered_equivalent() and not (dynamic_detail or {}).get("unverified_gates"):
56
+ verdict = "equivalent"
57
+ elif criterion == "not_equivalent":
58
+ verdict = "not_equivalent"
59
+ else: # timeout / no_information / a decomposition step not verified: declared but non-probative
60
+ verdict = "inconclusive"
61
+ tool = f"mqt.qcec@{_pkg_version('mqt.qcec')}"
62
+ method = "mqt.qcec verify() default portfolio (decision diagrams + ZX-calculus)"
63
+ if dynamic:
64
+ method += "; dynamic circuit transformed (deferred measurement, classical -> quantum control)"
65
+ if dynamic_detail and dynamic_detail.get("decomposed_source"):
66
+ method += ("; source decomposed to the basis at optimization level 0 before the comparison, each branch "
67
+ "gate's decomposition verified separately up to global phase — the deferred-measurement model "
68
+ "is sensitive to the global phase of gates inside classical branches (z vs rz(pi))")
69
+ if kind == "observational":
70
+ method += ("; observational equivalence (QCEC partial equivalence): the unitaries differ, but by nothing "
71
+ f"the {partial_detail['measured_qubits']} measured qubit(s) can observe — identical measurement statistics; "
72
+ f"partial check: {PARTIAL_CHECKERS}")
73
+ report = {
74
+ "schema": "secqdevops.equivalence/v1",
75
+ "release_id": ctx.release_id,
76
+ "tool": tool,
77
+ "method": method,
78
+ "criterion": criterion,
79
+ "verdict": verdict,
80
+ "equivalence_kind": kind,
81
+ "circuits": {"source": "quantum/circuit.qpy", "transpiled": "quantum/transpiled.qpy"},
82
+ **({"dynamic_check": dynamic_detail} if dynamic_detail else {}),
83
+ **({"partial_check": partial_detail} if partial_detail else {}),
84
+ }
85
+ raw = (json.dumps(report, indent=2, ensure_ascii=True) + "\n").encode("utf-8")
86
+ ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
87
+ (ctx.quantum_dir / "equivalence-report.json").write_bytes(raw)
88
+ ctx.state["equivalence_verdict"] = verdict
89
+
90
+ props = [
91
+ {"name": "secqdevops:equivalence-ref", "value": "quantum/equivalence-report.json"},
92
+ {"name": "secqdevops:equivalence-sha256", "value": C.sha256_bytes(raw)},
93
+ {"name": "secqdevops:equivalence-verdict", "value": verdict},
94
+ {"name": "secqdevops:equivalence-tool", "value": tool},
95
+ {"name": "secqdevops:equivalence-kind", "value": kind},
96
+ ]
97
+ return ActionResult(ID, PRODUCED, props)
98
+
99
+
100
+ def _criterion(result) -> str:
101
+ return str(result.equivalence).split(".")[-1]
102
+
103
+
104
+ def final_measurements(circuit) -> bool:
105
+ """True when the circuit measures at least one qubit and every measurement is
106
+ final (nothing but barriers after a measurement on its qubit) — the shape on
107
+ which measurement statistics are a meaningful notion of equivalence."""
108
+ measured: set = set()
109
+ index = {q: i for i, q in enumerate(circuit.qubits)}
110
+ for inst in circuit.data:
111
+ name = inst.operation.name
112
+ qubits = [index[q] for q in inst.qubits]
113
+ if name == "measure":
114
+ measured.update(qubits)
115
+ elif name != "barrier" and any(q in measured for q in qubits):
116
+ return False
117
+ return bool(measured)
118
+
119
+
120
+ # The partial check runs QCEC's portfolio without the ZX-calculus checker. ZX does not
121
+ # support partial equivalence (QCEC refuses method="zx" with it), yet the default
122
+ # portfolio still runs it: it compares the full unitaries, which are known to differ
123
+ # here, and says probably_not_equivalent. When that answer arrived before the decision
124
+ # diagrams', it conflicted with the simulation checker's "probably equivalent" and QCEC
125
+ # returned no_information — a correct level-3 compilation declared inconclusive in about
126
+ # 1 run in 100 (measured 24 Oct 2026, mqt.qcec 3.11.0). Without ZX the alternating
127
+ # decision-diagram checker, which is complete, decides every time.
128
+ PARTIAL_CHECKERS = "decision diagrams (alternating) + simulation; ZX-calculus off (it does not support partial equivalence)"
129
+
130
+
131
+ def partial_check(qcec, source, compiled):
132
+ """QCEC partial (observational) equivalence, the one configuration producer, service and drift sentinel share."""
133
+ return qcec.verify(source, compiled, check_partial_equivalence=True, run_zx_checker=False)
134
+
135
+
136
+ def verify_observational(qcec, source, transpiled, unitary_result):
137
+ """Re-check `not_equivalent` circuits for identical measurement statistics.
138
+
139
+ QCEC's partial equivalence treats measured qubits as the outputs that count
140
+ and ignores what no measurement observes (phases before a measurement,
141
+ unmeasured qubits). Returns (result, kind, detail): kind "observational" when
142
+ the partial check holds, otherwise the original result and "unitary".
143
+ """
144
+ second = partial_check(qcec, source, transpiled)
145
+ measured = len({q for inst in source.data if inst.operation.name == "measure" for q in inst.qubits})
146
+ detail = {
147
+ "unitary_criterion": _criterion(unitary_result),
148
+ "partial_criterion": _criterion(second),
149
+ "measured_qubits": measured,
150
+ "total_qubits": source.num_qubits,
151
+ "checkers": PARTIAL_CHECKERS,
152
+ }
153
+ if second.considered_equivalent():
154
+ return second, "observational", detail
155
+ if _criterion(second) != "not_equivalent":
156
+ # the partial check could not decide (no_information / timeout): no evidence either
157
+ # way, so the verdict is inconclusive (QNT-05 N-A), never a refutation. Found 20 Oct
158
+ # 2026 in CI: a correct level-3 compilation was declared not_equivalent and the
159
+ # service's re-derivation rightly disagreed.
160
+ # The drift sentinel already read it this way (_drift.meaning).
161
+ return second, "observational", detail
162
+ return unitary_result, "unitary", detail
163
+
164
+
165
+ def _collect_branch_ops(circuit, inside: bool, found: dict) -> None:
166
+ for inst in circuit.data:
167
+ op = inst.operation
168
+ blocks = getattr(op, "blocks", None)
169
+ if blocks:
170
+ for block in blocks:
171
+ _collect_branch_ops(block, True, found)
172
+ elif inside and op.name not in ("measure", "reset", "barrier", "delay"):
173
+ key = (op.name, tuple(round(float(p), 12) for p in op.params if isinstance(p, (int, float))), op.num_qubits)
174
+ found.setdefault(key, op)
175
+
176
+
177
+ def verify_dynamic(qcec, source, transpiled):
178
+ """QCEC on a dynamic circuit, with its one known blind spot closed.
179
+
180
+ QCEC's deferred-measurement transformation turns a classical branch into
181
+ quantum control. A gate's global phase is irrelevant under classical
182
+ control but becomes a relative phase under quantum control, so a body that
183
+ transpiles `z` to `rz(pi)` is reported `not_equivalent` although the
184
+ dynamic circuits agree (found 5 Oct 2026, mqt.qcec 3.10.1). The fix keeps
185
+ every step machine-checked: on `not_equivalent`, (1) decompose the source
186
+ to the basis at optimization level 0 (gate definitions only, no
187
+ optimisation), (2) verify each distinct gate inside a branch against its
188
+ own decomposition with QCEC in static mode (equivalent up to global phase
189
+ is exactly what classical control permits), (3) compare the decomposed
190
+ source with the transpiled circuit under the transformation. Equivalence
191
+ follows only if (2) and (3) both hold; a failed (2) makes the result
192
+ inconclusive, a failed (3) is a real `not_equivalent`.
193
+
194
+ Returns (result, detail-or-None); detail is recorded in the report.
195
+ """
196
+ from qiskit import QuantumCircuit, transpile
197
+
198
+ from .. import _collect as C
199
+
200
+ first = qcec.verify(source, transpiled, transform_dynamic_circuit=True)
201
+ if first.considered_equivalent() or _criterion(first) != "not_equivalent":
202
+ return first, None
203
+ decomposed = transpile(source, basis_gates=C.BASIS_GATES, optimization_level=0, seed_transpiler=C.SEED_TRANSPILER)
204
+ ops: dict = {}
205
+ _collect_branch_ops(source, False, ops)
206
+ gate_checks = []
207
+ unverified = []
208
+ for (name, params, n), op in sorted(ops.items(), key=lambda kv: kv[0][0]):
209
+ single = QuantumCircuit(n)
210
+ single.append(op, range(n))
211
+ single_decomposed = transpile(single, basis_gates=C.BASIS_GATES, optimization_level=0, seed_transpiler=C.SEED_TRANSPILER)
212
+ check = qcec.verify(single, single_decomposed)
213
+ gate_checks.append({"gate": name, "params": list(params), "criterion": _criterion(check)})
214
+ if not check.considered_equivalent():
215
+ unverified.append(name)
216
+ second = qcec.verify(decomposed, transpiled, transform_dynamic_circuit=True)
217
+ detail = {
218
+ "first_criterion": _criterion(first),
219
+ "decomposed_source": True,
220
+ "branch_gate_decompositions": gate_checks,
221
+ "decomposed_criterion": _criterion(second),
222
+ }
223
+ if unverified:
224
+ detail["unverified_gates"] = unverified
225
+ return second, detail
@@ -0,0 +1,49 @@
1
+ """record_provenance — build provenance and a manifest-digest record.
2
+
3
+ The `signature.json` written here is an honest, labelled placeholder
4
+ (`manifest-digest/unsigned`), not a cryptographic signature: real signing is
5
+ the `sign` action (sign_evidence.py), verified by CRA-SIGN-01.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from .base import PRODUCED, ActionResult, Context
10
+
11
+ ID = "record_provenance"
12
+
13
+
14
+ def run(ctx: Context) -> ActionResult:
15
+ from .. import _collect as C
16
+
17
+ provenance = {
18
+ "predicateType": "https://slsa.dev/provenance/v1",
19
+ # the same builder identity the producer's signed statement declares (_sign.BUILDER_ID)
20
+ "builder": {"id": "secqdevops/quantbom-sdk", "version": {"quantbom": C.sdk_version()}},
21
+ "buildType": "https://secqdevops/build/qrisp-qiskit",
22
+ "subject": [
23
+ {"name": "quantum/circuit.qpy", "digest": {"sha256": C.sha256_bytes(ctx.state["circuit_qpy"]).split(":", 1)[1]}},
24
+ {"name": "quantum/transpiled.qpy",
25
+ "digest": {"sha256": C.sha256_bytes(ctx.state["transpiled_qpy"]).split(":", 1)[1]}},
26
+ ],
27
+ "metadata": {
28
+ "release_id": ctx.release_id,
29
+ "qrisp_version": ctx.qrisp_version,
30
+ "qiskit_version": ctx.qiskit_version,
31
+ },
32
+ }
33
+ ctx.evidence_dir.mkdir(parents=True, exist_ok=True)
34
+ C._write_json(ctx.evidence_dir / "provenance.json", provenance)
35
+ signature = {
36
+ "signature_type": "manifest-digest/unsigned",
37
+ "subject": ctx.release_id,
38
+ "manifest_sha256": C.sha256_bytes(C.canonical_json(provenance).encode("utf-8")),
39
+ "note": "placeholder: replace with sigstore/in-toto or Ed25519 signing",
40
+ }
41
+ C._write_json(ctx.evidence_dir / "signature.json", signature)
42
+
43
+ props = [
44
+ {"name": "secqdevops:provenance-ref", "value": "evidence/provenance.json"},
45
+ {"name": "secqdevops:signature-ref", "value": "evidence/signature.json"},
46
+ {"name": "secqdevops:provenance-artifact-ref", "value": "evidence/provenance.json"},
47
+ {"name": "secqdevops:signature-artifact-ref", "value": "evidence/signature.json"},
48
+ ]
49
+ return ActionResult(ID, PRODUCED, props)
@@ -0,0 +1,53 @@
1
+ """verify_conversion — the framework-to-Qiskit translation is faithful (feeds QNT-06).
2
+
3
+ The comparison itself runs in the SDK (`_fidelity`), where the native
4
+ Cirq/Braket/PennyLane object is still in hand; this action records the report
5
+ verbatim — including `unfaithful` — for the gate to judge.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+
11
+ from .base import NOT_APPLICABLE, PRODUCED, ActionResult, Context
12
+
13
+ ID = "verify_conversion"
14
+
15
+
16
+ def run(ctx: Context) -> ActionResult:
17
+ ctx.state["conversion_verdict"] = None
18
+ if not ctx.conversion_fidelity:
19
+ return ActionResult(ID, NOT_APPLICABLE,
20
+ reason=f"no adapter-side translation for {ctx.source_format} input")
21
+ from .. import _collect as C
22
+
23
+ report = {**ctx.conversion_fidelity, "release_id": ctx.release_id}
24
+ verdict = str(report.get("verdict"))
25
+ raw = (json.dumps(report, indent=2, ensure_ascii=True) + "\n").encode("utf-8")
26
+ ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
27
+ (ctx.quantum_dir / "conversion-fidelity.json").write_bytes(raw)
28
+ ctx.state["conversion_verdict"] = verdict
29
+
30
+ props = [
31
+ {"name": "secqdevops:conversion-fidelity-ref", "value": "quantum/conversion-fidelity.json"},
32
+ {"name": "secqdevops:conversion-fidelity-sha256", "value": C.sha256_bytes(raw)},
33
+ {"name": "secqdevops:conversion-fidelity-verdict", "value": verdict},
34
+ # which method decided (K3): dense, statevector, decision-diagram, a '+'-joined set, or none
35
+ {"name": "secqdevops:conversion-fidelity-method", "value": str(report.get("method") or "none")},
36
+ ]
37
+ if ctx.native_export:
38
+ # the native program itself, as the framework exported it and as QNT-06
39
+ # compared it: hash-bound, signed, and bound to the report's own digest
40
+ # of the same bytes (CRA-INTEG-01), so the fingerprint is of the program,
41
+ # not only of a report about it
42
+ text, fmt = ctx.native_export
43
+ ext = "qasm" if str(fmt).startswith("openqasm") else "json" if str(fmt).endswith("json") else "txt"
44
+ native_raw = str(text).encode("utf-8")
45
+ (ctx.quantum_dir / f"native-program.{ext}").write_bytes(native_raw)
46
+ props += [
47
+ {"name": "secqdevops:native-program-ref", "value": f"quantum/native-program.{ext}"},
48
+ {"name": "secqdevops:native-program-sha256", "value": C.sha256_bytes(native_raw)},
49
+ {"name": "secqdevops:native-program-format", "value": str(fmt)},
50
+ ]
51
+ # an inconclusive report says why, so QNT-06's N-A names the real reason
52
+ why = f"inconclusive: {report.get('reason')}" if verdict == "inconclusive" and report.get("reason") else None
53
+ return ActionResult(ID, PRODUCED, props, reason=why)