schemas: ArchitecturalBuildingBlock — role-named components, decoupled from vendors - #223
Merged
Merged
Conversation
…d from vendors
Adds the ABB abstraction the estate has been missing. Zurich E-RDA2 uses the same
pattern — ABB.03 = DATABASE has slots that DB2, ADLS, BLOB, XYZ or CaaS can fill — because
it lets a vendor swap be a one-manifest edit rather than a call-site rewrite. Without it,
'we use Postgres' becomes a load-bearing invariant across ~74 services.
An ABB names a FUNCTION (`DATABASE`, `IMPLEMENTATION CONTROLLER`), not a technology
(`PostgreSQL`, `MSTR`). Implementations declare `implementsAbb: ['ABB.03']` in their
MeshActionRegistry participant entry and immediately become swappable slot fillers.
Consumers derive the shape by walking MAR and filtering — never authored alongside the
consuming code, following the same discipline that keeps intent-grid derived from the canon
rather than shadowing it.
Two invariants enforced by shape (asserted by REJECTION, a validator never observed
refusing is indistinguishable from no validator):
1. abbId is `ABB.<two digits>`. Ids are stable — renumbering is a breaking change to every
manifest, so `supersedes: [ABB.03]` retires with a forwarding pointer rather than
renumbering in place.
2. protocol.reads and protocol.writes are declared explicitly (may be empty).
A missing verb list is the failure — a write-only ABB writes `reads: []` on purpose,
so 'I forgot' cannot masquerade as 'no reads'.
Plus a validator-side check that role names are FUNCTIONS not VENDORS: 'postgres',
'PostgreSQL', 'mysql', 'AWS', 'MSTR', 'kubernetes' as roles are all caught by a deny list.
Not exhaustive by design — the goal is to catch obvious slips, not enumerate every product.
Determined authors can still name a role poorly; review catches what this misses.
Consumers (in flight): prophet-mesh Michael Agent's `route_by_abb` capability,
Noetica intent-grid's ABB-aware cell derivation, prophet-platform libs/python/solution-ranker
(walks MAR for candidates), libs/python/access-prewalk (graded consent per candidate).
22 checks. `make validate` green. Proven able to fail: loosening the abbId pattern to `^.+$`
exits 1; restored, exits 0.
mdheller
added a commit
that referenced
this pull request
Jul 30, 2026
… their ABB slots (#224) Second move in the Michael-agent capability-integration work. #223 (ArchitecturalBuildingBlock) gave us role-named components; this lets a participant CLAIM the roles it fills. participants[*].implementsAbb: ["ABB.03", "ABB.07"] A consumer walking the registry for an ABB (Michael's `route_by_abb`, or solution-ranker's candidate shortlist) filters on this. Absent/empty means the participant is not claimed as a slot filler — the consumer won't consider it. This field only establishes the CLAIM, not its truth. Verification is the consumer's job: walk the ABB's protocol + requiredCapabilities, confirm the participant delivers each. That split is deliberate — a validator here can reject unknown `ABB.NN` patterns but cannot know whether a participant actually satisfies the protocol without running the caller. Making that split explicit stops any misplaced trust in "the registry validated this so it must work." Enforced by 7 new validator cases (unknown pattern, bare number, three-digit, wrong namespace, duplicates, empty ⇒ absent-shaped, present ⇒ well-shaped). Existing 15 shape cases still pass. `make validate` green. The example receipt now shows two participants claiming ABB.01/02 — so a consumer can already dry-run the walk and get a non-trivial shortlist.
There was a problem hiding this comment.
Pull request overview
Adds a new ArchitecturalBuildingBlock (ABB) normative schema + example and wires in validation/CI so ABB IDs are stable (ABB.NN) and protocol.reads/protocol.writes are explicitly declared, enabling role-based (“function, not vendor”) component substitution across the estate.
Changes:
- Introduces
ArchitecturalBuildingBlockJSON Schema with closed shape, stableabbIdpattern, role naming, and explicitprotocol.reads/writes. - Adds a shipped ABB example (
ABB.03/DATABASE) plus a Python validator with negative controls. - Wires validation into
make validateand a dedicated GitHub Actions workflow for ABB-related files.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/validate_architectural_building_block.py | Adds a schema+example validator with negative controls (but currently does not actually enforce vendor-role rejection as described). |
| schemas/ArchitecturalBuildingBlock.json | Defines the ABB contract (closed object, stable ID pattern, protocol verb lists, optional conformance pointer). |
| examples/architectural-building-block.example.json | Adds a canonical ABB example (ABB.03 DATABASE) with protocol verbs and required capabilities (currently references a missing conformance file). |
| Makefile | Adds validate-architectural-building-block target and wires it into validate. |
| .github/workflows/architectural-building-block.yml | Adds CI gate that runs the ABB validator on schema/example/validator/workflow changes. |
Comments suppressed due to low confidence (1)
tools/validate_architectural_building_block.py:110
- The INVARIANT 1 block only checks whether the regex matches a few sample strings; it never asserts that vendor-named roles are actually rejected by the validator. After enforcing vendor-role rejection in
case(), this section should usecase(..., should_pass=False)so CI will fail if the deny list regresses.
vendor_examples = ["postgres", "PostgreSQL", "mysql", "AWS", "MSTR", "kubernetes"]
vendor_failures = 0
for v in vendor_examples:
e = copy.deepcopy(example)
e["role"] = v
Comment on lines
+61
to
+69
| def case(label: str, instance: Any, should_pass: bool) -> None: | ||
| nonlocal checks | ||
| checks += 1 | ||
| errs = sorted(validator.iter_errors(instance), key=lambda e: list(e.path)) | ||
| accepted = not errs | ||
| if accepted != should_pass: | ||
| want = "accepted" if should_pass else "rejected" | ||
| got = "accepted" if accepted else f"rejected ({errs[0].message[:100]})" | ||
| failures.append(f"{label}: expected {want}, was {got}") |
Comment on lines
+11
to
+17
| "requiredCapabilities": [ | ||
| "at-rest-encryption", | ||
| "row-level-auth", | ||
| "audit-chain-append", | ||
| "point-in-time-recovery" | ||
| ], | ||
| "conformanceTest": "conformance/abb-database-vectors.json" |
| "abbId": { | ||
| "type": "string", | ||
| "pattern": "^ABB\\.[0-9]{2}$", | ||
| "description": "Two-digit ABB identifier (ABB.01, ABB.02, ..., ABB.99). Namespaced by role, not by vendor — vendor is a property of the implementation, not the block. ABBs are numbered stably: renumbering an ABB is a breaking change to every manifest that references it, so ids are assigned once and never reused." |
mdheller
added a commit
that referenced
this pull request
Jul 30, 2026
) #223 shipped three instances of the defect class it was written to close. 1. The vendor-role control never looked at a document. It built one (`e = deepcopy(example); e["role"] = v`) and then never used it, matching its regex against six hardcoded strings instead. With `role: "postgres"` in the shipped example the validator exited 0 AND printed "OK 6 vendor-named roles caught by the deny list". A control that reports catching what it has not looked at is worse than an absent one, because it produces evidence. 2. `role` was `{type: string, minLength: 1}`. A deny list cannot prevent the failure that actually compounds — the same role spelled several ways. macOS ships 627 sandbox containers whose extension-point names include `extension` (45x), `Extension` (2x), `DiagnosticExtension` (8x), `diagnosticextension` (5x) and `diagnostic` (3x): fifteen years of a freeform role field with no grammar, after which the name can no longer group anything and a side-channel record has to carry the contract. 3. `conformanceTest` pointed at `conformance/abb-database-vectors.json`, which does not exist. `format: uri-reference` is a syntax check and resolves nothing, so the dangling pointer validated clean. Changes: - `role` is a closed enum over the four roles actually in use (prophet-mesh specs/abb-catalog.yaml plus the example). Rejection now happens in the schema, which every consumer already validates against, not in a private regex. Adding a role is a spec PR — the reviewed gate whose absence produced the five spellings above. - The deny list is deleted. Its cases are now fed to the schema AS DOCUMENTS: 8 vendor names, 6 case/whitespace variants, 1 invented role, all rejected. - INVARIANT 1a mutation-tests the enum. Without it the cases above would keep passing if `role` reverted to a freeform string while some unrelated constraint did the rejecting. Drop the enum, confirm `postgres` is accepted; restore, confirm rejected. - INVARIANT 3 resolves `conformanceTest` against the repo, with a negative control proving a dangling pointer is detected. The example's dangling pointer is removed rather than backfilled with invented vectors: absence is documented as meaningful, a fabricated suite would be a new paper control. Verified by probe, not by exit code: the exact document that passed before (`role: postgres`) now fails, as do `role: database` and a dangling conformanceTest. 34 checks, `make validate` green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First move in the Michael-agent capability-integration work. Adds the ABB abstraction the estate has been missing.
Why
Zurich E-RDA2 uses the same pattern —
ABB.03 = DATABASEhas slots that DB2, ADLS, BLOB, XYZ or CaaS can fill. Without an ABB layer, a service is inseparable from its implementation and 'we use Postgres' becomes a load-bearing invariant across ~74 services.An ABB names a function (
DATABASE,IMPLEMENTATION CONTROLLER), not a technology (PostgreSQL,MSTR). Implementations declareimplementsAbb: ['ABB.03']in their MeshActionRegistry participant and become swappable slot fillers.Invariants (both enforced by REJECTION)
abbIdisABB.<two digits>— ids are stable; renumbering is a breaking change to every manifest that references them.protocol.readsandprotocol.writesare declared explicitly (may be empty). A write-only ABB writesreads: []on purpose — 'forgot' cannot masquerade as 'no reads'.Plus a validator-side check that roles name functions not vendors:
postgres,AWS,MSTR,kubernetesare all caught by a deny list.Consumers (in flight)
route_by_abbcapabilitylibs/python/solution-ranker(walks MAR for candidates)libs/python/access-prewalk(graded consent per candidate)Verification
make validategreen (17 schema families)abbIdpattern to^.+$exits 1; restored, exits 0