Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ The main work (all changes without a GitHub username in brackets in the below li
## __WORK IN PROGRESS__
-->

## __WORK IN PROGRESS__

- @matter/model
- Breaking: A provisional element is no longer mandatory; conformance following a `P` describes the conformance intended once the element leaves provisional state

- @matter/types
- Breaking: Provisional cluster elements are now typed as optional rather than always present

- @matter/node
- Breaking: Provisional elements are no longer implemented by default; supply a state value or use `ClusterBehavior.enable()` to implement one

## 0.17.7 (2026-07-27)

- @matter/general
Expand Down
16 changes: 14 additions & 2 deletions packages/model/src/aspects/Conformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,17 @@ function computeApplicability(features: Set<string>, supportedFeatures: Set<stri
if (ast.type === Conformance.Special.Otherwise) {
let fallback = None;

// Per §7.3 terms following a "P" describe the conformance intended once the element is no longer provisional,
// so they contribute their condition but may never make the element mandatory. A provisional element must
// remain in the conformant view so an application can enable it.
let provisional = false;

for (const node of ast.param) {
if (node.type === Conformance.Flag.Provisional) {
provisional = true;
continue;
}

switch (assessOuterExpression(node)) {
case Conditional:
fallback = Conditional;
Expand All @@ -823,7 +833,7 @@ function computeApplicability(features: Set<string>, supportedFeatures: Set<stri
break;

case Mandatory:
return Mandatory;
return provisional ? Optional : Mandatory;
}
}

Expand Down Expand Up @@ -853,9 +863,11 @@ function computeApplicability(features: Set<string>, supportedFeatures: Set<stri

case Conformance.Flag.Disallowed:
case Conformance.Flag.Deprecated:
case Conformance.Flag.Provisional:
return None;

case Conformance.Flag.Provisional:
return Optional;

case Conformance.Flag.Mandatory:
return Mandatory;

Expand Down
13 changes: 10 additions & 3 deletions packages/model/src/logic/cluster-variance/InferredComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,13 @@ function addElement(components: InferredComponents, element: ValueModel) {
return;
}

// The intended conformance of a provisional element contributes its condition but may not make it mandatory
let provisional = false;

while (true) {
if (text.match(/^[DP], /)) {
const leading = text.match(/^([DP]), /);
if (leading) {
provisional ||= leading[1] === "P";
text = text.substring(3);
} else {
break;
Expand All @@ -358,8 +363,10 @@ function addElement(components: InferredComponents, element: ValueModel) {

if (text === "D") {
text = "O";
} else if (text === "M, D" || text === "P") {
} else if (text === "M, D") {
text = "M";
} else if (text === "P") {
text = "O";
}

// Revision conformance (Rev >= vN) indicates when an element was introduced.
Expand Down Expand Up @@ -391,7 +398,7 @@ function addElement(components: InferredComponents, element: ValueModel) {
const match = text.match(matcher.pattern);
if (match) {
matcher.processor((optional, condition) => {
addVariance(components, element, optional, condition);
addVariance(components, element, optional || provisional, condition);
}, match.slice(1));
return;
}
Expand Down
56 changes: 56 additions & 0 deletions packages/model/test/aspects/ConformanceTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,4 +563,60 @@ describe("Conformance", () => {
expect(conformance.toString()).equal("Rev >= v3");
});
});

describe("applicability", () => {
const { None, Optional, Conditional, Mandatory } = Conformance.Applicability;

function applicability(definition: string, ...supportedFeatures: string[]) {
return new Conformance(definition).applicabilityFor({
definedFeatures: new Set(["AA", "BB"]),
supportedFeatures: new Set(supportedFeatures),
});
}

it("resolves plain flags", () => {
expect(applicability("M")).equal(Mandatory);
expect(applicability("O")).equal(Optional);
expect(applicability("X")).equal(None);
expect(applicability("D")).equal(None);
});

it("resolves features", () => {
expect(applicability("AA", "AA")).equal(Mandatory);
expect(applicability("AA")).equal(None);
expect(applicability("AA, O", "AA")).equal(Mandatory);
expect(applicability("AA, O")).equal(Optional);
});

it("makes a standalone provisional element optional", () => {
expect(applicability("P")).equal(Optional);
});

it("caps an intended mandatory conformance at optional", () => {
expect(applicability("P, M")).equal(Optional);
});

it("caps an intended feature conformance at optional", () => {
expect(applicability("P, AA", "AA")).equal(Optional);
expect(applicability("P, AA & BB", "AA", "BB")).equal(Optional);
});

it("excludes an intended feature conformance whose feature is unsupported", () => {
expect(applicability("P, AA")).equal(None);
expect(applicability("P, AA & BB", "AA")).equal(None);
});

it("leaves an intended optional conformance optional", () => {
expect(applicability("P, O")).equal(Optional);
expect(applicability("P, [AA & BB]", "AA", "BB")).equal(Optional);
});

it("leaves an intended conditional conformance conditional", () => {
expect(applicability("P, desc")).equal(Conditional);
});

it("does not cap a mandatory term preceding the provisional term", () => {
expect(applicability("AA, P", "AA")).equal(Mandatory);
});
});
});
17 changes: 14 additions & 3 deletions packages/model/test/logic/ClusterVarianceTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ describe("ClusterVariance", () => {
expectComponents(attrs({ name: "attr", conformance: "D" }), { optional: ["attr"] });
});

it("ignores provisional", () => {
expectComponents(attrs({ name: "attr", conformance: "P, M" }), { mandatory: ["attr"] });
it("classifies provisional as optional", () => {
expectComponents(attrs({ name: "attr", conformance: "P, M" }), { optional: ["attr"] });
});

it("classifies a provisional element without intended conformance as optional", () => {
expectComponents(attrs({ name: "attr", conformance: "P" }), { optional: ["attr"] });
});
});

Expand All @@ -43,6 +47,13 @@ describe("ClusterVariance", () => {
});
});

it("classifies provisional by feature as optional", () => {
expectComponents(attrs(["FOO"], { name: "attr", conformance: "P, FOO" }), {
optional: ["attr"],
condition: { allOf: ["FOO"] },
});
});

it("classifies optional by feature", () => {
expectComponents(attrs(["FOO"], { name: "attr", conformance: "[FOO]" }), {
optional: ["attr"],
Expand Down Expand Up @@ -132,7 +143,7 @@ describe("ClusterVariance", () => {

it("parses provisional comma otherwise-list P, FOO, BAR, BAZ", () => {
expectComponents(attrs(["FOO", "BAR", "BAZ"], { name: "attr", conformance: "P, FOO, BAR, BAZ" }), {
mandatory: ["attr"],
optional: ["attr"],
condition: { anyOf: ["FOO", "BAR", "BAZ"] },
});
});
Expand Down
21 changes: 19 additions & 2 deletions packages/node/src/behaviors/groupcast/GroupcastServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@ const UNMAPPED_KEYSET_ID = 0xffff;
/** TODO: remove once the Groupcast cluster leaves provisional state in the Matter specification. */
const GROUPCAST_IS_PROVISIONAL = true;

/**
* Every element of this provisional cluster is optional, so declare the ones this server implements.
*
* TODO: remove once the Groupcast cluster leaves provisional state in the Matter specification. Its elements become
* mandatory again at that point and matter.js implements them without this declaration.
*/
const Base = GroupcastBehavior.enable({
attributes: {
membership: true,
maxMembershipCount: true,
maxMcastAddrCount: true,
usedMcastAddrCount: true,
fabricUnderTest: true,
},
events: { groupcastTesting: true },
});

/**
* This is the default server implementation of {@link GroupcastBehavior}.
*
Expand All @@ -44,7 +61,7 @@ const GROUPCAST_IS_PROVISIONAL = true;
* - On first use by a fabric, marks the fabric as "GroupcastAdopted" in GKM, making GroupKeyMap read-only.
* - Migrates legacy group data from the Groups cluster on startup.
*/
export class GroupcastServer extends GroupcastBehavior {
export class GroupcastServer extends Base {
declare internal: GroupcastServer.Internal;

/** Timer for GroupcastTesting auto-disable. */
Expand Down Expand Up @@ -652,7 +669,7 @@ export namespace GroupcastServer {
}

/** Default state overrides for GroupcastServer. */
export class State extends GroupcastBehavior.State {
export class State extends Base.State {
/**
* Implementation-defined maximum membership count (min 10 per spec).
* Set to 2 * GKM.maxGroupsPerFabric (44) so per-fabric quota floor(44/2)=22 aligns exactly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ describe("GroupKeyManagementServer", () => {
MockTime.init();
});

it("does not implement the provisional GroupcastAdoption attribute by default", () => {
const state = new (GroupKeyManagementServer.with("Groupcast").State)();

expect(state.groupcastAdoption).undefined;
});

it("prevents too many group keys", async () => {
await using site = new MockSite();
// Device is automatically configured with vendorId 0xfff1 and productId 0x8000
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2022-2026 Matter.js Authors
* SPDX-License-Identifier: Apache-2.0
*/

import { ThreadNetworkDiagnosticsServer } from "#behaviors/thread-network-diagnostics";
import { AttributeId } from "@matter/types";
import { MockServerNode } from "../../node/mock-server-node.js";

// ExtAddress (0x3f) and Rloc16 (0x40) are provisional ("P, M") in Matter 1.6 so they are absent until an application
// implements them
const EXT_ADDRESS_ID = AttributeId(0x3f);
const RLOC16_ID = AttributeId(0x40);

describe("provisional ThreadNetworkDiagnostics attributes", () => {
async function attributeIdsOf(type: typeof ThreadNetworkDiagnosticsServer, state?: Record<string, unknown>) {
await using node = await MockServerNode.create(MockServerNode.RootEndpoint.with(type), {
threadNetworkDiagnostics: state,
});
return new Set(node.globalsOf(type).attributeList);
}

it("are absent by default", async () => {
const ids = await attributeIdsOf(ThreadNetworkDiagnosticsServer);

expect(ids.has(EXT_ADDRESS_ID)).false;
expect(ids.has(RLOC16_ID)).false;
});

it("are present once the application supplies a value", async () => {
const ids = await attributeIdsOf(ThreadNetworkDiagnosticsServer, { extAddress: null, rloc16: null });

expect(ids.has(EXT_ADDRESS_ID)).true;
expect(ids.has(RLOC16_ID)).true;
});
});
4 changes: 2 additions & 2 deletions packages/types/src/clusters/access-control.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading