28. Compliance Boundaries
Most architectural concerns live entirely inside the team — what services exist, what they call, how they’re owned. Compliance is different. Compliance is about boundaries — what data crosses which lines, which services are in PCI scope, which talk to PII, which can reach the public internet. These boundaries exist whether or not you model them. The question is whether the model knows about them.
This chapter shows how to use labels, views, and validation rules together to make compliance boundaries first-class in the architecture. The result: a model that surfaces violations at parse time, generates compliance views automatically, and makes audit conversations trivial.
The scenario
A small system spanning three security tiers: public-facing services (DMZ), internal services (Internal), and PCI-scoped services (PCI). Plus a separate data-classification axis: services touching PII have to be marked.
We want three things from the model:
- Automatic compliance views — “show me everything in PCI scope,” “show me everything touching PII.”
- Boundary enforcement — no service in
Internalshould be called by a service inDMZdirectly; PCI services should never be called from outside PCI. - Onboarding clarity — a new engineer reading the model should know, for each service, what compliance constraints apply.
The labels
Two label axes, both cascading:
| Label | Values | Purpose |
|---|---|---|
security.zone | Public, DMZ, Internal, PCI, External | Network/security tier |
data.classification | public, internal, pii, pci, secret | Data sensitivity |
Both axes are conventions. They aren’t built into the language. You declare them in your project’s stdlib (or use the stdlib defaults), then every module sets them as appropriate.
Optionally, a third axis:
| Label | Values | Purpose |
|---|---|---|
compliance.regime | gdpr, ccpa, pci-dss, sox, hipaa | Which regulatory framework applies |
Multi-value labels aren’t supported — a module that’s both GDPR and PCI scope gets a label per regime by declaring two: compliance.regime.gdpr: true, compliance.regime.pci: true. The dotted-key field form covers this cleanly.
The modules
platform.arch (selectively, just to show labeling):
frontend StoreFront { team: Frontend
labels { domain: Commerce security.zone: Public data.classification: public }
"Customer-facing storefront. Public-internet exposed."}
service WebGateway { team: Platform
labels { domain: Platform security.zone: DMZ data.classification: internal }
"Edge gateway. Terminates TLS, applies WAF rules, forwards into Internal."}
service Orders { team: Commerce
labels { domain: Commerce security.zone: Internal data.classification: pii }
"Order processing. Stores customer name/address/email — PII."}
service Payments { team: Payments
labels { domain: Payments security.zone: PCI data.classification: pci compliance.regime.pci: true }
"Card payment processing. Card data NEVER leaves this module unencrypted."}
database PaymentVault { team: Payments
labels { domain: Payments security.zone: PCI data.classification: pci data.encryption: aes-256 }
"Encrypted vault for tokenized card data."}Note compliance.regime.pci: true — a boolean field works well for “this module is in scope for that regime.” Multiple regimes get multiple keys.
Mindset shift. Compliance labels are not optional metadata. They define behavioral constraints. A label of
security.zone: PCIisn’t a tag — it’s a contract that this module’s data and calls will be treated to PCI rules. If you only carry labels for the modules that happen to be compliant, you’ll get false negatives (a Internal module slipping a PCI call past validation). Set the labels everywhere, including forPublicservices. The model is comprehensive or it’s useless.
The views
views.arch:
view PCIScope { focus security.zone: PCI group by team layout dagre "Every module in PCI scope. Used in quarterly compliance review."}
view PIIScope { focus data.classification: pii group by team layout dagre "Every module touching PII. Used in data-mapping exercises."}
view PublicSurface { focus security.zone: Public focus security.zone: DMZ layout dagre "Internet-exposed surface. Useful for pen-test scoping."}
view CrossZoneFlow { layout dagre "All processes; highlights edges that cross security zones."}The first three are trivial once labels are set. CrossZoneFlow is the same model but with a layout that gives reviewers the easiest read of zone boundaries (typically dagre or hierarchical works best for that).
Validation rules
This is where the metamodel earns its keep. The stdlib’s validator runs a small set of built-in checks (required blanks, callable resolution). Custom checks live in your project’s types — required blanks specifically targeted at compliance facts.
kinds.arch:
// Every service in PCI scope must declare a runbook and a contact.export type service pci_service { required cascade team required ext.runbook_url required ext.compliance.contact required labels.data.classification // instance must pick a value (pii, pci, secret, ...)
labels { security.zone: PCI compliance.regime.pci: true }}
// Every service touching PII must declare a retention policy.export type service pii_service { required cascade team required data.retention_days required data.deletion_endpoint
labels { data.classification: pii }}Now instead of service Payments, write:
pci_service Payments { team: Payments ext.runbook_url: "https://wiki.acme.com/pci-runbook" ext.compliance.contact: "compliance@acme.com" labels { data.classification: pci // fulfills required blank }
command Authorize command Capture command Refund}If a developer adds a pci_service without one of the required fields, validation fails. Compliance facts are no longer “we should remember to add this” — they’re enforced at parse time.
Process-level constraints
Built-in process validation catches structural errors (caller is a module, callee is an interface). Compliance-level constraints — “no DMZ service should call an Internal service directly” — are not yet first-class in the validator. The pattern that works today:
Use labels and views to surface violations visually, and a small custom check script (built on @archlang/core, Chapter 24) to enforce them in CI:
import { loadPackage } from "@archlang/lsp";import { NodeIO } from "@archlang/lsp/node";import { resolvePackage, buildGraph } from "@archlang/core";
const io = new NodeIO();const loaded = await loadPackage(io, ".");const model = resolvePackage(loaded.input, loaded.deps).model;const graph = buildGraph(model);
function zone(moduleId: string | undefined): string | undefined { if (!moduleId) return undefined; const m = graph.modulesById.get(moduleId); return m?.labels.get("security.zone");}
const violations: string[] = [];for (const edge of graph.edges) { const fromZone = zone(edge.fromModuleId); const toZone = zone(edge.toModuleId);
if (fromZone === "DMZ" && toZone === "Internal") { violations.push(`${edge.fromName} → ${edge.toName} (DMZ→Internal)`); } if (fromZone !== "PCI" && toZone === "PCI") { violations.push(`${edge.fromName} → ${edge.toName} (outside→PCI)`); }}
if (violations.length > 0) { console.error("Zone violations:\n" + violations.join("\n")); process.exit(1);}Run it in CI. Violations fail the build. Compliance is now machine-enforced.
What governance teams see
After the labels and types are in place:
- The PCI quarterly review — open the
PCIScopeview, screenshot, file. The view is exact and current because it’s a function of the source. - The PII data map — open
PIIScope. Every service touching PII is listed; for each, retention and deletion are visible. - The DPIA inventory — script over the resolved model: list every module with
compliance.regime.gdpr: true. - A boundary violation — a developer adds a
servicewith no compliance labels and a process step into a PCI service. CI fails. The developer either justifies the call (in which case it’s reviewed) or removes it.
None of this requires keeping a parallel compliance document up to date. The document is the model.
Decisions you’ll face
Label coverage. Either every module carries security.zone and data.classification, or you have invisible gaps. Reach completeness by making the labels required on your kind types. Cost: a one-time pass updating existing instances. Benefit: ironclad coverage.
Cascade vs explicit. A system PaymentsDomain { ... } container with labels { security.zone: PCI } cascades to every contained service. Cleaner than repeating the label five times. Use cascade for shared zones; use explicit labels when a single module is the exception.
Where the rules live. Project-local types in your stdlib (pci_service, pii_service) encode the compliance vocabulary. Reuse them across packages by exporting from a shared package and importing.
What to NOT model. Data retention implementation (the cron job that runs the deletion). Specific encryption algorithms (those are operational, sometimes per-instance). Audit log destinations. Anything that’s an implementation choice should be captured in fields (“which one”) not in the model’s structure (“does it exist”).
Summary
- Labels (
security.zone,data.classification,compliance.regime.*) are how the model carries compliance facts. - Cascade those labels — set them once, reach everything inside.
- Type kinds (
pci_service,pii_service) make required compliance fields enforceable at parse time. - Compliance views (
focus security.zone: PCI) fall out of labels — no manual maintenance. - Process-level boundary checks live in a small CI script built on
@archlang/core. - The model becomes the compliance document.
What’s next
Chapter 29: Designing a Metamodel → — kind-author tutorial. Build a domain-specific vocabulary on top of the stdlib.