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 +1 -0
- assembler/__init__.py +27 -0
- assembler/cli.py +112 -0
- assembler/conformance.py +246 -0
- assembler/core.py +762 -0
- assembler/indexes.py +421 -0
- pgc_assembler-2.0.0.dist-info/METADATA +148 -0
- pgc_assembler-2.0.0.dist-info/RECORD +13 -0
- pgc_assembler-2.0.0.dist-info/WHEEL +5 -0
- pgc_assembler-2.0.0.dist-info/entry_points.txt +2 -0
- pgc_assembler-2.0.0.dist-info/licenses/LICENSE +67 -0
- pgc_assembler-2.0.0.dist-info/licenses/NOTICE +11 -0
- pgc_assembler-2.0.0.dist-info/top_level.txt +1 -0
assembler/indexes.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
indexes.py — cross-domain query indexes over the assembled snapshot.
|
|
3
|
+
|
|
4
|
+
Three inspection indexes, built AFTER composition (the assembler is the only point with the full
|
|
5
|
+
federated view of all domains — the PGC successor to RI-0's cross-structure `build` aggregation):
|
|
6
|
+
|
|
7
|
+
* artifact_index/index.json — FQDN → domain / kind / owner_subdomain / canonical_path /
|
|
8
|
+
evidence_paths / per-domain addresses. Consumed by `si`.
|
|
9
|
+
* kind_index/index.json — rich by-kind cross-reference (workflows / CCs / CTs / CSs / intents /
|
|
10
|
+
runtime_bindings / actors / events + cross-refs + vocabulary +
|
|
11
|
+
domain groupings). The si/tooling query database.
|
|
12
|
+
* store_index/index.json — store → owning storage STRUCTURE, declared path, and binding
|
|
13
|
+
surface (RB + CS + workflows + consumer CCs).
|
|
14
|
+
|
|
15
|
+
The assembler produces an INSPECTABLE snapshot, not merely an executable one: the compiler owns
|
|
16
|
+
the correctness of one domain, the assembler the correctness and indexing of the composition, and
|
|
17
|
+
`snapshot_inspector` the read-only query interface over it. An index is a composition-level fact —
|
|
18
|
+
no single domain build can compute one — which is why all three live here and none in the compiler.
|
|
19
|
+
|
|
20
|
+
Re-emission only: every fact is read from materialized projections in the consolidated snapshot
|
|
21
|
+
(canonical/<domain>, vocabulary/<domain>, evidence/<domain>). Zero re-derivation, deterministic
|
|
22
|
+
(sorted keys, no timestamps), fail-hard on malformed input.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
SCHEMA_VERSION = "v0"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Shared readers over the consolidated snapshot
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def _load_canonical(out_root: Path) -> dict[str, dict]:
|
|
39
|
+
"""fqdn → canonical artifact doc, across all domains (canonical/<domain>/<type>/*.json)."""
|
|
40
|
+
canon = out_root / "canonical"
|
|
41
|
+
docs: dict[str, dict] = {}
|
|
42
|
+
if not canon.is_dir():
|
|
43
|
+
return docs
|
|
44
|
+
for f in sorted(canon.rglob("*.json")):
|
|
45
|
+
if f.name == "metadata.json":
|
|
46
|
+
continue
|
|
47
|
+
raw = json.loads(f.read_text(encoding="utf-8"))
|
|
48
|
+
fqdn = raw.get("fqdn_id")
|
|
49
|
+
if fqdn and "::" in fqdn:
|
|
50
|
+
raw["_canonical_path"] = f.relative_to(out_root).as_posix()
|
|
51
|
+
docs[fqdn] = raw
|
|
52
|
+
return docs
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_membership(out_root: Path) -> dict[str, dict[str, str]]:
|
|
56
|
+
"""fqdn → {domain: hex_address}, from each vocabulary/<domain>/reverse.json."""
|
|
57
|
+
vocab = out_root / "vocabulary"
|
|
58
|
+
membership: dict[str, dict[str, str]] = {}
|
|
59
|
+
if not vocab.is_dir():
|
|
60
|
+
return membership
|
|
61
|
+
for d in sorted(p for p in vocab.iterdir() if p.is_dir()):
|
|
62
|
+
rev_path = d / "reverse.json"
|
|
63
|
+
if not rev_path.is_file():
|
|
64
|
+
continue
|
|
65
|
+
for fqdn, addr in json.loads(rev_path.read_text(encoding="utf-8")).items():
|
|
66
|
+
membership.setdefault(fqdn, {})[d.name] = addr
|
|
67
|
+
return membership
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _load_evidence_edges(out_root: Path) -> list[dict]:
|
|
71
|
+
"""All semantic edges from every evidence/<domain>/evidence.json.
|
|
72
|
+
|
|
73
|
+
`evidence.json` carries the SEMANTIC graph (WF_BINDS_RB, WF_CONTAINS_NODE, CC_BINDS_CS,
|
|
74
|
+
NODE_NEXT, …), keyed by FQDN. Its sibling `evidence_graph.json` is the COMPILE TRACE
|
|
75
|
+
(STAGE_SEQUENCE / CAUSALITY, keyed by event id) and holds none of those kinds — reading it
|
|
76
|
+
here yielded zero matches and left every consumer's cross-reference silently empty.
|
|
77
|
+
"""
|
|
78
|
+
evidence = out_root / "evidence"
|
|
79
|
+
edges: list[dict] = []
|
|
80
|
+
if not evidence.is_dir():
|
|
81
|
+
return edges
|
|
82
|
+
for eg in sorted(evidence.glob("*/evidence.json")):
|
|
83
|
+
data = json.loads(eg.read_text(encoding="utf-8"))
|
|
84
|
+
edges.extend(data.get("edges", []))
|
|
85
|
+
return edges
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_artifact_index(out_root: Path) -> dict[str, Any]:
|
|
89
|
+
docs = _load_canonical(out_root)
|
|
90
|
+
membership = _load_membership(out_root)
|
|
91
|
+
evidence_root = out_root / "evidence"
|
|
92
|
+
|
|
93
|
+
artifacts: dict[str, dict] = {}
|
|
94
|
+
for fqdn, raw in docs.items():
|
|
95
|
+
domain = fqdn.split("::", 1)[0]
|
|
96
|
+
scopes = membership.get(fqdn, {})
|
|
97
|
+
evidence_paths = {}
|
|
98
|
+
for scope in sorted(scopes):
|
|
99
|
+
eg_rel = f"evidence/{scope}/evidence_graph.json"
|
|
100
|
+
if (evidence_root / scope / "evidence_graph.json").is_file():
|
|
101
|
+
evidence_paths[scope] = eg_rel
|
|
102
|
+
artifacts[fqdn] = {
|
|
103
|
+
"domain": domain,
|
|
104
|
+
"kind": raw.get("artifact_type"),
|
|
105
|
+
"owner_subdomain": (raw.get("frontmatter") or {}).get("concern") or None,
|
|
106
|
+
"canonical_path": raw["_canonical_path"],
|
|
107
|
+
"evidence_paths": evidence_paths,
|
|
108
|
+
"addresses": {s: scopes[s] for s in sorted(scopes)},
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
"schema_version": SCHEMA_VERSION,
|
|
112
|
+
"generated_by": "snapshot_assembler",
|
|
113
|
+
"artifact_count": len(artifacts),
|
|
114
|
+
"artifacts": dict(sorted(artifacts.items())),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# kind_index (si query database)
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def build_kind_index(out_root: Path) -> dict[str, Any]:
|
|
123
|
+
docs = _load_canonical(out_root)
|
|
124
|
+
edges = _load_evidence_edges(out_root)
|
|
125
|
+
|
|
126
|
+
workflows: dict[str, dict] = {}
|
|
127
|
+
capability_contracts: dict[str, dict] = {}
|
|
128
|
+
capability_transforms: dict[str, dict] = {}
|
|
129
|
+
capability_side_effects: dict[str, dict] = {}
|
|
130
|
+
intents: dict[str, dict] = {}
|
|
131
|
+
runtime_bindings: dict[str, dict] = {}
|
|
132
|
+
actors: dict[str, dict] = {}
|
|
133
|
+
events: dict[str, dict] = {}
|
|
134
|
+
|
|
135
|
+
for fqdn, doc in docs.items():
|
|
136
|
+
atype = doc.get("artifact_type", "")
|
|
137
|
+
ns = doc.get("namespace", fqdn.split("::")[0])
|
|
138
|
+
fm = doc.get("frontmatter", {})
|
|
139
|
+
core = fm.get("core", {})
|
|
140
|
+
base = {
|
|
141
|
+
"fqdn": fqdn, "namespace": ns, "code": fqdn.split("::")[-1],
|
|
142
|
+
"version": fm.get("version", "v0"), "raw": doc,
|
|
143
|
+
}
|
|
144
|
+
if atype == "WF":
|
|
145
|
+
workflows[fqdn] = {**base, "subdomain": fm.get("subdomain", ""),
|
|
146
|
+
"summary": core.get("summary", ""), "start_node": core.get("start_node", ""),
|
|
147
|
+
"nodes": core.get("nodes", {}), "actor_context": core.get("actor_context", "")}
|
|
148
|
+
elif atype == "CC":
|
|
149
|
+
rsc = core.get("result_status_contract", {})
|
|
150
|
+
capability_contracts[fqdn] = {**base, "summary": core.get("summary", ""),
|
|
151
|
+
"outcomes": rsc.get("allowed", []), "pipeline": core.get("pipeline", []),
|
|
152
|
+
"inputs": core.get("inputs", {}), "outputs": core.get("outputs", {})}
|
|
153
|
+
elif atype == "CT":
|
|
154
|
+
machine = fm.get("machine", {})
|
|
155
|
+
capability_transforms[fqdn] = {**base, "summary": core.get("summary", fm.get("description", "")),
|
|
156
|
+
"purity": machine.get("ct_purity", "ct_pure"),
|
|
157
|
+
"inputs": core.get("inputs", {}), "outputs": core.get("outputs", {})}
|
|
158
|
+
elif atype == "CS":
|
|
159
|
+
capability_side_effects[fqdn] = {**base, "operations": core.get("operations", {})}
|
|
160
|
+
elif atype == "IN":
|
|
161
|
+
intents[fqdn] = base
|
|
162
|
+
elif atype == "RB":
|
|
163
|
+
runtime_bindings[fqdn] = {**base, "bindings": core.get("bindings", {})}
|
|
164
|
+
elif atype == "AC":
|
|
165
|
+
actors[fqdn] = {**base, "type": core.get("type", ""), "attributes": core.get("attributes", {})}
|
|
166
|
+
elif atype == "EV":
|
|
167
|
+
events[fqdn] = {**base, "schema": core.get("schema", {})}
|
|
168
|
+
|
|
169
|
+
# --- cross-references ---
|
|
170
|
+
wf_to_ccs = {
|
|
171
|
+
wf: [n.get("fqdn_id", f"{w['namespace']}::{code}")
|
|
172
|
+
for code, n in w["nodes"].items() if n.get("type") == "CC"]
|
|
173
|
+
for wf, w in workflows.items()
|
|
174
|
+
}
|
|
175
|
+
cc_outcomes = {cc: c["outcomes"] for cc, c in capability_contracts.items()}
|
|
176
|
+
cc_to_ct_cs: dict[str, list[str]] = {}
|
|
177
|
+
for cc, c in capability_contracts.items():
|
|
178
|
+
cc_to_ct_cs[cc] = [s["transform"] for s in c["pipeline"] if "transform" in s]
|
|
179
|
+
cc_upstream: dict[str, list[str]] = {}
|
|
180
|
+
cc_downstream: dict[str, list[str]] = {}
|
|
181
|
+
for edge in edges:
|
|
182
|
+
k, src, tgt = edge.get("kind", ""), edge.get("source_fqdn", ""), edge.get("target_fqdn", "")
|
|
183
|
+
if k == "NODE_NEXT" and src and tgt:
|
|
184
|
+
cc_downstream.setdefault(src, [])
|
|
185
|
+
if tgt not in cc_downstream[src]:
|
|
186
|
+
cc_downstream[src].append(tgt)
|
|
187
|
+
cc_upstream.setdefault(tgt, [])
|
|
188
|
+
if src not in cc_upstream[tgt]:
|
|
189
|
+
cc_upstream[tgt].append(src)
|
|
190
|
+
|
|
191
|
+
# --- domain / subdomain groupings ---
|
|
192
|
+
domains: dict[str, list[str]] = {}
|
|
193
|
+
subdomains: dict[str, list[str]] = {}
|
|
194
|
+
for fqdn in docs:
|
|
195
|
+
domains.setdefault(fqdn.split("::")[0], []).append(fqdn)
|
|
196
|
+
for wf, w in workflows.items():
|
|
197
|
+
if w["subdomain"]:
|
|
198
|
+
subdomains.setdefault(w["subdomain"], []).append(wf)
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"schema_version": SCHEMA_VERSION,
|
|
202
|
+
"generated_by": "snapshot_assembler",
|
|
203
|
+
"workflows": dict(sorted(workflows.items())),
|
|
204
|
+
"capability_contracts": dict(sorted(capability_contracts.items())),
|
|
205
|
+
"capability_transforms": dict(sorted(capability_transforms.items())),
|
|
206
|
+
"capability_side_effects": dict(sorted(capability_side_effects.items())),
|
|
207
|
+
"intents": dict(sorted(intents.items())),
|
|
208
|
+
"runtime_bindings": dict(sorted(runtime_bindings.items())),
|
|
209
|
+
"actors": dict(sorted(actors.items())),
|
|
210
|
+
"events": dict(sorted(events.items())),
|
|
211
|
+
"cross_references": {
|
|
212
|
+
"wf_to_ccs": dict(sorted(wf_to_ccs.items())),
|
|
213
|
+
"cc_to_ct_cs": dict(sorted(cc_to_ct_cs.items())),
|
|
214
|
+
"cc_outcomes": dict(sorted(cc_outcomes.items())),
|
|
215
|
+
"cc_upstream": dict(sorted(cc_upstream.items())),
|
|
216
|
+
"cc_downstream": dict(sorted(cc_downstream.items())),
|
|
217
|
+
},
|
|
218
|
+
"domains": {d: sorted(v) for d, v in sorted(domains.items())},
|
|
219
|
+
"subdomains": {s: sorted(v) for s, v in sorted(subdomains.items())},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# store_index (storage ownership + binding surface)
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
_DATA_ROOT_TEMPLATE = "{{module_data_root}}/"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def build_store_index(out_root: Path) -> dict[str, Any]:
|
|
231
|
+
"""Materialize the store-ownership join the composed snapshot already declares in three places:
|
|
232
|
+
|
|
233
|
+
storage STRUCTURE artifacts → core.entity_stores (store name → data path)
|
|
234
|
+
RB artifacts → core.bindings (CS → policy path)
|
|
235
|
+
evidence.json → WF_BINDS_RB, WF_CONTAINS_NODE, CC_BINDS_CS
|
|
236
|
+
|
|
237
|
+
into: store → owning structure, declared path, binding surface (RB + CS + workflows +
|
|
238
|
+
consumer CCs). Re-emission of declared facts only; deterministic; no policing.
|
|
239
|
+
"""
|
|
240
|
+
docs = _load_canonical(out_root)
|
|
241
|
+
stores = _declared_stores(docs)
|
|
242
|
+
rb_paths = _rb_store_paths(docs)
|
|
243
|
+
cc_stores = _cc_declared_stores(docs)
|
|
244
|
+
|
|
245
|
+
wf_binds_rb: dict[str, set] = {}
|
|
246
|
+
wf_contains: dict[str, set] = {}
|
|
247
|
+
cc_binds_cs: dict[str, set] = {}
|
|
248
|
+
for edge in _load_evidence_edges(out_root):
|
|
249
|
+
kind, src, tgt = edge.get("kind"), edge.get("source_fqdn"), edge.get("target_fqdn")
|
|
250
|
+
if not src or not tgt:
|
|
251
|
+
continue
|
|
252
|
+
if kind == "WF_BINDS_RB":
|
|
253
|
+
wf_binds_rb.setdefault(src, set()).add(tgt)
|
|
254
|
+
elif kind == "WF_CONTAINS_NODE":
|
|
255
|
+
wf_contains.setdefault(src, set()).add(tgt)
|
|
256
|
+
elif kind == "CC_BINDS_CS":
|
|
257
|
+
cc_binds_cs.setdefault(src, set()).add(tgt)
|
|
258
|
+
|
|
259
|
+
def bindings_for(path: str, store_name: str) -> list[dict[str, Any]]:
|
|
260
|
+
bindings: list[dict[str, Any]] = []
|
|
261
|
+
for (rb_fqdn, cs_fqdn), declared_paths in sorted(rb_paths.items()):
|
|
262
|
+
if path not in declared_paths:
|
|
263
|
+
continue
|
|
264
|
+
# Which contracts consume *this* store, not every store the binding reaches. A binding
|
|
265
|
+
# declared through a storage structure reaches every path that structure owns, so
|
|
266
|
+
# asking the binding alone answers "which contracts touch this domain's storage" and
|
|
267
|
+
# reports identical consumers for three different stores. A contract names the store
|
|
268
|
+
# each of its steps uses, and that is the fact being asked for.
|
|
269
|
+
# Scoped to what this binding actually reaches: the workflows bound to this RB,
|
|
270
|
+
# the contracts they contain, and of those the ones binding this capability.
|
|
271
|
+
rb_workflows = {wf for wf, rbs in wf_binds_rb.items() if rb_fqdn in rbs}
|
|
272
|
+
candidates = {cc for wf in rb_workflows
|
|
273
|
+
for cc in wf_contains.get(wf, set())
|
|
274
|
+
if cs_fqdn in cc_binds_cs.get(cc, set())}
|
|
275
|
+
if len(declared_paths) > 1:
|
|
276
|
+
# The binding reaches every store its structure owns, so it cannot say which one a
|
|
277
|
+
# contract used; the contract's own step declaration can. Filtering only here keeps
|
|
278
|
+
# a binding naming one concrete path — where the path *is* the store — answering for
|
|
279
|
+
# contracts that never needed to name it.
|
|
280
|
+
candidates = {cc for cc in candidates
|
|
281
|
+
if store_name in cc_stores.get(cc, set())}
|
|
282
|
+
consumer_ccs = sorted(candidates)
|
|
283
|
+
workflows = sorted({wf for wf in rb_workflows
|
|
284
|
+
if wf_contains.get(wf, set()) & candidates})
|
|
285
|
+
bindings.append({
|
|
286
|
+
"rb": rb_fqdn,
|
|
287
|
+
"cs": cs_fqdn,
|
|
288
|
+
"workflows": workflows,
|
|
289
|
+
"consumer_ccs": consumer_ccs,
|
|
290
|
+
})
|
|
291
|
+
return bindings
|
|
292
|
+
|
|
293
|
+
indexed = {
|
|
294
|
+
key: {
|
|
295
|
+
"store": store["store"],
|
|
296
|
+
"domain": store["domain"],
|
|
297
|
+
"declarations": [
|
|
298
|
+
{**declaration, "bindings": bindings_for(declaration["path"], store["store"])}
|
|
299
|
+
for declaration in store["declarations"]
|
|
300
|
+
],
|
|
301
|
+
}
|
|
302
|
+
for key, store in stores.items()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"schema_version": SCHEMA_VERSION,
|
|
307
|
+
"generated_by": "snapshot_assembler",
|
|
308
|
+
"store_count": len(indexed),
|
|
309
|
+
"stores": dict(sorted(indexed.items())),
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _declared_stores(docs: dict[str, dict]) -> dict[str, dict[str, Any]]:
|
|
314
|
+
"""Stores declared via core.entity_stores in storage STRUCTUREs, keyed '<domain>::<STORE>'.
|
|
315
|
+
|
|
316
|
+
A store name may be declared by more than one storage STRUCTURE in a domain — with the same
|
|
317
|
+
path (a shared store) or different paths (per-subdomain stores sharing a name). Each
|
|
318
|
+
declaration is recorded as the protocol states it: no merging, no policing.
|
|
319
|
+
"""
|
|
320
|
+
stores: dict[str, dict[str, Any]] = {}
|
|
321
|
+
for fqdn, doc in docs.items():
|
|
322
|
+
if doc.get("artifact_type") != "STRUCTURE":
|
|
323
|
+
continue
|
|
324
|
+
core = doc.get("frontmatter", {}).get("core", {})
|
|
325
|
+
entity_stores = core.get("entity_stores")
|
|
326
|
+
if not entity_stores:
|
|
327
|
+
continue
|
|
328
|
+
domain = core.get("domain") or fqdn.split("::", 1)[0]
|
|
329
|
+
for store_name in sorted(entity_stores):
|
|
330
|
+
declared = entity_stores[store_name]
|
|
331
|
+
entry = stores.setdefault(
|
|
332
|
+
f"{domain}::{store_name}",
|
|
333
|
+
{"store": store_name, "domain": domain, "declarations": []},
|
|
334
|
+
)
|
|
335
|
+
entry["declarations"].append({
|
|
336
|
+
"path": declared.get("path", ""),
|
|
337
|
+
"description": declared.get("description", ""),
|
|
338
|
+
"declared_by": fqdn,
|
|
339
|
+
})
|
|
340
|
+
for entry in stores.values():
|
|
341
|
+
entry["declarations"].sort(key=lambda d: (d["path"], d["declared_by"]))
|
|
342
|
+
return stores
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _cc_declared_stores(docs: dict[str, dict]) -> dict[str, set[str]]:
|
|
346
|
+
"""CC fqdn → the store names its pipeline steps declare.
|
|
347
|
+
|
|
348
|
+
A contract states the store each side-effect step reaches. Nothing else in the composition
|
|
349
|
+
records which store a contract uses: the runtime binding names a capability and a structure,
|
|
350
|
+
and the evidence graph records that a contract binds a capability — neither says which of the
|
|
351
|
+
structure's stores the contract writes.
|
|
352
|
+
"""
|
|
353
|
+
out: dict[str, set[str]] = {}
|
|
354
|
+
for fqdn, doc in docs.items():
|
|
355
|
+
if doc.get("artifact_type") != "CC":
|
|
356
|
+
continue
|
|
357
|
+
pipeline = doc.get("frontmatter", {}).get("core", {}).get("pipeline") or []
|
|
358
|
+
named = {step.get("store") for step in pipeline
|
|
359
|
+
if isinstance(step, dict) and step.get("store")}
|
|
360
|
+
if named:
|
|
361
|
+
out[fqdn] = named
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _rb_store_paths(docs: dict[str, dict]) -> dict[tuple[str, str], set[str]]:
|
|
366
|
+
"""(RB fqdn, CS fqdn) → the data paths that binding reaches.
|
|
367
|
+
|
|
368
|
+
A binding declares where its capability writes in one of two ways, and reading only the first
|
|
369
|
+
left this join blind to fourteen of the composition's fifteen stores:
|
|
370
|
+
|
|
371
|
+
policy.path one concrete path, with the data-root template prefix stripped
|
|
372
|
+
policy.structure a storage STRUCTURE, whose `entity_stores` declare every path it owns
|
|
373
|
+
|
|
374
|
+
The second is what every pipeline-authored domain uses, and the reference workload besides;
|
|
375
|
+
only `ai_governance` names paths in its policies. Resolving just the concrete form meant
|
|
376
|
+
`si.store.consumers` answered for that one domain and reported no consumer for every other
|
|
377
|
+
store in the composition — including stores three contracts demonstrably write.
|
|
378
|
+
|
|
379
|
+
A binding whose policy declares neither binds no store (CS_CLOCK_V0 under any RB is the
|
|
380
|
+
standing example) and contributes nothing to the join.
|
|
381
|
+
"""
|
|
382
|
+
paths: dict[tuple[str, str], set[str]] = {}
|
|
383
|
+
for fqdn, doc in docs.items():
|
|
384
|
+
if doc.get("artifact_type") != "RB":
|
|
385
|
+
continue
|
|
386
|
+
core = doc.get("frontmatter", {}).get("core", {})
|
|
387
|
+
bindings = core.get("bindings", {})
|
|
388
|
+
for cs_fqdn in sorted(bindings):
|
|
389
|
+
policy = bindings[cs_fqdn].get("policy") or {}
|
|
390
|
+
declared: set[str] = set()
|
|
391
|
+
|
|
392
|
+
concrete = policy.get("path")
|
|
393
|
+
if concrete:
|
|
394
|
+
if concrete.startswith(_DATA_ROOT_TEMPLATE):
|
|
395
|
+
concrete = concrete[len(_DATA_ROOT_TEMPLATE):]
|
|
396
|
+
declared.add(concrete)
|
|
397
|
+
|
|
398
|
+
# A binding may declare its structure, or lean on the one the runtime binding declares
|
|
399
|
+
# for all of them. Reading only the per-binding form left every store whose binding
|
|
400
|
+
# carries an empty policy unreachable, though the RB says plainly where it writes.
|
|
401
|
+
# A binding may declare its structure, or lean on the one the runtime binding declares
|
|
402
|
+
# for all of them — but only when it names no path of its own. A binding that names one
|
|
403
|
+
# concrete path has already said where it writes, and adding its structure's other
|
|
404
|
+
# paths on top would make an unambiguous binding look like it reached them all.
|
|
405
|
+
structure = policy.get("structure") or (None if declared else core.get("storage_structure"))
|
|
406
|
+
if structure:
|
|
407
|
+
entity_stores = ((docs.get(structure) or {})
|
|
408
|
+
.get("frontmatter", {}).get("core", {}).get("entity_stores") or {})
|
|
409
|
+
declared.update(store.get("path") for store in entity_stores.values()
|
|
410
|
+
if isinstance(store, dict) and store.get("path"))
|
|
411
|
+
|
|
412
|
+
if declared:
|
|
413
|
+
paths[(fqdn, cs_fqdn)] = declared
|
|
414
|
+
return paths
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def write_index(out_root: Path, rel_path: str, content: dict[str, Any]) -> Path:
|
|
418
|
+
path = out_root / rel_path
|
|
419
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
420
|
+
path.write_text(json.dumps(content, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
421
|
+
return path
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pgc-assembler
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: PGC snapshot assembler — composes compiled projections into a manifest-pinned executable snapshot (import package: assembler)
|
|
5
|
+
Author-email: Bhash Ganti <bachipeachy@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://omnibachi.org/
|
|
8
|
+
Project-URL: Repository, https://github.com/protocol-governed-computing/snapshot_assembler
|
|
9
|
+
Project-URL: Standard, https://doi.org/10.5281/zenodo.22150616
|
|
10
|
+
Keywords: protocol-governed-computing,pgc,snapshot,assembler,manifest
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
License-File: NOTICE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# snapshot_assembler
|
|
26
|
+
|
|
27
|
+
**Protocol-Governed Computing — snapshot assembler** (import package: `assembler`).
|
|
28
|
+
|
|
29
|
+
Composes each domain's compiled projections (from the protocol compiler) into one **executable
|
|
30
|
+
snapshot** with a content-derived, **manifest-pinned identity**. The runtime consumes only the
|
|
31
|
+
assembled snapshot — never an individual repo's compiled layout.
|
|
32
|
+
|
|
33
|
+
> The assembly contract — what a compiled projection must look like for the assembler to
|
|
34
|
+
> accept it — is governed by the standard, not by this repository.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
repos → protocol_compiler → each repo's compiled/ projections
|
|
38
|
+
→ snapshot_assembler → protocol-governed-computing/snapshot/ → runtime warm reboot
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The platform is an ordinary member of the composition — no singleton branch. The composition
|
|
42
|
+
currently assembles **seven domains**: `platform`, `workload`, `inspection`, `transformation`,
|
|
43
|
+
and the business domains `ai_governance`, `blockchain` and `book_library_mgmt`.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pgc-assembler
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Once installed:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
snapshot_assembler --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Use
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# assemble sibling platform compiled/ -> sibling snapshot/
|
|
61
|
+
./assemble.sh
|
|
62
|
+
|
|
63
|
+
# explicit / multi-source (future domains)
|
|
64
|
+
./assemble.sh --source /abs/software_governance/snapshot/compiled --out /abs/protocol-governed-computing/snapshot
|
|
65
|
+
|
|
66
|
+
# module form
|
|
67
|
+
PYTHONPATH=. python -m assembler.cli assemble --source <compiled_root> --out <snapshot_dir>
|
|
68
|
+
PYTHONPATH=. python -m assembler.cli verify --out <snapshot_dir>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Product
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
protocol-governed-computing/snapshot/
|
|
75
|
+
manifest.json # COMMITTED — the identity + root of trust
|
|
76
|
+
tokenized/<domain>/ # regenerated build product (gitignored)
|
|
77
|
+
trust/<domain>/ # regenerated build product (gitignored)
|
|
78
|
+
vocabulary/<domain>/ # regenerated build product (gitignored)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- **`manifest.json` is the committed identity record.** The assembled projections are regenerated
|
|
82
|
+
build products whose contents MUST match the manifest during warm reboot.
|
|
83
|
+
- **`composite_hash`** is content-derived over the identity view of `domains[]` (per-domain
|
|
84
|
+
projection/attestation/graph hashes). Provenance and timestamps are excluded → same inputs +
|
|
85
|
+
same compiler + same assembler ⇒ same identity.
|
|
86
|
+
|
|
87
|
+
## Composition conformance
|
|
88
|
+
|
|
89
|
+
Assembly does not end at the manifest. **Composition Conformance** is the lifecycle phase after it:
|
|
90
|
+
rules that can only be asked of the whole — the ones a single domain build contains no evidence for.
|
|
91
|
+
It runs on every assemble and its result is written to `conformance/composition.json`, so a snapshot
|
|
92
|
+
carries the record of having been judged as a composition rather than as a pile of domains.
|
|
93
|
+
|
|
94
|
+
## Scope
|
|
95
|
+
|
|
96
|
+
- **Copy and pin:** each domain's projections are copied, hashes are lifted from compiler output, the
|
|
97
|
+
manifest is written, and round-trip `verify` runs after every `assemble`.
|
|
98
|
+
- **Vocabulary address-space reconciliation** remains the deferred real job. Collision detection is
|
|
99
|
+
enforced today; composing the reconciled forward/reverse maps across domains is not yet done, and
|
|
100
|
+
the composition has grown past the one-domain case that made it postponable.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
Apache-2.0.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## The package family
|
|
109
|
+
|
|
110
|
+
| Package | Repository | Role |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `pgc-compiler` | `protocol_compiler` | declarations → compiled projections |
|
|
113
|
+
| `pgc-assembler` | `snapshot_assembler` | projections → sealed snapshot |
|
|
114
|
+
| `pgc-runtime` | `protocol_runtime` | snapshot → governed execution |
|
|
115
|
+
| `pgc-inspector` | `snapshot_inspector` | snapshot → read-only inspection |
|
|
116
|
+
| `pgc-transformation` | `transformation` | change request → protocol artifacts |
|
|
117
|
+
| `pgc-governance` | `software_governance` | the governance surface and its capability implementations |
|
|
118
|
+
| `pgc-workloads` | `conformance_workloads` | the workloads that make conformance observable |
|
|
119
|
+
| `pgc-domains` | `business_domains` | the business domain implementations the composed snapshot binds |
|
|
120
|
+
|
|
121
|
+
`pip install pgc` brings in the whole family.
|
|
122
|
+
|
|
123
|
+
**Installing the toolchain is one of two steps.** The compiler resolves the governance surface from
|
|
124
|
+
`PGC_PLATFORM_ROOT` — fail-hard, cwd-independent, zero inference — so the *declarations* come from a
|
|
125
|
+
repository you point at, never from a wheel. A registry inside a package would be a second governance
|
|
126
|
+
surface competing with the repository's, and a build could then be governed by a stale copy.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone https://github.com/protocol-governed-computing/software_governance
|
|
130
|
+
export PGC_PLATFORM_ROOT=$PWD/software_governance
|
|
131
|
+
pgc # reports what is installed and whether the anchor resolves
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`PGC_BUILD_ROOT` (compiled output, keeping the governance repo read-only) and `PGC_DOMAIN_ROOTS`
|
|
135
|
+
(additional domains contributing their own `registry/structures`) are optional.
|
|
136
|
+
|
|
137
|
+
**Versioning.** Two schemes, and the published version follows the second.
|
|
138
|
+
|
|
139
|
+
- **Internal** — each repository's `VERSION` file, a monotonic composition ordinal. PGC versions the
|
|
140
|
+
composition rather than each repo: they release together and the governance closure forces lockstep,
|
|
141
|
+
so the ordinal names which composition a repo belongs to. Development happens on `dev/<N>` and each
|
|
142
|
+
cycle is tagged `release-<N>`. This is not published.
|
|
143
|
+
- **Public** — `PUBLIC_VERSION`, tagged on every component repository. The platform is at **`v2`**.
|
|
144
|
+
|
|
145
|
+
**The published version is the public one: `v2` is `2.0.0`.** The standard the packages implement is a
|
|
146
|
+
separate artifact on its own track and is not this number.
|
|
147
|
+
|
|
148
|
+
The standard these packages implement is published separately: https://doi.org/10.5281/zenodo.22150616
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
assembler/VERSION,sha256=mpKtvAzuOO9ljHHOGxv4xlZo8Wa_shNkTIlcyxrQeiU,3
|
|
2
|
+
assembler/__init__.py,sha256=W7Z98sqFkwwevJmmQa476GJ974aEajPD2d1epyZAqw0,1285
|
|
3
|
+
assembler/cli.py,sha256=nThjf3vwz1qmn4FZeG4c5uMBuSdaM6wtpcvp8W7L9D0,4690
|
|
4
|
+
assembler/conformance.py,sha256=zi2uDGTsMUdkoEGGHtdoP2dIQMimpHJ_MHDw2Qn5vUM,10597
|
|
5
|
+
assembler/core.py,sha256=55Sfd3HQxSKP4fW2X8a_ePSPdrVrC-GDsRbdKONY_yE,37010
|
|
6
|
+
assembler/indexes.py,sha256=e-0LEqMnE48seVIOJy9tvhDpFPFmTAlSk3FjADR2b8g,19922
|
|
7
|
+
pgc_assembler-2.0.0.dist-info/licenses/LICENSE,sha256=wf4s-Px76q2zHB2b8Gbvuu4ULua8AMds2IY01MkBn0A,2544
|
|
8
|
+
pgc_assembler-2.0.0.dist-info/licenses/NOTICE,sha256=nE4--l6r3rMCz0nVaFyFohZtY4NYADnrhcS9K2pdNr0,407
|
|
9
|
+
pgc_assembler-2.0.0.dist-info/METADATA,sha256=Zn2tY9ACsi1TGiNrzR2Hx1-eKXo5KTmHNufseUNgmXM,6651
|
|
10
|
+
pgc_assembler-2.0.0.dist-info/WHEEL,sha256=oFyoA3ogC3W5C0Id4sPJabNAAnDnz8g-u7AUl41-31I,90
|
|
11
|
+
pgc_assembler-2.0.0.dist-info/entry_points.txt,sha256=LHtd453be3lrx7dCgM52JKTLQTQAYldM3KqZzc-MI9Q,58
|
|
12
|
+
pgc_assembler-2.0.0.dist-info/top_level.txt,sha256=MjECpqT3FbgJ3vnCel7GSPTAy5WBRGZS6XNWlyTpqoI,10
|
|
13
|
+
pgc_assembler-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Bhash Ganti aka Bachi
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
------------------------
|
|
21
|
+
FULL LICENSE TEXT BELOW
|
|
22
|
+
------------------------
|
|
23
|
+
|
|
24
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
25
|
+
|
|
26
|
+
1. Definitions.
|
|
27
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
28
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
29
|
+
"Licensor" shall mean the copyright owner.
|
|
30
|
+
"Legal Entity" shall mean the union of the acting entity and all other
|
|
31
|
+
entities that control, are controlled by, or are under common control.
|
|
32
|
+
"You" shall mean an individual or Legal Entity exercising permissions.
|
|
33
|
+
"Source" form shall mean the preferred form for making modifications.
|
|
34
|
+
"Object" form shall mean any form resulting from mechanical transformation.
|
|
35
|
+
"Work" shall mean the work of authorship.
|
|
36
|
+
"Derivative Works" shall mean any work based on the Work.
|
|
37
|
+
"Contribution" shall mean any work intentionally submitted.
|
|
38
|
+
"Contributor" shall mean Licensor and any individual submitting Contributions.
|
|
39
|
+
|
|
40
|
+
2. Grant of Copyright License.
|
|
41
|
+
Each Contributor grants You a perpetual, worldwide, non-exclusive,
|
|
42
|
+
no-charge, royalty-free copyright license.
|
|
43
|
+
|
|
44
|
+
3. Grant of Patent License.
|
|
45
|
+
Each Contributor grants a patent license to make, use, sell, etc.
|
|
46
|
+
|
|
47
|
+
4. Redistribution.
|
|
48
|
+
You may reproduce and distribute copies provided that:
|
|
49
|
+
- You include a copy of this License
|
|
50
|
+
- You retain notices
|
|
51
|
+
- You state modifications
|
|
52
|
+
- You include NOTICE file if present
|
|
53
|
+
|
|
54
|
+
5. Submission of Contributions.
|
|
55
|
+
Contributions are under this License unless stated otherwise.
|
|
56
|
+
|
|
57
|
+
6. Trademarks.
|
|
58
|
+
This License does not grant trademark rights.
|
|
59
|
+
|
|
60
|
+
7. Disclaimer of Warranty.
|
|
61
|
+
Provided "AS IS", without warranties.
|
|
62
|
+
|
|
63
|
+
8. Limitation of Liability.
|
|
64
|
+
No liability for damages.
|
|
65
|
+
|
|
66
|
+
9. Accepting Warranty or Additional Liability.
|
|
67
|
+
You may offer support/warranty on your own behalf only.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
PGS — Protocol-Governed Systems
|
|
2
|
+
Copyright 2026 Bhash Ganti aka Bachi
|
|
3
|
+
|
|
4
|
+
This project introduces a protocol-first execution model in which:
|
|
5
|
+
|
|
6
|
+
- Behavior is declared in protocol artifacts
|
|
7
|
+
- Execution is performed by a deterministic runtime
|
|
8
|
+
- Capability implementations are bound at compile time
|
|
9
|
+
- Governance is enforced through invariants and assertions
|
|
10
|
+
|
|
11
|
+
Extensibility is achieved by declaration, not refactor.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
assembler
|