Skip to content

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:

  1. Reason about the change. Understand the cluster, the command, the state machine, the failure modes. Do not cargo-cult.
  2. 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

  1. Read the matter.js source first. Likely paths:
  2. schema constant / revision / id → ../matter.js/packages/model/src/standard/elements/<name>.element.ts
  3. cluster behaviour (defaults, mandatory attrs, conformance, write constraints, command logic) → ../matter.js/packages/node/src/behaviors/<name>/<Name>Server.ts
  4. device type → ../matter.js/packages/node/src/devices/<name>.ts
  5. wire codec / IM / sigma → ../matter.js/packages/types/src/tlv/, ../matter.js/packages/protocol/src/
  6. wire-truth cross-check → ../connectedhomeip/src/app/..., ../connectedhomeip/src/messaging/...
  7. 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.
  8. 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.
  9. 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.
  10. Record deliberate divergences in notes/parity/by_design.md (the living catalogue). A non-trivial divergence also gets an ADR under docs/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:

  1. 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.
  2. 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.
  3. 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.
  4. Refresh the test snapshot (make testparity-snapshot, against the same clone): make testparity-check then names every test file upstream added or removed and every ported, partial or excluded entry whose file changed. Re-check those mappings and exclusions, update their snapshotSha, and move testparityPlannedRatchet with the planned count 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:line in 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.