Canonical automation module surface — design
Tier: Public
Status: Accepted design 2026-05-19 (core team approval recorded; B1–B3 pre–Phase A checkpoints resolved 2026-05-19, see §12; NOT a separate Accepted RFC — implementation is phased per §9; amendments per RFC-0002 §12)
Audience: core team, brewery-vertical maintainers, OpenPLC sister-repo maintainers, module SDK authors
Resolves: RFC-0001 Decision E §7.2
Builds on: RFC-0001, RFC-0002, docs/PLATFORM-ARCHITECTURE.md §4.4, §6, packages/sdk/module-sdk/README.md
Disclaimer. Recommends concrete shapes for canonical
automation. Does not allocate canonical codes, change licenses, or relitigate pi-sidecar runtime (Jinja + FastAPI stays per RFC-0001 §7.3). Safety-validated PLC logic in the OpenPLC sister repo remains authoritative until a formal re-validation program says otherwise.
1. Summary
RFC-0001 commits automation as tier-1 canonical and defers surface design here. Four §7.2 questions — recommendations, not TBDs:
| # | Question | Recommendation |
|---|---|---|
| 1 | Adapter contract | Canonical automation owns generic adapter SDK; brewery (tier 6) ships thin brewery.openplc.v1 implementation |
| 2 | Equipment vs Vessel | Two models + optional FK: EquipmentProfile (brewery design-time) ↔ Vessel (automation runtime) |
| 3 | OpenPLC translation | Alarm ladder in sister repo; workspace shell, Modbus mailbox contract, Prisma, AI tools, tier limits in platform |
| 4 | Pi-sidecar vs integrations | Layered: integrations = read-mostly devices; automation = supervisory control; pi-sidecar = field UI; shell = system-of-record |
β layout per RFC-0002: services/api/src/modules/automation/, apps/web/app/[locale]/(automation)/, apps/native/src/modules/automation/, packages/canonical/automation/contracts/. Postgres schema automation when tables land (§8).
2. Problem statement
Three concerns must not collapse (RFC-0001 §7.1):
- Design-time brewing math —
EquipmentProfile(schema.prisma): kettle volume, mash efficiency; recipe/water inputs; no live PLC state. - Read-mostly telemetry —
Integration/IntegrationDevice/IntegrationReadingviaintegrationsGeneric.ts(Tilt, iSpindel, RAPT). - Supervisory PLC — OpenPLC sister repo (Modbus, pi-sidecar,
PI_*mailbox, frozen alarm layer2.0.1-dev).
(3) is not "another integration" and not brewery-internal-only. This design assigns lifecycles and safety boundaries per concern.
3. Recommendation 1 — Generic adapter SDK (canonical owns contract)
Recommendation: @umbraculum/automation-adapter-sdk (MIT, tier 2) defines AutomationAdapterDefinition (kind, protocol, capabilities, connect/disconnect/readSnapshot/optional applyCommand). Canonical automation owns AdapterConnection persistence, secrets refs, connection supervisor, snapshot→Vessel mapping, and maxAdaptersConnected enforcement. Brewery ships brewery.openplc.v1 — Modbus map aligned to sister-repo PI_*; no alarm ladder in TypeScript.
Rationale: Same pattern as Decision F (one platform-owned interface, many consumers). Supervisor + tenancy + billing are identical across verticals; only protocol packs differ.
Rejected: Per-vertical adapters against undocumented primitives — duplicates supervisor logic and breaks hosted consistency.
4. Recommendation 2 — EquipmentProfile + Vessel (two models, optional FK)
| Model | Owner | Schema | Purpose |
|---|---|---|---|
EquipmentProfile | Brewery (tier 6) | brewery.* | Design-time params for recipes / water |
Vessel | Canonical automation | automation.vessels | Runtime instance: mode, temps, alarms, adapter link |
Link: Vessel.equipmentProfileId → EquipmentProfile.id (optional). Vessel-only (PLC import) and profile-only (planning) are both valid.
Rejected: Single unified model — mixes snapshot-copy semantics with live PLC rows; confuses maxRecipes vs maxVessels.
MRP / CRP entry point (deferred). When mrp (and crp) ship, they will need read access to EquipmentProfile to do capacity planning (profiles × N vessels per profile = total workspace capacity — see §12 cardinality note). That is the second-consumer trigger from RFC-0002 §7 item 3 ("cross-module shared types") to extract @umbraculum/equipment-contracts (or @umbraculum/brewery-equipment-contracts if the concept stays brewery-specific — vertical-prefixed per sub-plan #9 §1.3 TRAP-avoidance discipline). Until then, the field stays brewery-internal; automation references vesselId via @umbraculum/automation-contracts. No platform-equipment-contracts until the conflict is concrete — YAGNI.
5. Recommendation 3 — OpenPLC translation seam
5.1 Sister repo (validated runtime — do not duplicate without re-validation)
- Safety alarm ladder / interlocks (
DEVELOPMENT.mdalarm-layer stability). - OpenPLC Editor artifacts, compile/upload, bench vs field profiles.
- Runtime upload bundle (
prepare_openplc_runtime_upload.py).
5.2 Umbraculum platform
Vessel/AdapterConnection/AutomationAlarmEvent(Prismaautomationschema).PI_*mailbox semantics in@umbraculum/automation-contracts(enforced by brewery adapter).- AI tools, tier limits, workspace shell UI
(automation)/. - Pi-sidecar runtime stays on Pi (FastAPI + Jinja) — not replaced by Next.js.
5.3 Runtime flow (bench)
OpenPLC Runtime ◄─Modbus PI_*─► Pi sidecar (field UI)
▲ │
│ upload bundle │ HTTPS
└──────────────────────────────┼──► Umbraculum API
│ automation + brewery.openplc.v1
PLC releases follow sister-repo validation; platform ships on normal API/web cadence. Surface AdapterConnection.runtimeVersion + contractVersion for skew detection.
6. Recommendation 4 — Layered integrations + UI
6.1 Data
| Layer | Use when | Examples |
|---|---|---|
| Integrations | Read-mostly, high-volume series, brew-session attach | Tilt, iSpindel, RAPT |
| Automation | Multi-vessel state, commands, alarm authority | OpenPLC vessels |
Bridge: Optional vesselId in integration metadataJson for UI correlation only — do not copy IntegrationReading rows into Vessel. AI joins via tools in one workspace context (PLATFORM-ARCHITECTURE.md §4.0).
6.2 UI
| Surface | Role |
|---|---|
| Pi-sidecar | Field operator, local/offline-tolerant control — keep |
(automation)/ shell | Fleet, alarms, AI, billing, audit — primary desk UX |
(brewery)/ | Recipes, equipment profiles — deep-link to vessel when linked |
Rejected: Web-only replacement of pi-sidecar in H2 2026.
7. Prisma sketch (automation schema)
Illustrative; migrations land with implementation. equipmentProfileId is a Prisma cross-schema FK to brewery.equipment_profiles (RFC-0010).
model Vessel {
id String @id @default(uuid())
workspaceId String @map("workspace_id")
code String
displayName String @map("display_name")
vesselKind String @map("vessel_kind")
equipmentProfileId String? @map("equipment_profile_id")
adapterConnectionId String? @map("adapter_connection_id")
mode String?
currentTempC Float? @map("current_temp_c")
targetTempC Float? @map("target_temp_c")
alarmActive Boolean @default(false) @map("alarm_active")
lastSeenAt DateTime? @map("last_seen_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
adapterConnection AdapterConnection? @relation(fields: [adapterConnectionId], references: [id])
alarmEvents AutomationAlarmEvent[]
@@unique([workspaceId, code])
@@index([workspaceId])
@@map("vessels")
@@schema("automation")
}
model AdapterConnection {
id String @id @default(uuid())
workspaceId String @map("workspace_id")
adapterKind String @map("adapter_kind")
displayName String @map("display_name")
status String @default("disconnected")
configJson Json @map("config_json")
secretRefId String? @map("secret_ref_id")
contractVersion String @map("contract_version")
runtimeVersion String? @map("runtime_version")
lastSeenAt DateTime? @map("last_seen_at")
vessels Vessel[]
@@index([workspaceId])
@@map("adapter_connections")
@@schema("automation")
}
model AutomationAlarmEvent {
id String @id @default(uuid())
vesselId String @map("vessel_id")
code String
severity String
message String
active Boolean @default(true)
raisedAt DateTime @default(now()) @map("raised_at")
clearedAt DateTime? @map("cleared_at")
vessel Vessel @relation(fields: [vesselId], references: [id], onDelete: Cascade)
@@index([vesselId, active])
@@map("alarm_events")
@@schema("automation")
}
8. AI tools, tier limits, registration
8.1 AI tools (H2 2026 — read-only; ownerModule: "automation", scopes: ["read"])
| Tool | Purpose |
|---|---|
automation.vesselState | One vessel or fleet snapshot |
automation.listVessels | Paginated fleet + filters |
automation.activeAlarms | Open alarms |
automation.adapterHealth | Connection status, versions, last-seen |
automation.setpointHistory | Bounded audit of mode/setpoint changes |
Write/propose flows follow human-in-the-loop per PLATFORM-ARCHITECTURE.md §6.5 (H1 2027+).
8.2 Tier limits (tierLimits(tier) slice)
Status: registered at API boot (2026-05-28 scaffold) in services/api/src/modules/automation/tierLimits.ts; values appear in billing limits JSON. Route enforcement (maxVessels on vessel create, maxAdaptersConnected on adapter connect) remains Phase C.
| Field | Meaning | Illustrative |
|---|---|---|
maxVessels | Vessels per workspace | free 2 / premium 8 / pro 24 / pro_plus 100 |
maxAdaptersConnected | Connections in connected | free 0 / premium 1 / pro 2 / pro_plus 10 |
automationAiToolsEnabled | Gate tool family | free false / paid true |
addonCodes: ["automation_module"]. Add-on enforcement deferred H1 2027 (RFC-0009); α uses tier limits only on vessel create and adapter connect.
8.3 registerModule() (shape per §4.4)
registerModule(app, {
code: "automation",
routes: [automationVesselsRoutes, automationAdaptersRoutes, automationAlarmsRoutes],
prismaSchema: "automation",
registerAiTools(registry, app) {
registerAutomationTools(registry, app.prisma);
},
tierLimits: (tier) => ({ maxVessels, maxAdaptersConnected, automationAiToolsEnabled }),
addonCodes: ["automation_module"],
});
9. H2 2026 phasing (RFC-0001 §7.3 working assumption)
Per docs/ROADMAP.md H2 2026 AI-first; full MRP ↔ automation co-design waits H1 2027.
| Phase | Window | Deliverable |
|---|---|---|
| A — Contracts | Q3 2026 | @umbraculum/automation-contracts, adapter SDK types; PI_* doc sync |
| B — Read path | Q3–Q4 2026 | Prisma schema, mock adapter, vesselState / listVessels, read-only (automation)/ |
| C — Brewery adapter | Q4 2026 | brewery.openplc.v1 bench Modbus; adapterHealth |
| D — Alarms | Q4 2026 | AutomationAlarmEvent, activeAlarms, integration metadata bridge |
| E — Write proposals | H1 2027+ | Human-in-the-loop setpoints (not H2 2026) |
Not H2 2026: pi-sidecar UI replacement; alarm ladder in TS; autonomous AI PLC writes. registerModule() should land with or before Phase B (feature-flag OK).
10. Brewery tier-6 checklist
- Package implementing
brewery.openplc.v1(adapter SDK consumer). - Keep
EquipmentProfilein brewery module; optional link UX toVessel. - Sister-repo README cross-link to this doc + RFC-0001.
- Ferm integration screens keep integrations routes; optional vessel link when automation installed.
11. Non-goals
eslint-plugin-boundariesformodules/automation/(RFC-0001 follow-on).- MRP ↔ vessel scheduling.
- Native automation screens beyond minimal status unless proven.
- Elevating this artifact to a separate Accepted RFC (RFC-0003) unless a future governance change explicitly requires the RFC track; canonical status and layout are already committed in RFC-0001 and RFC-0002.
12. Pre–Phase A checkpoints — Resolved 2026-05-19
Owner: OpenPLC + API maintainer (project lead). All structural recommendations in §§3–6 are accepted; the items below close the remaining detail before Phase A contract work (@umbraculum/automation-contracts + adapter SDK types) starts. Tier-limit numerics (§8.2) and full MRP/CRP cross-references (§4) remain illustrative until their respective phases.
12.1 Pilot context (informs the rest of this section)
The OpenPLC sister repo today is a dev / pilot — not deployed in any customer's production environment. The brewery vertical is real, but does not currently consume the pilot. Implication for Phase A–C: we have full freedom to evolve PI_* names, add registers, restructure the contract, and break compatibility on our own bench without notifying anyone external. Discipline (version handshake, refuse-on-major-mismatch) is wired in from day one so that by the time real customers exist, the muscle memory is in place — not because today's churn would harm anyone.
12.2 B1 — PI_* / Modbus mailbox source of truth
- SoT. OpenPLC sister repo is authoritative for
PI_*names, addresses, and semantics. Safety-validated alarm logic is part of that SoT and is not duplicated in TypeScript. - Mirror mechanism (M2). Sister repo emits a checked-in artifact (script-generated
PI_*.jsonand/or.ts). Platform@umbraculum/automation-contractsimports / mirrors that artifact via PR. Drift becomes visible in PR diffs rather than at runtime. Upgrade to a published@umbraculum/openplc-mailbox-specpackage (M3) deferred until a second adapter or third-party consumer appears. - Platform extensions. Platform never adds
PI_*names unilaterally; new mailbox entries require sister-repo PR + revalidation. Non-PLC platform-only data (AI summaries, UI hints, saved presets) lives in Postgres onVessel/sibling tables, not in the mailbox namespace. - Version handshake. Reuse the existing baseline
contract_versionfrom the integrated release (sister-repopyproject.tomlis the canonical sidecar source per the integrated-release-versioning rule). Phase Aautomation-contractsexports aCONTRACT_VERSIONconstant tracking that field. The PLC reports its runtime version via a dedicatedPI_FIRMWARE_VERSION(or equivalent) register; if one does not yet exist in the sister repo, Phase A includes a sister-repo PR to add it. - Mismatch policy. Major-version mismatch → adapter refuses to connect, raises an
AutomationAlarmEvent. Minor-version mismatch → connects with a warning surfaced onautomation.adapterHealth. Patch differences are silent. - Phase A scope for write path. Types and version constants only. No Modbus client in the platform until Phase C (
brewery.openplc.v1).
12.3 B2 — Vessel.equipmentProfileId cross-schema link
- Mechanism (L2 — shipped RFC-0010). Prisma cross-schema FK:
equipmentProfileId String?onautomation.Vessel→brewery.EquipmentProfile, nullable,@relationwithonDelete: SetNull. Application services still validate profile existence at write time. - Prior L1 path. Before RFC-0010, the link was an app-level UUID FK with no Prisma
@relationacross schemas; backfill + relation declaration completed in the schema-split migration. - Cardinality — N:1 (industry-standard). Multiple
Vesselrows may reference the sameEquipmentProfile. Concrete brewery example: one profileP-FV-15bbl-Std(15 bbl conical, glycol, 2" arm) referenced by four physical vessels K1, K2, K3, K4. Same physical layout, single design record, single calibration update applies to all four. The 1-of-1 case (mash tun, kettle, HLT — typically unique designs in a brewery) is the trivial sub-case of N:1 and is allowed without special handling. - Profile delete semantics. Default is block when any vessel references the profile; the API returns a clear error naming the referencing vessel codes. Explicit detach is supported via an opt-in flag (e.g.
?cascade=detach) — never silent. Avoids surprise data loss while keeping the operator override path explicit. - Tier interactions. Automation not installed → brewery never reads or writes the field. Brewery not installed (post-canonical hypothetical) → the field stays nullable; vessels without profiles are allowed.
- MRP/CRP forward note. When
mrp/crpships, capacity planning aggregatescount(vessels) × profile.capacityper workspace. The N:1 cardinality is what makes that aggregation meaningful. Trigger for extracting@umbraculum/equipment-contractsis documented in §4 above.
12.4 B3 — Phase A scope confirmation
Phase A is strictly:
- New
packages/canonical/automation/contracts/— mailbox types, adapter SDK types,CONTRACT_VERSIONconstant. No runtime code. - Sister-repo PR(s) — emit the mailbox artifact; ensure
contract_versionbaseline includes a slot for adapter consumers; addPI_FIRMWARE_VERSIONregister if absent. - This design doc — §12 (this section) records resolutions; §4 records the MRP/CRP entry point.
Phase A explicitly does not include:
- Prisma migrations (Phase B).
registerModule({ code: "automation" })registration inapp.ts(lands with or before Phase B).- Any Modbus client implementation (Phase C).
- Web
(automation)/routes (Phase B).
Implementation PRs should link this doc and cite the accepted Phase (§9).
12.5 Phase A entry checklist
Concrete steps Phase A includes, in order. Tracked under sub-plan #7 (umbrella plan).
| # | Step | Repo | Status |
|---|---|---|---|
| 1 | Scaffold packages/canonical/automation/contracts/ (types-only, vitest, tsup) | umbraculum-dev | Done — see commit log |
| 2 | Wire new package into root build:packages and .github/workflows/typecheck.yml workspaces array | umbraculum-dev | Done |
| 3 | Mailbox-emitter PR shape draft (handoff to sister-repo maintainer) | umbraculum-dev (docs/design/openplc-mailbox-emitter-pr-shape.md) | Done |
| 4 | Sister-repo PR — emit PI_* mailbox artifact (mailbox.json and/or .ts); align contract_version slot for adapter consumers; add PI_FIRMWARE_VERSION register if missing | OpenPLC sister repo | Done — folded into the sister-repo main baseline tagged 2.0.1-dev (tools/build_mailbox_artifact.py, tools/test_build_mailbox_artifact.py, out/mailbox.json 356 entries, PI_FIRMWARE_VERSION AT %QW222, make mailbox-artifact / make test-tools 17/17 passing) |
| 5 | Sync first emitted mailbox artifact into packages/canonical/automation/contracts/ (M2 mirror); bump CONTRACT_VERSION to first non--dev value agreed with sister-repo maintainer | umbraculum-dev | Done — master commit 9253b1b (packages/canonical/automation/contracts/data/mailbox.json byte-for-byte mirror, src/mailbox-data.ts validator + frozen MAILBOX_SPEC, CONTRACT_VERSION = "2.0.1-dev" tracking sister-repo INTEGRATED_RELEASE_TAG, scripts/sync-automation-mailbox-mirror.sh with --check drift mode, vitest 28/28 passing in container, dist/* regenerated). Phase A 5/5 done. Note: 2.0.1-dev retains the -dev suffix because the sister-repo integrated release itself is still pre-release; the first non--dev bump is gated by sister-repo cutting 2.0.1 (or later) on its bench-acceptance milestone, then both repos move together. |
| 6 | Phase B unblock signal — Phase B (read path: Prisma automation schema, vesselState, listVessels, read-only (automation)/) is now the active focus per §9 | both | Done — Phase B-1 landed (see §12.6) |
Hold rule. Steps 1–3 land independently in umbraculum-dev (types and shape only — no consumer). Step 4 is the first sister-repo touch and needs the maintainer's review window. Step 5 is the first time the platform mirror carries real PI_* names, so it should land in a single dedicated PR for clean diff review.
No-customer pilot context (per §12.1). Steps 4 + 5 may iterate freely on names and addresses without external compatibility concern; the version handshake (steps 1 + 5) is the discipline that catches accidental skew on our own bench during Phase B/C.
12.6 Phase B execution checklist
Phase B is split into three reviewable commits, B-1 → B-3, all landing in umbraculum-dev. Each is independently typechecked and tested in container; the sister repo is not touched in Phase B.
| # | Step | Status |
|---|---|---|
| B-1 | Prisma multiSchema enabled + @@schema("public") on all 58 existing models/enums + Vessel / AdapterConnection / AutomationAlarmEvent under @@schema("automation") (migration add_automation_schema); services/api/src/modules/automation/ skeleton (registerAutomationModule(app) via @umbraculum/module-sdk, no routes); app.ts wire-up; docker-compose api+web service mounts for module-sdk + automation-contracts; vitest setup + idempotent guard for repeat buildApp() calls; api test suite green (49 files / 400 tests) | Done |
| B-2 | services/api/src/modules/automation/adapters/mockAdapter.ts (deterministic AutomationAdapterDefinition impl, no real Modbus, automation.mock.v0 kind); services/vesselsService.ts (DB read with assertMembership + VesselStateSchema.parse(...) translation); routes/automationVesselsRoutes.ts (GET /automation/vessels, GET /automation/vessels/:code, workspace-scoped, response shapes pinned via VesselListResponseSchema / VesselStateResponseSchema); AI tools automation.listVessels + automation.vesselState registered alongside brewery tools in app.ts; packages/canonical/automation/contracts/src/adapter.ts migrated to Zod schemas (VesselSnapshotSchema, VesselStateSchema, VesselListResponseSchema, VesselStateResponseSchema, AdapterCapabilitiesSchema) per RFC-0003; vitest unit tests for the mock adapter + the new schemas | Done — 2026-05-19 |
| B-3 | apps/web/app/[locale]/(automation)/page.tsx (vessel list); apps/web/app/[locale]/(automation)/[vesselCode]/page.tsx (vessel detail); top-nav entry in PrimaryNav.tsx + automation i18n namespace (English + Italian placeholder); api integration tests in services/api/src/tests/automationVessels.test.ts covering 401 unauth, list happy path with deterministic code asc ordering, get-by-code happy path + 404, L2 cross-workspace isolation pins (shared-code collision + B-only code 404 from A); api typecheck clean; full api vitest suite green (51 files / 413 tests); web typecheck has no new errors beyond the pre-existing project-wide Tamagui shorthand-prop type-resolution issue; services/api/src/modules/automation/README.md now carries an automation-vs-crp surface-boundary guardrail + "how to insert demo vessels locally" note (no seed extension per design) | Done — 2026-05-19 |
B-1 invariants captured. At B-1 ship (2026-05-19), brewery and platform tables were still in public; only the new automation schema was added. RFC-0010 (2026-05-28) moved horizontal models to platform.* and brewery domain tables to brewery.* via forward-only ALTER SCHEMA (migration 20260528170000_split_platform_brewery_schemas). The cross-schema equipmentProfileId was L1 app-level at B-1 ship; RFC-0010 upgraded it to a formal Prisma @relation to brewery.EquipmentProfile. The adapter contract version handshake (AdapterConnection.contractVersion ↔ MAILBOX_SPEC.contractVersion from @umbraculum/automation-contracts) is wired into the table shape but not yet enforced — enforcement lands with the real brewery.openplc.v1 adapter in Phase C.
B-1 follow-on captured for module-sdk. @umbraculum/module-sdk's registerModule() uses a process-wide singleton registry that throws on duplicate registration. The api vitest.setup.ts clears it between test files and services/api/src/modules/automation/index.ts carries an idempotent guard for intra-file repeats. The cleaner long-term fix is to split metadata-recording (process-wide, idempotent) from per-app route registration in module-sdk itself; tracked as a sub-plan #9 follow-on (sub-plan #9 scoping pass done 2026-05-19 — see brewery-scope-migration-plan.md; the metadata/registration split is a post-rename refactor expected to land after slot 11 closes the @umbraculum/module-sdk rename).
B-2 closure note (2026-05-19, on the post-RFC-0003 Zod v4 pattern). The B-2 commit lands the read path entirely on the new validation-slice pattern: adapter.ts rewrites the interface-based contracts as Zod schemas with z.infer types (VesselSnapshotSchema, VesselStateSchema, VesselListResponseSchema, VesselStateResponseSchema); the service layer parses Prisma rows through VesselStateSchema.parse(...) at the wire-shape boundary so a future Prisma migration that adds a column without updating the wire shape surfaces the drift at the service boundary, not at the client; the route layer parses route params (/automation/vessels/:code) via an inline z.object({ code }) schema; the AI tool layer parses the model-provided input via Zod .parse() in the handler (the JSON Schema literal stays on the inputSchema property because the Anthropic SDK consumes JSON Schema, not Zod schemas — until the AI-tool registry grows a Zod-aware adapter in Phase C). All four boundary types (request body / route params / Prisma row → wire / model input) get a Zod validation pass, matching the "validate at the boundary, once" discipline from the rewritten 22-typescript-contracts-runtime-validation.mdc plugin-pack rule. Integration tests (the canonical 6-axis L2 pattern from services/api/src/tests/ for the new /automation/vessels routes + an L2 cross-workspace isolation pin) are tracked under B-3's testing scope so they land in the same commit as the web UI that exercises them.
B-3 closure note (2026-05-19). Web read path landed: apps/web/app/[locale]/(automation)/page.tsx (vessel list) + (automation)/[vesselCode]/page.tsx (vessel detail) consume GET /api/automation/vessels(+:code) via apiFetch, re-validate the response through VesselListResponseSchema / VesselStateResponseSchema on the client (boundary validation per the Zod v4 standard — server's VesselStateSchema.parse(...) is the canonical pin, the client re-parse catches HTTP-layer drift), and render only the live-controller-state fields explicitly enumerated in §11 (mode, currentTempC, targetTempC, alarmActive, lastSeenAt). Automation-vs-crp surface guardrail is now first-class: the services/api/src/modules/automation/README.md carries a "Surface boundary — automation vs. crp" section that contributors must respect (no scheduling / utilization / booking / capacity views on the automation surface; those belong to the future crp module). The top nav (PrimaryNav.tsx) exposes the surface with the automation i18n label; English + Italian placeholder copy land in packages/platform/i18n/src/{en,it}.json. Tests: 8 new tests in services/api/src/tests/automationVessels.test.ts covering 401 unauth, list happy path with deterministic ordering, get-by-code + 404, and two L2 cross-workspace isolation pins (the shared-code collision case and the B-only-code 404 case); full api suite (51 files / 413 tests) green. The apps/web typecheck has no NEW errors beyond the pre-existing project-wide Tamagui shorthand-prop type-resolution issue (same pattern as app/[locale]/recipes/page.tsx and others); fixing that is orthogonal Tamagui type-debt and tracked separately. Phase C (real brewery.openplc.v1 adapter + version handshake enforcement) is unblocked.
13. References
- RFC-0001 — Decision E §7.2, Decision F
- RFC-0002 — β layout,
automationschema name docs/PLATFORM-ARCHITECTURE.md— §4.4, §4.5, §6docs/ROADMAP.md— H2 2026 / H1 2027integrationsGeneric.ts,schema.prisma(EquipmentProfile,Integration*)- OpenPLC sister repo —
PI_*, alarm-layer stability,DEVELOPMENT.md
Accepted design 2026-05-19. Implementation PRs should link this doc, RFC-0001 §7.2, and RFC-0002 (β layout). Amendments follow the same lightweight design-review procedure as RFC-0002 §12 unless a formal RFC-0003 is opened for governance reasons.