Commit graph

13 commits

Author SHA1 Message Date
2558e21787 registry: RangeController, ModeController, ToggleController with resources, presets and semantics
The three generic controllers are described in full and leave the stub table (62 stubs remain). A declaration
takes an instance name, friendly names from text() and asset(), and the options of the interface: range, unit and
presets for a range; supportedModes and ordered for a mode; semantics() for all three, with actions mapped to
directives and states mapped to a value or a range of values.

Refused at device.add(), each with the endpoint, interface and instance in the message: no instance name or no
friendly name; an instance declared twice; a first friendly name or a phrase used by another capability of the
endpoint; a reserved word, an asset id that is not in the catalog; range.max not above range.min; a preset off the
precision grid or outside the range; fewer than two modes or a mode listed twice; a mapping to a directive the
interface does not have, to a payload that does not fit, to a mode that is not listed, to AdjustMode on modes
that are not ordered; SetEcoOn, SetEcoOff, EcoOn and EcoOff on anything but a toggle.

For 1.x callers: Alexa.RangeController lists rangeValue (1.5.2 sent supported: []); addSupportedModes() announces
a mode given as a string as { value } and notes it, and takes { ordered: false } as a second argument, the default
stays true as 1.5.2 sent it (device.add() defaults to false). Declared the 1.x way, the zoo fan and curtain give
the same JSON as before.

Tests: the zoo blind, whose range, unit and semantics were written by hand into the JSON of 1.5.2, is declared
with device.add() and equals that JSON plus the Alexa capability; five capability objects of Amazon's examples
are reproduced the same way. 27 examples of four pages added. npm test: 108 pass (was 85) in 10.5-11.3 s, also on
Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:50:25 +00:00
c492ef74d1 discovery: generate capability JSON from the registry
A capability is a descriptor plus what the endpoint declares (device/Capability.ts), and its discovery object is
generated from the two. The new API is bridge.addDevice({ endpointId, name, categories, ... }) and
device.add(PowerController, options): both throw a DeclarationError that names the endpoint, the interface and
the instance (device/validate.ts), and leave the bridge and the device as they were. AlexaInterface is the same
Capability with the 1.x methods on it; it, ActionMapping and the enums moved to src/compat/, Device to src/device/.

What a 1.x caller can observe:
- every endpoint ends with { type: "AlexaInterface", interface: "Alexa", version: "3" } (alexa-interface.html);
  new Alex2MQTT(..., { alexaInterface: false }) leaves it out
- the fields of a capability object come in the order of Amazon's examples; their content is unchanged
- addCapability() with a name that is not an interface throws (1.5.2 announced it with the version "UNKNOWN")
- ActionMapping takes the payload as an object; a JSON string is parsed (1.5.2 sent the string), any other throws
- what Alexa would reject in a 1.x declaration is not refused: device.check() lists it and the bridge logs each
  line once, as "warning: ..." through the log hook, when it answers a discovery
- a device whose JSON cannot be built is left out of the answer and reported as an error event
- PowerController and EndpointHealth are the descriptors and keep ON/OFF and OK/UNREACHABLE; PowerState is new

Tests: six zoo devices declared the 1.x way give the JSON that Alexa accepted from 1.5.2 on 2026-09-28, plus the
Alexa capability. npm test: 85 pass (was 57) in 10-12 s, also on Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:35:46 +00:00
aa0ffd64ea registry: describe an Alexa interface as data
src/registry/ holds what the library knows about an interface: namespace, version, the page it was read from,
properties with their value schemas and directives with their payload schemas. Five interfaces are described
(Alexa, PowerController, BrightnessController, TemperatureSensor, EndpointHealth); the other 65 names of
AlexaInterfaceType are stubs with the version and property names of 1.5.2. schema.ts is the run-time check behind
it (241 lines, no new dependency), catalog.ts the vocabularies of the pages: 103 assets (23 units), 6 actions,
9 states, 56 display categories, 22 reserved words, 73 error types under 11 namespaces.

AlexaInterface.getVersion() and getProps() read the registry; the two switch statements are gone (-167 lines).
On the wire: Alexa.EndpointHealth is announced at 3.1 (was 3.3; the page is titled 3.1 and no page mentions 3.3),
and TimeHoldController and Camera.LiveViewController at 3 and 1.7 (1.5.2 sent the string "UNKNOWN").
DisplayCategory gains VACUUM. New exports: registry, DeclarationError, SchemaError, Assets, Units, Actions,
States, DisplayCategories and the descriptor types.

Tests: 20 JSON examples of the five pages under test/fixtures/alexa-docs; every directive payload and property
value in them parses with its descriptor. npm test: 57 pass (was 30) in 10.8 s, also on Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:18:22 +00:00
128ca35c2a test: find the imports of the builds with the compiler's scanner
The test that compares the imports of dist/ with "dependencies" matched a regular expression against the whole
file. tsc keeps comments in its output, so a usage line in a doc comment (import { ActionMapping } from
"alex2node") counted as an import and failed the commit gate with a message about dependencies.

ts.preProcessFile lists the imports instead. Stripping comments with two more expressions was tried first and
reads strings wrong: in `"src/*.ts"; require("real-one"); "*/"` it drops the require, and it never saw
import("lazy-one"); the scanner returns both. The declarations are scanned too: a .d.ts that imports a package the
consumer does not get breaks the consumer's type check.

Checked in a scratch copy: a doc comment and a string with an import in them pass; require("left-pad") in
dist/cjs, an aedes import in a declaration and an unused declared dependency each fail. npm test: 30 pass in
8.9-10.0 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:44:34 +00:00
d96c6b8372 build: refuse unknown arguments; name the fix when typescript is missing
Two failures of scripts/build.mjs did not say what to change. `node scripts/build.mjs --outt /some/dir` ignored
the misspelt flag, exited 0 and replaced the tracked dist/. Without devDependencies (npm install --omit=dev in a
clone runs "prepare") the script died with a stack trace, because typescript was resolved at module level,
outside the try block.

The script now takes no arguments or `--out <dir>` and answers anything else with one line and the usage, exit 1.
A missing compiler gives "build: typescript is not installed: run npm install (it is a devDependency)". The body
moved into build(args), so the staging directory and the compiler path are no longer module-level variables.

Two tests on a temporary package: three refused argument lists, and a package without node_modules; both compare
the package before and after. No change to dist/. npm test: 30 pass in 9.5-10.2 s (load average 7 on 4 cores).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:42:33 +00:00
ee311dab01 build: stage beside dist/, not inside it
The staging directory was dist/.build-XXXXXX. A build that is killed cannot remove it, the next build removed
only its own, and "files": ["dist"] put the leftover into the npm tarball. .gitignore hid it from git status and
the freshness test skipped every name starting with ".build-", so nothing reported it.

The staging directory is now .dist-staging beside the output (.<name>-staging for --out <dir>) and is cleared at
the start of each build. The freshness test no longer skips anything: a stray file under dist/ fails it.

Checked in a scratch copy: a build stopped with SIGKILL after 1.5 s leaves .dist-staging/ and an unchanged dist/;
npm pack lists 36 files, none from the staging directory; the next build removes it. New test for the same on a
temporary package. npm test: 28 pass in 8.2-9.5 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:40:36 +00:00
c340e11f6f test: a failed build leaves dist/ as it was
scripts/build.mjs and its commit message state that a failed build does not touch dist/. That held when checked by
hand, and no test covered it, so a later change to the swap could break it unnoticed.

test/build.test.js runs a copy of the script in a temporary package with one source file and the dist/ of an
earlier build, and compares every file and directory of that package before and after. Two cases: a type error
(the first compiler run fails, TS2322) and "export =" (CommonJS compiles, the ES module run fails with TS1203, so
the staging directory already holds cjs/ and types/).

Checked in a scratch copy: with the cleanup of the staging directory removed both tests fail; with cjs/ and types/
swapped in before the ES module run the second one fails. npm test: 27 pass in 5.7 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:34:53 +00:00
70072d39e3 build: the ES module build gets its own declarations
An ES module TypeScript consumer was given the CommonJS declarations in dist/types. With those,
`import alex2node from "alex2node"` compiled (module Node16) and then failed when Node loaded it: "The requested
module 'alex2node' does not provide an export named 'default'". The ES module build has named exports only.

tsconfig.esm.json now emits declarations next to dist/esm/*.js, under that directory's {"type": "module"}, and
"exports" selects per condition: import -> dist/esm/index.d.ts, require -> dist/types/index.d.ts. The default
import is now refused with TS1192; named imports are unchanged. "main", "module" and "types" are as before.

test/fixtures/types.mts holds the default import under @ts-expect-error, and a new test requires "types" before
"default" and a declaration for every built file. dist/: 25 files, 132,291 B -> 33 files, 155,647 B; the npm
tarball 25,125 B -> 25,837 B. npm test: 25 pass in 5.6 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:31:34 +00:00
f74773132a test: read an ErrorResponse off the broker; every answer has its own messageId
Step 2 replaced uuid by crypto.randomUUID() at three call sites (AlexaStatusMessage, AlexaErrorResponse,
Device.sendSceneResponse) and only the first was asserted, in the ES module test. With the messageId dropped or set
to a constant in the other two, the suite still passed 22/22, and no test read an ErrorResponse at all.

New: an ErrorResponse compared whole against the documented shape (header, endpoint, payload, no context), and one
test that sends two directives each to a light, an unreachable light and a scene and requires six different
version 4 UUIDs. The Response, StateReport, scene and change report tests check the messageId too; the pattern
lives in the harness.

Checked in a scratch copy: a missing or constant messageId at each of the three sites now fails the suite (the
missing one in AlexaStatusMessage does not compile). No change to src/ or dist/. npm test: 24 pass in 5.5 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:26:44 +00:00
1d88c7811a build: dual CJS/ESM output, ES2020 target, Node 18 floor
"exports" gains "import" and "types" next to "require": import "alex2node" failed with
ERR_PACKAGE_PATH_NOT_EXPORTED. scripts/build.mjs compiles src/ to dist/cjs, dist/esm and dist/types in a staging
directory and swaps them in only when both compiler runs passed. "main" and "types" follow for resolvers that do
not read "exports". Target ES6 -> ES2020, "engines": node >= 18, relative imports carry ".js" for Node's ESM loader.
uuid gives way to crypto.randomUUID(); mqtt is the only runtime dependency left.

Tests load the package by name. New: both entry points, the modules the builds import, dist/ equal to a fresh
build, a round trip on the ES module build, and two compiled type fixtures in place of the .d.ts regexes.
22 tests pass in 5.7 s. dist/: 16 files, 79,711 B -> 25 files, 132,291 B; the npm tarball is 25,125 B.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 13:58:17 +00:00
58869d44c1 test: split the 1.5.2 suite into a harness and per-topic files
test/helpers/harness.js holds the aedes broker, the Alexa-side watcher, until() and the cleanups, plus directive(),
connected() and setup(), which the old file spelled out in each test. test/alex2node.test.js (194 lines, 8 tests)
becomes connection, discovery, directives, change-report and typings.

The 50-line round-trip test is cut at its topic boundaries and the duplicate-endpointId test no longer carries the
thermostat discovery check, so a failure names a case: 8 tests become 15, every 1.5.2 assertion kept. Four checks
are stricter: the "directive" event is compared whole, the StateReport is checked for its token and properties, the
Celsius conversion covers lowerSetpoint as well as temperature, and a duplicate endpointId is listed once.

No change to src/ or dist/. npm test: 15 pass in 0.9 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 13:45:38 +00:00
275f00f8a7 1.5.2: send() never rejects (status, error, scene, change report: resolves the topic or "" and reports the failure through the bridge's error event when listened to - 1.5.1 rejected, and an un-caught .send() killed the host on any broker hiccup); types ship (declaration: true, dist/*.d.ts tracked; addSupportedModes takes {value, modeResources}, ActionMapping payload optional, addHealthProp accepts EndpointHealth or the string); disconnect()/connect() re-binds devices to the new client; registerDevice returns the existing device on a duplicate endpointId (warning via the log hook, console.warn without one); ThermostatController discovery lists targetSetpoint and drops adaptiveRecoveryStatus; the UNSUPORTED INTERFACE TYPE stderr spam goes through the log hook. Examples: require("alex2node"), an error listener in each, EndpointHealth.OK, neutral endpoint ids, BlindControl reads correlationToken from the header, the thermostat reports Fahrenheit as Fahrenheit, ExamplePowerController is power-only again + new ExamplePowerControllerWithBrightness. readme (install from Forgejo, 1.5.2 changelog, table syntax), LICENSE (MIT); tests for the non-rejecting send, reconnect, duplicate, thermostat discovery and the shipped declaration signatures (8/8 on an in-process broker).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 04:58:39 +00:00
717af632de 1.5.1: broker errors as events that never crash the host (error only when listened to; connect/offline/reconnect/close; connected flag), configurable broker host + mqtt options + log hook, Alexa.ChangeReport via Device.getChangeReport (<root>/changeReport), SceneController discovery + sendSceneResponse, addCapability options (proactivelyReported), unregisterDevice/clearDevices/getDevices/disconnect, discover/directive events, promise-returning quiet send(); tests on an in-process broker (aedes); readme
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 19:29:38 +00:00