Found while taking the measurement for #5899 (see PR #6284 for the full population). Filed unassigned as an observation, for PM triage. Not fixed there — out of that card's scope.
The divergence
packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.tsx:60:
interfaceFlowNode{id: string;type?: string;label?: string;description?: string;config?: Record<string,unknown>;[k: string]: unknown;}Probed against the installed @objectstack/spec@17.2.0:
FlowNodeSchema.safeParse({ id, type, label, description, config })
→ unrecognized_keys: ["description"]
FlowNodeSchema is .strict(), and its refusal message is explicit about why:
Unrecognized key(s) on this flow node: … Until #4001 these were dropped silently — the node still parsed, so a mis-placed config shipped as a step that quietly ignored it.
So this inspector's type says an author may put description on a flow node; the contract refuses it. Same direction as the inverted arm recorded in #5652 — a local declaration admitting what the contract rejects. Second, smaller divergence in the same interface: label? is optional here and required by FlowNodeSchema.
The duplicate-copy half
The same file declares FlowEdge at line 69, and FlowEdgeInspector.tsx:49 declares FlowEdge too. The two copies already disagree:
| FlowEdgeInspector.tsx:49 | FlowNodeInspector.tsx:69 |
|---|
condition | ExpressionInput | unknown |
FlowEdgeInspector's version is the correct one and says so in its own doc comment — it was narrowed to ExpressionInput precisely because the looser string \| { source?: string } spelling described an envelope the server rejects, which is how #3171 came to be filed against a defect that does not reproduce (#3202). The second copy in FlowNodeInspector was not narrowed with it. "Two copies of one shape is how the wrong one survives being fixed" is FlowPreview.tsx's own comment about this exact pair.
ActionParam has the same problem: ActionDefaultInspector.tsx:266 and ActionPreview.tsx:47 are two hand subsets of ActionParamSchema that disagree on options / helpText / defaultValue, on the index signature, and on label's type.
Why nothing caught it
All of these are module-local interfaces, and both scanners in scripts/check-spec-symbol-derivation.mjs skip non-exported declarations — including declarations under the spec's own export names, which is rule 1's trigger. That hole is #5899's subject; these sit in its measured population.
Fix shape
The narrow fix is to drop description (or, if flow nodes genuinely should carry one, that is a spec change, not a local type widening) and make label required. The durable fix is the one FlowPreview.tsx already made for its own pair — stop restating the shape and alias the canvas's type, so there is one declaration per shape in the package instead of two or three.
Measured on a76b18cf2 against @objectstack/spec@17.2.0.
Related: #5899 (the instrument hole that hid it), PR #6284 (the census), #5652 (the same failure shape, found by reading rather than by a gate), #3202.
Found while taking the measurement for #5899 (see PR #6284 for the full population). Filed unassigned as an observation, for PM triage. Not fixed there — out of that card's scope.
The divergence
packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.tsx:60:Probed against the installed
@objectstack/spec@17.2.0:FlowNodeSchemais.strict(), and its refusal message is explicit about why:So this inspector's type says an author may put
descriptionon a flow node; the contract refuses it. Same direction as the inverted arm recorded in #5652 — a local declaration admitting what the contract rejects. Second, smaller divergence in the same interface:label?is optional here and required byFlowNodeSchema.The duplicate-copy half
The same file declares
FlowEdgeat line 69, andFlowEdgeInspector.tsx:49declaresFlowEdgetoo. The two copies already disagree:FlowEdgeInspector.tsx:49FlowNodeInspector.tsx:69conditionExpressionInputunknownFlowEdgeInspector's version is the correct one and says so in its own doc comment — it was narrowed toExpressionInputprecisely because the looserstring \| { source?: string }spelling described an envelope the server rejects, which is how #3171 came to be filed against a defect that does not reproduce (#3202). The second copy inFlowNodeInspectorwas not narrowed with it. "Two copies of one shape is how the wrong one survives being fixed" isFlowPreview.tsx's own comment about this exact pair.ActionParamhas the same problem:ActionDefaultInspector.tsx:266andActionPreview.tsx:47are two hand subsets ofActionParamSchemathat disagree onoptions/helpText/defaultValue, on the index signature, and onlabel's type.Why nothing caught it
All of these are module-local
interfaces, and both scanners inscripts/check-spec-symbol-derivation.mjsskip non-exported declarations — including declarations under the spec's own export names, which is rule 1's trigger. That hole is #5899's subject; these sit in its measured population.Fix shape
The narrow fix is to drop
description(or, if flow nodes genuinely should carry one, that is a spec change, not a local type widening) and makelabelrequired. The durable fix is the oneFlowPreview.tsxalready made for its own pair — stop restating the shape and alias the canvas's type, so there is one declaration per shape in the package instead of two or three.Measured on
a76b18cf2against@objectstack/spec@17.2.0.Related: #5899 (the instrument hole that hid it), PR #6284 (the census), #5652 (the same failure shape, found by reading rather than by a gate), #3202.