B3.1 + B3.2 + B3.3: ChildRouter fan-out + commandRegistry 'none' + UnitPolicy dual-shape

B3.1 ChildRouter per-listener fan-out (drop emit monkey-patch):
  Partial-filter subscriptions enumerate every concrete
  <type>.measured.<position> event name (cartesian product over
  the canonical POSITIONS list + 19 KNOWN_TYPES) and register a
  plain emitter.on() per combo. Multi-parent semantics are trivial:
  each ChildRouter's listeners are independent. Drop the wrap/unwrap
  bookkeeping in tearDown. ChildRouter.js 184→164 lines.

B3.2 commandRegistry 'none' + description:
  Add 'none' to payloadSchema.type — handler still fires; logs warn
  if msg.payload is non-empty (catches accidental passes). Add
  optional `description` field per descriptor; surfaced via .list()
  so wikiGen can render per-topic effect text.
  commandRegistry.js 157→164 lines. 23/23 tests pass.

B3.3 UnitPolicy dual-shape:
  policy.canonical/output/curve are now BOTH callable methods AND
  frozen property bags. policy.canonical('flow') === 'm3/s' and
  policy.canonical.flow === 'm3/s' both work. Property bags are
  frozen (assign/delete/redefine throw in strict). Drops the
  _unitView workaround in MGC + rotatingMachine specificClass.
  UnitPolicy.js 149→163 lines, 15/15 tests pass.

CONTRACTS.md §4 + §6 updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-11 17:13:15 +02:00
parent ff9aec8702
commit f11754635b
6 changed files with 255 additions and 88 deletions

View File

@@ -10,7 +10,7 @@
// JSON-Schema. Anything richer belongs in the handler itself, which has
// access to logger via ctx.
const SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'object', 'any']);
const SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'object', 'any', 'none']);
class CommandRegistry {
constructor(commands, options = {}) {
@@ -49,6 +49,7 @@ class CommandRegistry {
topic: cmd.topic,
aliases,
payloadSchema: cmd.payloadSchema || null,
description: typeof cmd.description === 'string' ? cmd.description : null,
handler: cmd.handler,
};
this._byKey.set(cmd.topic, descriptor);
@@ -75,6 +76,7 @@ class CommandRegistry {
topic: d.topic,
aliases: d.aliases.slice(),
payloadSchema: d.payloadSchema,
description: d.description,
}));
}
@@ -119,6 +121,12 @@ class CommandRegistry {
return true;
}
if (type === 'any') return true;
if (type === 'none') {
if (payload !== undefined && payload !== null) {
log.warn?.(`${descriptor.topic}: payload ignored — this is a trigger-only topic`);
}
return true;
}
// typeof null === 'object' — explicit null fails an object schema.
if (type === 'object') {
if (payload === null || typeof payload !== 'object') {