Matter Behavioural-Parity Contract¶
Audience: every contributor and AI agent that touches code in this module. This is a standing contract, not an audit you run once. Read it before your first change here and treat it as binding.
go-fabric is a deliberate, behaviour-level port of
matter.js HEAD. This page defines
what "parity" means in practice and which standing guards keep the port
honest, so that a change which merely looks right but drifts from the gold
standard cannot land unnoticed.
It complements the
matter.js is the gold standard section of this repository's
CLAUDE.md: that one tells you matter.js HEAD is the gold standard and how to
regenerate the schema; this one tells you what parity means, why
behaviour parity (not just schema parity) is the bar, and which standing
guards enforce it. What the ecosystems themselves do with the result is
collected in
Matter Ecosystem Observations.
1. The principle¶
Think for yourself, and always verify against matter.js / connectedhomeip. Mirror behaviour, not just shape.
Two halves, both mandatory:
- Reason about the change. Understand the cluster, the command, the state machine, the failure modes. Do not cargo-cult.
- Verify against the gold standard. Before you write a fix or feature, read the corresponding matter.js source (and connectedhomeip for wire-truth). Your implementation mirrors theirs — same defaults, same constraints, same status codes, same order, same wire shape — unless a divergence is deliberate and recorded.
A change that "looks right" but was never checked against matter.js is not acceptable, no matter how reasonable it seems. The protocol's edge cases were encoded into matter.js through real interop testing against Apple Home, Google Home, and Alexa; we mirror that hard-won behaviour rather than re-deriving it.
2. Why matter.js / chip, and why behaviour¶
matter.js HEAD is a production-tested, continuously-evolving Matter stack (it is not CSA-certified, and neither is this module). connectedhomeip (chip) is the CSA reference. Together they are the authority for:
- cluster IDs, revisions, attribute / command / event IDs (schema), and
- defaults, constraint enforcement on writes, command semantics, status codes, conformance gating, subscribe / report cadence, commissioning and CASE state machines (behaviour), and
- the byte-level wire shape (TLV, IM messages, sigma).
Schema parity is necessary but not sufficient. It is easy to advertise the right attribute IDs and revisions and still get the behaviour wrong — accept a write matter.js rejects, return the wrong status, skip a constraint, drop a session that must survive. Those defects pass every schema test and surface as silent Apple / Google pair-aborts or mis-behaving devices that take days to attribute back. Behaviour parity is the bar.
This is not hypothetical: a parity sweep found a whole class of write-constraint
and command-semantics defects (wrong setpoint limits, an unenforced
percent-max, a SupportedOperatingModes bitmap that marked a mandatory mode
unsupported, a MoveToColorTemperature that never moved, an UpdateNOC that
tore down its own response session) — every one of which a schema test would
have waved through.
3. The workflow for every change¶
- Read the matter.js source first. Likely paths:
- schema constant / revision / id →
../matter.js/packages/model/src/standard/elements/<name>.element.ts - cluster behaviour (defaults, mandatory attrs, conformance, write
constraints, command logic) →
../matter.js/packages/node/src/behaviors/<name>/<Name>Server.ts - device type →
../matter.js/packages/node/src/devices/<name>.ts - wire codec / IM / sigma →
../matter.js/packages/types/src/tlv/,../matter.js/packages/protocol/src/ - wire-truth cross-check →
../connectedhomeip/src/app/...,../connectedhomeip/src/messaging/... - Mirror the behaviour in Go idiom (struct-with-methods,
context.Context, goroutines for TS decorators / mixins /Promise<T>). Keep the same defaults, constraints, status codes, and order. - Cite the source in the Go code:
// Mirrors matter.js packages/node/src/behaviors/.../FooServer.ts:bar(or the chip path). Provenance must survive drift. - Add a parity test — behaviour, not just schema. A new constraint adds a row to the negative-write parity table (§4). A new cluster server adds a schema parity case. A wire change adds / updates a TLV fixture. PRs without parity coverage are rejected.
- Record deliberate divergences in
notes/parity/by_design.md(the living catalogue). A non-trivial divergence also gets an ADR underdocs/adr/. Valid divergences are TypeScript-only optimisations that fight Go's GC, or decorator patterns with no Go equivalent. Invalid divergences are hand-coding cluster revisions, attribute IDs, constraint defaults, status codes, or Apple-required tag patterns — those go verbatim from matter.js.
4. The standing guards (enforcement)¶
Parity is held by build- and test-time guards, not by periodically regenerated audit reports. These are the mechanism; keep them green and extend them with every change:
| Guard | Location | Locks |
|---|---|---|
| Schema parity | parity/ (the embedded matter.js HEAD extract) + schema/parity_test.go, schema/provenance_test.go, schema/staleness_test.go, and the *parity_matterjs_test.go files across the tree |
cluster / device-type IDs, revisions, attribute / command / event IDs vs matter.js HEAD; that the generated Go maps match the snapshot they were generated from |
| Behavioural negative-write parity | cluster/matter_negative_write_parity_test.go |
a write/invoke matter.js rejects is rejected with the matching IM status (ConstraintError 0x87 / InvalidCommand 0x85), plus boundary positive controls against over-rejection. Add one row per new constraint. |
| Wire-codec parity | tlv/parity_matterjs_test.go, im/wire_fixtures_parity_test.go, bridge/groups_parity_matterjs_test.go (GroupKeyManagement / Groups command payloads), bridge/groupcast_parity_matterjs_test.go (Groupcast commands, Membership / AuxiliaryAcl values and events) and groups/parity_matterjs_test.go (group key schedule and sealed group messages), each reading its own testdata/ copy of the fixtures; the generators live at notes/parity/matter/ |
byte-level TLV / IM / group-message shape |
| Reference-controller validation | conformance/ and internal/chiptool/ under the chiptool build tag, driven by .github/workflows/chiptool.yml |
end-to-end behaviour against the real chip-tool commissioner, plus borrowed CSA Test_TC_* cases |
| matter.js pin freshness | .github/workflows/matterjs-pin.yml + schema/staleness_test.go |
the embedded extract does not silently fall behind matter.js HEAD |
| matter.js citations | script/citecheck, make cite-check (needs a matter.js clone in MATTERJS_DIR), the cite-check job in .github/workflows/ci.yml |
every <file>.ts:<line> the Go code cites resolves at the pin (parity/schema.json → matter.sourceCommit, or the citation's own at <sha>) — a packages/… path exactly, a partial path as a unique suffix, a bare file name as a unique basename — and its lines exist (hard: fails); a citation whose sentence gives, nearer to it than to any other reference, names the cited file contains, none of them within ±15 lines of the cited range, is drift (soft), listed in the committed script/citecheck/drift.json and held by the exact ratchet citecheckDriftRatchet. Fails on a hard error and when regenerating moves drift.json or summary.md; make cite-report regenerates them, committed with the ratchet constant in the same commit. |
| One time port | clock/noraw_test.go, part of make test |
no production .go file outside clock/, examples/ and script/ calls or references time.Now, time.AfterFunc, time.NewTimer, time.NewTicker, time.Sleep, time.After, time.Tick, time.Since or time.Until; every interval runs on a clock.Clock, so a test drives matter.js's timings with clocktest.Fake (after matter.js MockTime) instead of the wall clock. Exceptions are listed in the test with a reason and an exact count (one: the socket read deadlines of mdns/subtype_responder.go). |
| Divergence catalogue | notes/parity/by_design.md |
every intentional deviation, with rationale and what pins it |
| Open-gap register | notes/parity/matter_behaviour_findings.md |
known behaviour gaps that are not by design, as scoped fix packages |
| Test-porting register | script/testparity/ (register.json against the committed snapshot matterjs_tests.json), rendered to notes/parity/matterjs_tests.md; make testparity-check and the CI job testparity |
every *Test.ts under matter.js's packages/node/test and packages/protocol/test at the pin is ported, partial (naming the it cases not covered), planned or excluded with one of five classes; every Go test an entry names exists; a ported, partial or excluded verdict fails once its matter.js file changes; the number of planned files sits exactly on testparityPlannedRatchet |
When a parity sweep is genuinely warranted, prefer extending these guards over producing a throw-away report: a guard catches the next regression, a report catches only today's.
Sweeping a pin bump¶
Moving parity/schema.json to a newer matter.js commit propagates the
schema mechanically; the behaviour that changed between the two pins does
not follow by itself. Every pin bump is therefore followed by a sweep of the
commits in between, the way
notes/parity/sweeps/2026-10-f07365a8-85cf6647.md
was made:
- List the commits, not just the files:
git -C ../matter.js log --oneline <old>..<new> -- packages/node/src packages/protocol/src packages/types/src. A commit message carries the reason a file diff hides. - Read each commit at its pin (
git -C ../matter.js show <commit>) and give it one verdict: (1) not applicable to the roles this module plays (device-side responder, commissioner/controller) or to a feature it does not have — say which; (2) already matches — cite the Go code; (3) go-fabric differs — fix it through the workflow above (read, mirror, cite, unit or parity test, CHANGELOG), or, if it is large, record a finding in the open-gap register. - Commit the table as
notes/parity/sweeps/<yyyy-mm>-<old>-<new>.md(commit, subject, verdict, Go reference / fix commit / finding), and re-run every certification family that covers a verdict-3 change. - Refresh the test snapshot (
make testparity-snapshot, against the same clone):make testparity-checkthen names every test file upstream added or removed and everyported,partialorexcludedentry whose file changed. Re-check those mappings and exclusions, update theirsnapshotSha, and movetestparityPlannedRatchetwith theplannedcount in the same commit.
5. Where the host's own parity obligation begins¶
This module owns the wire format. A bridge is this module plus a host that
projects its own devices onto clusters, and the host half has its own gold
standard — for OpenCCU-Loom, the reference consumer, that is aiohomematic on
the CCU side. The two reference layers do not overlap: CCU wire knowledge stays
in aiohomematic, Matter wire knowledge stays in matter.js.
The boundary is the contract/ package. Everything on this side
of it mirrors matter.js and is covered by §4. Everything on the far side —
which data point becomes which cluster, which device type a product maps to —
is the host's decision and the host's parity contract. A host that gets the
projection wrong produces a device that pairs and then misbehaves, which is
exactly the class of defect §2 is about; it is simply not caught by the guards
listed here.
One asymmetry worth stating: a port of a CCU stack converges, because the source stands still. The Matter mirror never "finishes" — matter.js HEAD bumps cluster and device-type revisions, and the wire shape is interop-critical. Parity here is a permanent discipline, re-verified on every change and whenever the matter.js pin is advanced.
6. The non-negotiable checklist¶
Before you open a PR:
- [ ] I read the matter.js (and, for wire-truth, chip) source for this cluster / behaviour / device-type.
- [ ] My implementation mirrors its defaults, constraints, status codes, and
order — or the divergence is recorded in
by_design.md(+ ADR if non-trivial). - [ ] I cited the matter.js / chip
path:linein the Go code. - [ ] I added or extended a behaviour parity test (a constraint row, a command-semantics case, a wire fixture) — not only a schema assertion.
- [ ] The standing guards in §4 are green.
If you cannot check every box, the change is not ready.