pgc-assembler 2.0.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.
assembler/VERSION ADDED
@@ -0,0 +1 @@
1
+ 14
assembler/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """PGC Snapshot Assembler — composes compiled projections into a manifest-pinned snapshot.
2
+
3
+ Contract: snapshot_assembler/doc/SNAPSHOT_ASSEMBLY_CONTRACT.md
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+
9
+ def _release() -> str:
10
+ """The composition's release ordinal, read from the repo's single version declaration.
11
+
12
+ PGC versions the *composition*, not each repo independently: all repos are released together
13
+ and the governance closure forces lockstep, so a monotonic integer names which composition a
14
+ repo belongs to. `VERSION` is the sole declaration — pyproject derives it, and so does this.
15
+ Never restate it as a literal; two statements of one fact is how they drift apart.
16
+ """
17
+ # Source tree and editable installs: the repo-root VERSION is authoritative.
18
+ declared = Path(__file__).resolve().parent.parent / "VERSION"
19
+ if declared.is_file():
20
+ return declared.read_text(encoding="utf-8").strip()
21
+ # Installed wheel: no repo root. The ordinal is staged into the package at
22
+ # build time by _build_hook.py — a build artifact, not a second declaration.
23
+ return (Path(__file__).resolve().parent / "VERSION").read_text(encoding="utf-8").strip()
24
+
25
+
26
+ ASSEMBLER_VERSION = _release() # derived from VERSION — never edited here
27
+ MANIFEST_VERSION = "v0"
assembler/cli.py ADDED
@@ -0,0 +1,112 @@
1
+ """
2
+ cli.py — PGC snapshot assembler CLI.
3
+
4
+ assemble — compose compiled projections into the assembled snapshot + manifest,
5
+ then run Composition Conformance over the result
6
+ verify — verify an assembled snapshot against its manifest (root of trust)
7
+ conform — run Composition Conformance alone against an assembled snapshot
8
+
9
+ Assembly and Composition Conformance are distinct lifecycle phases, not one step: the assembler
10
+ composes and proves identity; the conformance phase proves governance properties of the
11
+ composition. `assemble` runs both because an unproven snapshot should never be left on disk
12
+ looking finished — but each is separately invocable, and neither implements the other.
13
+
14
+ Paths are explicit or resolved from documented sibling defaults. No cwd guessing.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ from assembler import conformance, core
26
+
27
+
28
+ def _build_parser() -> argparse.ArgumentParser:
29
+ p = argparse.ArgumentParser(prog="snapshot_assembler", description="PGC snapshot assembler")
30
+ subs = p.add_subparsers(dest="command", required=True)
31
+
32
+ a = subs.add_parser("assemble", help="Compose compiled projections into the assembled snapshot")
33
+ a.add_argument(
34
+ "--source", action="append", required=True, metavar="COMPILED_ROOT",
35
+ help="A compiler compiled/ root (repeatable). e.g. .../software_governance/snapshot/compiled",
36
+ )
37
+ a.add_argument(
38
+ "--out", required=True, metavar="SNAPSHOT_DIR",
39
+ help="Assembled snapshot output dir (the product). e.g. .../protocol-governed-computing/snapshot",
40
+ )
41
+ a.add_argument(
42
+ "--profile", default=os.environ.get("PGC_SNAPSHOT_PROFILE", ""), metavar="PROFILE_IDENTITY",
43
+ help="The profile identity this snapshot claims (3b SN-5). Required; a snapshot that claims "
44
+ "none cannot have clause 4 of 3b §7 evaluated about it.",
45
+ )
46
+
47
+ v = subs.add_parser("verify", help="Verify an assembled snapshot against its manifest")
48
+ v.add_argument("--out", required=True, metavar="SNAPSHOT_DIR", help="Assembled snapshot dir to verify")
49
+
50
+ c = subs.add_parser("conform", help="Run Composition Conformance against an assembled snapshot")
51
+ c.add_argument("--out", required=True, metavar="SNAPSHOT_DIR", help="Assembled snapshot dir to check")
52
+
53
+ return p
54
+
55
+
56
+ def _fatal(msg: str) -> None:
57
+ print(f"[assembler] Error: {msg}", file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+
61
+ def main() -> None:
62
+ args = _build_parser().parse_args()
63
+
64
+ if args.command == "assemble":
65
+ source_roots = [Path(s).resolve() for s in args.source]
66
+ out_root = Path(args.out).resolve()
67
+ for s in source_roots:
68
+ if not s.is_dir():
69
+ _fatal(f"source root is not a directory: {s}")
70
+ try:
71
+ manifest = core.assemble(source_roots, out_root, args.profile)
72
+ core.verify_snapshot(out_root) # round-trip self-check
73
+ except core.AssemblyError as exc:
74
+ _fatal(str(exc))
75
+
76
+ doms = [d["domain"] for d in manifest["domains"]]
77
+ print(f"[assembler] Assembled {len(doms)} domain(s): {', '.join(doms)}")
78
+ print(f"[assembler] snapshot_id: {manifest['snapshot_id']}")
79
+ print(f"[assembler] out: {out_root}")
80
+ print(f"[assembler] manifest: {out_root / 'manifest.json'}")
81
+ print("[assembler] round-trip verify: OK")
82
+
83
+ # Composition Conformance — a separate phase over the composed snapshot.
84
+ try:
85
+ ev = conformance.check_composition(out_root)
86
+ except conformance.ConformanceError as exc:
87
+ _fatal(str(exc))
88
+ print(f"[conformance] composition: {ev['status']} "
89
+ f"({ev['rules_evaluated']} rule(s) over {ev['artifacts_examined']} artifacts)")
90
+
91
+ elif args.command == "verify":
92
+ out_root = Path(args.out).resolve()
93
+ try:
94
+ manifest = core.verify_snapshot(out_root)
95
+ except core.AssemblyError as exc:
96
+ _fatal(str(exc))
97
+ print(f"[assembler] VERIFIED snapshot_id={manifest['snapshot_id']}")
98
+ print(f"[assembler] domains: {json.dumps([d['domain'] for d in manifest['domains']])}")
99
+
100
+ elif args.command == "conform":
101
+ out_root = Path(args.out).resolve()
102
+ try:
103
+ ev = conformance.check_composition(out_root)
104
+ except conformance.ConformanceError as exc:
105
+ _fatal(str(exc))
106
+ print(f"[conformance] {ev['status']} snapshot_id={ev['snapshot_id']}")
107
+ for f in ev["findings"]:
108
+ print(f" {f['status']:<7} {f['invariant']:<58} {f['message']}")
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,246 @@
1
+ """
2
+ conformance.py — Composition Conformance: the lifecycle phase after assembly.
3
+
4
+ compiler → verified domain → assembler → composed snapshot
5
+ → COMPOSITION CONFORMANCE → attested snapshot
6
+
7
+ A domain is verified in isolation, against the governance surface it imported. Properties that
8
+ only exist once independently-governed domains are composed have no earlier enforcement point:
9
+ the compiler sees one domain at a time and never holds a snapshot, and the assembler is a
10
+ composition *engine* — it proves identity and integrity, not governance. This phase proves
11
+ properties **of the composition**.
12
+
13
+ Remit (the permanent home for this class of check):
14
+ * evaluate snapshot-scoped invariants [implemented]
15
+ * verify profile uniqueness [implemented — via declared composition_check]
16
+ * verify cross-domain uniqueness [remit, not yet declared]
17
+ * verify implementation resolvability [remit, not yet declared]
18
+ * verify composition contracts [remit, not yet declared]
19
+ * emit composition evidence [implemented]
20
+ * fail hard [implemented]
21
+
22
+ Two boundaries this module must keep:
23
+
24
+ 1. **No dependency on the compiler.** Rules are read from the assembled snapshot's own canonical
25
+ invariants, never from the compiler's handler registry. The phase is a peer of the compiler,
26
+ not a client of it.
27
+ 2. **No implementation semantics in the declaration.** An invariant declares a language-neutral
28
+ `composition_check` — a selector plus a cardinality rule. It does not name a handler, module,
29
+ or callable. Evaluating a declaration is this module's job; *being* one is the artifact's.
30
+
31
+ Admission is by declaration: an invariant participates iff it declares
32
+ `assert_projection.composition_check`. Nothing here enumerates which invariants exist.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import json
38
+ from dataclasses import dataclass
39
+ from datetime import datetime, timezone
40
+ from pathlib import Path
41
+ from typing import Any
42
+
43
+ from assembler import ASSEMBLER_VERSION
44
+
45
+ CONFORMANCE_VERSION = "v0"
46
+
47
+ # Cardinality rules this phase can evaluate. Extending the vocabulary is a declaration act in
48
+ # SCHEMA_INVARIANT_V0 plus one entry here — never a special case inside an evaluator.
49
+ _CARDINALITY = {
50
+ "exactly_one": lambda n: n == 1,
51
+ "at_least_one": lambda n: n >= 1,
52
+ "at_most_one": lambda n: n <= 1,
53
+ "none": lambda n: n == 0,
54
+ }
55
+
56
+ # What each rule requires, phrased for a diagnostic. A rule whose violation message described a
57
+ # different rule would send a reader looking for the wrong defect.
58
+ _REQUIREMENT = {
59
+ "exactly_one": "exactly one is required",
60
+ "at_least_one": "at least one is required",
61
+ "at_most_one": "at most one may be present",
62
+ "none": "none may be present",
63
+ }
64
+
65
+
66
+ class ConformanceError(RuntimeError):
67
+ """Raised when the composed snapshot violates a composition-scoped invariant. Fail hard."""
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class Finding:
72
+ invariant: str
73
+ subject: str
74
+ rule: str
75
+ status: str # PASSED | FAILED
76
+ examined: int # artifacts the selector matched — recorded so a vacuous check is visible
77
+ matched: list[str] # fqdn_ids of the matched artifacts
78
+ message: str
79
+
80
+
81
+ def _read_json(path: Path) -> dict:
82
+ try:
83
+ return json.loads(path.read_text(encoding="utf-8"))
84
+ except (OSError, json.JSONDecodeError) as exc:
85
+ raise ConformanceError(f"unreadable snapshot artifact {path}: {exc}") from exc
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Rule discovery — declared, never enumerated
90
+ # ---------------------------------------------------------------------------
91
+
92
+ def _canonical_artifacts(out_root: Path) -> list[dict]:
93
+ """Every canonical artifact in the composed snapshot, across all domains."""
94
+ canonical = out_root / "canonical"
95
+ if not canonical.is_dir():
96
+ raise ConformanceError(f"no canonical/ in snapshot: {out_root}")
97
+ return [_read_json(p) for p in sorted(canonical.glob("*/*/*.json"))
98
+ if p.name != "metadata.json"]
99
+
100
+
101
+ def _composition_rules(artifacts: list[dict]) -> list[tuple[str, dict]]:
102
+ """(invariant_fqdn, composition_check) for every invariant declaring one.
103
+
104
+ Read from the assembled snapshot itself, so the rules enforced are exactly the rules the
105
+ composition carries — a domain cannot be checked against governance it did not compile under.
106
+ """
107
+ rules = []
108
+ for a in artifacts:
109
+ if a.get("artifact_type") != "INVARIANT":
110
+ continue
111
+ proj = (a.get("frontmatter", {}) or {}).get("assert_projection", {}) or {}
112
+ check = proj.get("composition_check")
113
+ if check:
114
+ rules.append((a.get("fqdn_id", a.get("artifact_code", "?")), check))
115
+ return sorted(rules)
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Evaluation
120
+ # ---------------------------------------------------------------------------
121
+
122
+ def _declared(frontmatter: dict, path: str) -> Any:
123
+ """Read a declaration field by dotted path — `status`, or `handler.kind`.
124
+
125
+ A generalization of exact-match, not a special case: a single-segment path is the flat lookup
126
+ it always was. Nested declarations are ordinary structure in an artifact, and a selector that
127
+ could only reach the top level would force rules to be phrased against naming conventions
128
+ instead of against what an artifact actually declares.
129
+
130
+ A path that does not resolve yields None, which matches nothing — absence is not equality.
131
+ """
132
+ node: Any = frontmatter
133
+ for segment in path.split("."):
134
+ if not isinstance(node, dict):
135
+ return None
136
+ node = node.get(segment)
137
+ return node
138
+
139
+
140
+ def _matches(artifact: dict, selector: dict) -> bool:
141
+ """All declared selector fields must match. Unknown fields are a hard failure, not a pass."""
142
+ for field, expected in selector.items():
143
+ if field == "namespace":
144
+ if artifact.get("namespace") != expected:
145
+ return False
146
+ elif field == "artifact_type":
147
+ if artifact.get("artifact_type") != expected:
148
+ return False
149
+ elif field == "artifact_code_prefix":
150
+ if not str(artifact.get("artifact_code", "")).startswith(expected):
151
+ return False
152
+ elif field == "where":
153
+ fm = artifact.get("frontmatter", {}) or {}
154
+ for key, value in expected.items():
155
+ if _declared(fm, key) != value:
156
+ return False
157
+ else:
158
+ raise ConformanceError(
159
+ f"unknown composition_check selector field {field!r} — the declaration is admissible "
160
+ f"only if every field has an evaluator (no silent skip)"
161
+ )
162
+ return True
163
+
164
+
165
+ def _evaluate(invariant: str, check: dict, artifacts: list[dict]) -> Finding:
166
+ rule = check.get("rule")
167
+ predicate = _CARDINALITY.get(rule)
168
+ if predicate is None:
169
+ raise ConformanceError(
170
+ f"{invariant}: unknown composition_check rule {rule!r} "
171
+ f"(known: {', '.join(sorted(_CARDINALITY))})"
172
+ )
173
+ selector = check.get("selector") or {}
174
+ if not selector:
175
+ raise ConformanceError(f"{invariant}: composition_check declares an empty selector")
176
+
177
+ matched = sorted(a.get("fqdn_id", a.get("artifact_code", "?"))
178
+ for a in artifacts if _matches(a, selector))
179
+ subject = check.get("subject", "artifact")
180
+ ok = predicate(len(matched))
181
+ if ok:
182
+ message = f"{rule}: found {len(matched)} {subject}"
183
+ elif len(matched) == 0:
184
+ message = (f"{rule} violated: no {subject} found in the composed snapshot. "
185
+ f"The composition declares none; {_REQUIREMENT[rule]}.")
186
+ else:
187
+ plural = subject if subject.endswith("s") else f"{subject}s"
188
+ message = (f"{rule} violated: {len(matched)} competing {plural} in the composed snapshot "
189
+ f"— {', '.join(matched)}. Independently-governed domains have each declared one; "
190
+ f"exactly one may be active across the whole composition.")
191
+ return Finding(
192
+ invariant=invariant, subject=subject, rule=rule,
193
+ status="PASSED" if ok else "FAILED",
194
+ examined=len(matched), matched=matched, message=message,
195
+ )
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Phase entry point
200
+ # ---------------------------------------------------------------------------
201
+
202
+ def check_composition(out_root: Path) -> dict[str, Any]:
203
+ """Run Composition Conformance over an assembled snapshot.
204
+
205
+ Emits composition evidence to `conformance/composition.json` and raises ConformanceError on
206
+ any violation. Evidence is written before raising: a failed composition must leave a record
207
+ of *why* it failed, not just a non-zero exit.
208
+ """
209
+ artifacts = _canonical_artifacts(out_root)
210
+ rules = _composition_rules(artifacts)
211
+ findings = [_evaluate(fqdn, check, artifacts) for fqdn, check in rules]
212
+ failed = [f for f in findings if f.status == "FAILED"]
213
+
214
+ manifest_path = out_root / "manifest.json"
215
+ manifest = _read_json(manifest_path) if manifest_path.is_file() else {}
216
+
217
+ evidence = {
218
+ "conformance_version": CONFORMANCE_VERSION,
219
+ "assembler_version": ASSEMBLER_VERSION,
220
+ "phase": "composition_conformance",
221
+ "snapshot_id": manifest.get("snapshot_id"),
222
+ "domains": [d.get("domain") for d in manifest.get("domains", [])],
223
+ "artifacts_examined": len(artifacts),
224
+ "rules_evaluated": len(findings),
225
+ "status": "FAILED" if failed else "PASSED",
226
+ "findings": [
227
+ {
228
+ "invariant": f.invariant, "subject": f.subject, "rule": f.rule,
229
+ "status": f.status, "examined": f.examined,
230
+ "matched": f.matched, "message": f.message,
231
+ }
232
+ for f in findings
233
+ ],
234
+ "checked_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
235
+ }
236
+ out_dir = out_root / "conformance"
237
+ out_dir.mkdir(parents=True, exist_ok=True)
238
+ (out_dir / "composition.json").write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8")
239
+
240
+ if failed:
241
+ detail = "\n".join(f" [{f.invariant}] {f.message}" for f in failed)
242
+ raise ConformanceError(
243
+ f"composition conformance FAILED — {len(failed)} of {len(findings)} "
244
+ f"composition-scoped invariant(s) violated:\n{detail}"
245
+ )
246
+ return evidence