tools: add output-manifest-verify; extend flow-lint with fan-out checks
- tools/output-manifest-verify/ — enforces .claude/rules/output-coverage.md
§3: every node ships test/_output-manifest.md and every declared key
is referenced by at least one test file. First run shows only
machineGroupControl has the manifest (16 keys covered); all other nodes
warn. --strict escalates "missing manifest" to an error for CI gating.
- flow-lint gains two rules from the same output-coverage rule:
* FN_OUTPUT_WIRES_MISMATCH — function declares outputs=N but wires has
M arrays (causes silent dropped or duplicate emissions).
* FN_PAYLOAD_NULL_LITERAL — function source contains `payload: null`
literal (the η-null ui-chart crash pattern from 2026-05-14).
First run found 1 instance in mgc/02-Dashboard.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ function lintFlow(flowPath) {
|
||||
checkLinkPair(n, byId, findings);
|
||||
checkDebugLog(n, findings);
|
||||
checkBackwardWires(n, findings);
|
||||
checkFunctionFanOut(n, findings);
|
||||
}
|
||||
checkGroupWidths(nodes, findings);
|
||||
return findings;
|
||||
@@ -172,6 +173,28 @@ function checkBackwardWires(n, findings) {
|
||||
}
|
||||
}
|
||||
|
||||
function checkFunctionFanOut(n, findings) {
|
||||
if (n.type !== 'function') return;
|
||||
const outputs = Number(n.outputs);
|
||||
if (!Number.isFinite(outputs) || outputs <= 1) return;
|
||||
if (!Array.isArray(n.wires) || n.wires.length !== outputs) {
|
||||
findings.push({
|
||||
rule: 'FN_OUTPUT_WIRES_MISMATCH',
|
||||
severity: 'error',
|
||||
node: n.id,
|
||||
msg: `function "${n.name || n.id}" declares outputs=${outputs} but wires has ${Array.isArray(n.wires) ? n.wires.length : 'no'} arrays.`,
|
||||
});
|
||||
}
|
||||
if (typeof n.func === 'string' && /payload\s*:\s*null\b/.test(n.func)) {
|
||||
findings.push({
|
||||
rule: 'FN_PAYLOAD_NULL_LITERAL',
|
||||
severity: 'error',
|
||||
node: n.id,
|
||||
msg: `function "${n.name || n.id}" emits a literal { payload: null } — crashes ui-chart on first frame; return the whole msg as null instead so the port skips.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkGroupWidths(nodes, findings) {
|
||||
const pages = new Map();
|
||||
for (const n of nodes) {
|
||||
|
||||
Reference in New Issue
Block a user