Alex2Node/scripts/capability-table.js
David dd071548bc 2.0.0
The interface registry, generated and checked discovery, typed dispatch with
automatic error answers, message builders, proactive events and typed
helpers for every error type; the 1.x API is kept and the examples of 1.5.2
run unchanged. docs/wire-changes.md lists what a 1.x caller can observe.

On 2026-09-28 a real Alexa account drove 33 test devices on this API through
the public Alex2MQTT service: Alexa accepted the discovery of all of them,
50 of 58 cases passed, 4 had no action in the control call and 4 failed on
what the check could see, none on an answer of the library. The readme table
names the interfaces that run covered. Voice commands were not tested yet.

307 tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 22:55:23 +00:00

124 lines
5.5 KiB
JavaScript

"use strict";
// Writes what readme.md takes from the code: the table of interfaces, from the registry, between the two marker
// lines, and the text of a file into the code block that follows a line <!-- file: examples/lamp.js -->.
//
// npm run build && npm run docs write them
// node scripts/capability-table.js --check exit 1 when the readme has another text
//
// It reads the built registry (dist/cjs), so the build comes first. test/readme.test.js fails on a stale readme.
const fs = require("node:fs");
const path = require("node:path");
const ROOT = path.join(__dirname, "..");
const README = path.join(ROOT, "readme.md");
const BEGIN = "<!-- capabilities: written by scripts/capability-table.js, npm run docs -->";
const END = "<!-- /capabilities -->";
// What was driven from a real Alexa account through the public Alex2MQTT service, and when: 33 test devices on
// the 2.0 API, 60 cases. A row is added here when a run is recorded, not when a descriptor is written.
const DRIVEN_ON = "2026-09-28";
const DRIVEN_WITH = "alex2node 2.0.0";
const DRIVEN = {
"Alexa": "ReportState",
"Alexa.BrightnessController": "directives, change report",
"Alexa.ColorController": "directives",
"Alexa.ColorTemperatureController": "directives",
"Alexa.ContactSensor": "change report",
"Alexa.DoorbellEventSource": "event",
"Alexa.InventoryLevelSensor": "change report",
"Alexa.LockController": "directives with a deferred answer, change report",
"Alexa.ModeController": "directives",
"Alexa.MotionSensor": "change report",
"Alexa.PercentageController": "directives",
"Alexa.PowerController": "directives, change report",
"Alexa.PowerLevelController": "directives",
"Alexa.RangeController": "directives, change report",
"Alexa.SceneController": "directives",
"Alexa.SecurityPanelController": "directives, change report",
"Alexa.SimpleEventSource": "event",
"Alexa.TemperatureSensor": "state report",
"Alexa.ThermostatController": "directives",
"Alexa.ThermostatController.Schedule": "state report",
"Alexa.ToggleController": "directives",
};
const code = (names) => (names.length > 0 ? names.map((name) => `\`${name}\``).join(", ") : "-");
function row(descriptor) {
const { namespace, version, doc, tier, instanced, properties, directives, events = {} } = descriptor;
return [
`[${namespace}](${doc})${instanced ? " (instances)" : ""}`,
version,
code(Object.keys(properties)),
code(Object.keys(directives)),
code(Object.keys(events)),
String(tier),
DRIVEN[namespace] ?? "-",
];
}
/** The text between the markers, from the registry of the build. */
function table(registry = require(path.join(ROOT, "dist", "cjs", "index.js")).registry) {
const all = registry.list();
const unknown = Object.keys(DRIVEN).filter((namespace) => !registry.has(namespace));
if (unknown.length > 0) throw new Error(`DRIVEN names ${unknown.join(", ")}, which the registry does not have`);
const described = all.filter((descriptor) => descriptor.tier !== 3);
const named = all.filter((descriptor) => descriptor.tier === 3);
const header = ["Interface", "Version", "Properties", "Directives", "Events", "Tier", "Through Alexa"];
const lines = [header, header.map(() => "---"), ...described.map(row)].map((cells) => `| ${cells.join(" | ")} |`);
return [
...lines,
"",
`${described.length} interfaces are described (tier 1 and 2), ${named.length} are named (tier 3).`,
`"Through Alexa" is what a real Alexa account drove through the public Alex2MQTT service on ${DRIVEN_ON}, with`,
`${DRIVEN_WITH}; "-" is an interface that no such run has covered.`,
"",
`Tier 3: ${named.map(({ namespace, version, doc }) => `[${namespace}](${doc}) ${version}`).join(", ")}.`,
].join("\n");
}
// A line that names a file, and the code block after it
const FILE = /<!-- file: (\S+) -->\n```(\w+)\n[\s\S]*?\n```/g;
/** The readme with the text the named files have now in their code blocks. */
function files(readme) {
return readme.replace(FILE, (block, file, language) => {
const text = fs.readFileSync(path.join(ROOT, file), "utf8").trimEnd();
if (text.includes("```")) throw new Error(`${file} has a code fence, which would end its block in readme.md`);
return `<!-- file: ${file} -->\n\`\`\`${language}\n${text}\n\`\`\``;
});
}
/** The files the readme shows, as the marker lines name them. */
function shown(readme) {
return [...readme.matchAll(FILE)].map(([, file]) => file);
}
/** The readme with the table of now and the files of now. Throws when the markers are not there, each once and in this order. */
function update(readme, text = table()) {
const begin = readme.indexOf(BEGIN);
const end = readme.indexOf(END);
if (begin < 0 || end < begin || readme.indexOf(BEGIN, begin + 1) >= 0 || readme.indexOf(END, end + 1) >= 0) {
throw new Error(`readme.md needs the lines "${BEGIN}" and "${END}", once each and in this order`);
}
return files(`${readme.slice(0, begin)}${BEGIN}\n${text}\n${readme.slice(end)}`);
}
if (require.main === module) {
const before = fs.readFileSync(README, "utf8");
const after = update(before);
if (process.argv.includes("--check")) {
if (after !== before) {
console.error("readme.md has another table of interfaces or another text of a file than the code gives: npm run build && npm run docs");
process.exit(1);
}
} else if (after !== before) {
fs.writeFileSync(README, after);
console.log("readme.md was written");
} else {
console.log("readme.md is up to date");
}
}
module.exports = { table, update, files, shown, BEGIN, END, DRIVEN, README };