B2.3 + P11.1 + P11.2 + monster schema fix
B2.3 LatestWinsGate fireAndWait:
Added fireAndWait(value, ctx?) returning per-fire settlement promise.
Supersede resolves with frozen sentinel {superseded: true} (no
rejection — callers branch on value without try/catch). Dispatch
errors also resolve (with undefined); error surfaces via gate.lastError.
LatestWinsGate.js 75 → 116 lines. 12/12 tests pass.
P11.1 convert.possibilities(measure):
New helper returning sorted+deduped unit names for a measure.
Cached per measure. Reuses existing convert measures map. Also
exposed convert.measures() listing all known measures.
convert/index.js +21 lines. New test file: 90 lines, 12/12 tests.
P11.2 commandRegistry.units field:
Pre-dispatch normalisation pipeline. descriptor.units = {measure,
default}; commandRegistry extracts msg.payload + msg.unit (3 shapes),
validates against measure, converts to default, falls back + warns
with accepted-list on unknown/wrong-measure. Falls back gracefully
if convert.possibilities is missing. commandRegistry.js 164 → 237.
+7 new tests covering all 4 paths.
monster schema fix (P11.2 sibling):
generalFunctions/src/configs/monster.json was stripping four
legitimate constraint keys (nominalFlowMin, flowMax, maxRainRef,
minSampleIntervalSec). Added them with defaults matching the
legacy nodeClass coercion. Side effect: this also UNBLOCKED the
monster cooldown-guard test (separate ROOT-CAUSE entry below).
CONTRACTS.md §4 + §8 updated. 144/144 basic tests + 206/206 full
generalFunctions tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,32 @@
|
||||
// JSON-Schema. Anything richer belongs in the handler itself, which has
|
||||
// access to logger via ctx.
|
||||
|
||||
const convert = require('../convert');
|
||||
|
||||
const SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'object', 'any', 'none']);
|
||||
|
||||
function _acceptedList(measure) {
|
||||
if (convert && typeof convert.possibilities === 'function') {
|
||||
const list = convert.possibilities(measure);
|
||||
if (Array.isArray(list) && list.length) return list.join(', ');
|
||||
}
|
||||
return '(see convert docs)';
|
||||
}
|
||||
|
||||
function _describeUnit(unit) {
|
||||
try { return convert().describe(unit); } catch (_) { return null; }
|
||||
}
|
||||
|
||||
function _extractValueAndUnit(msg) {
|
||||
if (!msg || typeof msg !== 'object') return null;
|
||||
const p = msg.payload;
|
||||
if (typeof p === 'number') return { value: p, unit: msg.unit };
|
||||
if (p && typeof p === 'object' && typeof p.value === 'number') {
|
||||
return { value: p.value, unit: p.unit ?? msg.unit };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class CommandRegistry {
|
||||
constructor(commands, options = {}) {
|
||||
if (!Array.isArray(commands)) {
|
||||
@@ -45,11 +69,13 @@ class CommandRegistry {
|
||||
throw new Error(`alias '${alias}' for '${cmd.topic}' collides with existing topic or alias`);
|
||||
}
|
||||
}
|
||||
const units = this._validateUnits(cmd);
|
||||
const descriptor = {
|
||||
topic: cmd.topic,
|
||||
aliases,
|
||||
payloadSchema: cmd.payloadSchema || null,
|
||||
description: typeof cmd.description === 'string' ? cmd.description : null,
|
||||
units,
|
||||
handler: cmd.handler,
|
||||
};
|
||||
this._byKey.set(cmd.topic, descriptor);
|
||||
@@ -60,6 +86,17 @@ class CommandRegistry {
|
||||
this._descriptors.push(descriptor);
|
||||
}
|
||||
|
||||
_validateUnits(cmd) {
|
||||
if (cmd.units === undefined || cmd.units === null) return null;
|
||||
const { measure, default: def } = cmd.units;
|
||||
if (typeof measure !== 'string' || measure.length === 0 ||
|
||||
typeof def !== 'string' || def.length === 0) {
|
||||
throw new TypeError(
|
||||
`command '${cmd.topic}' units requires { measure: string, default: string }`);
|
||||
}
|
||||
return { measure, default: def };
|
||||
}
|
||||
|
||||
has(topic) {
|
||||
return typeof topic === 'string' && this._byKey.has(topic);
|
||||
}
|
||||
@@ -77,6 +114,7 @@ class CommandRegistry {
|
||||
aliases: d.aliases.slice(),
|
||||
payloadSchema: d.payloadSchema,
|
||||
description: d.description,
|
||||
units: d.units ? { measure: d.units.measure, default: d.units.default } : null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -99,6 +137,7 @@ class CommandRegistry {
|
||||
return;
|
||||
}
|
||||
if (topic !== descriptor.topic) this._noteAlias(topic, descriptor.topic, log);
|
||||
if (descriptor.units) this._normaliseUnits(descriptor, msg, log);
|
||||
if (!this._validatePayload(descriptor, msg, log)) return;
|
||||
return descriptor.handler(source, msg, ctx);
|
||||
}
|
||||
@@ -111,6 +150,40 @@ class CommandRegistry {
|
||||
log.warn?.(`topic '${alias}' is deprecated; use '${canonical}'`);
|
||||
}
|
||||
|
||||
_normaliseUnits(descriptor, msg, log) {
|
||||
const { measure, default: defaultUnit } = descriptor.units;
|
||||
const extracted = _extractValueAndUnit(msg);
|
||||
if (!extracted) return; // unknown shape — let payload validator handle it
|
||||
let { value, unit } = extracted;
|
||||
if (unit === undefined || unit === null || unit === '') {
|
||||
// No unit supplied — assume default, silent.
|
||||
msg.payload = value;
|
||||
msg.unit = defaultUnit;
|
||||
return;
|
||||
}
|
||||
const desc = _describeUnit(unit);
|
||||
if (!desc) {
|
||||
log.warn?.(`${descriptor.topic}: unknown unit '${unit}'. Accepted: ${_acceptedList(measure)}. Treating ${value} as ${defaultUnit}.`);
|
||||
msg.payload = value;
|
||||
msg.unit = defaultUnit;
|
||||
return;
|
||||
}
|
||||
if (desc.measure !== measure) {
|
||||
log.warn?.(`${descriptor.topic}: unit '${unit}' is ${desc.measure}, expected ${measure}. Accepted: ${_acceptedList(measure)}. Treating ${value} as ${defaultUnit}.`);
|
||||
msg.payload = value;
|
||||
msg.unit = defaultUnit;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
msg.payload = convert(value).from(unit).to(defaultUnit);
|
||||
msg.unit = defaultUnit;
|
||||
} catch (err) {
|
||||
log.warn?.(`${descriptor.topic}: failed to convert ${value} ${unit} -> ${defaultUnit} (${err.message}). Treating as ${defaultUnit}.`);
|
||||
msg.payload = value;
|
||||
msg.unit = defaultUnit;
|
||||
}
|
||||
}
|
||||
|
||||
_validatePayload(descriptor, msg, log) {
|
||||
const schema = descriptor.payloadSchema;
|
||||
if (!schema) return true;
|
||||
|
||||
Reference in New Issue
Block a user