Skip to content

feat(fiber): offline DefinitionLinter for authoring safety#195

Open
ottobot-ai wants to merge 2 commits into
mainfrom
feat/fiber-definition-validator
Open

feat(fiber): offline DefinitionLinter for authoring safety#195
ottobot-ai wants to merge 2 commits into
mainfrom
feat/fiber-definition-validator

Conversation

@ottobot-ai

Copy link
Copy Markdown
Collaborator

Implements Proposal 01 / P1 of the fiber-ergonomics program (docs/proposals/fiber-ergonomics/01-authoring-safety.md, RFCs in #192): a pure, offline, advisory-only validator that catches the hand-rolled-JSON-Logic footguns (F4/F5/F10) before a definition is signed and the cluster sees it — instead of misbehaving silently at combine.

What it is

DefinitionLinter.validate(definition, shape: Option[MachineShape] = None): List[Diagnostic] — pure (no F[_], no CalculatedState, no network), returning source-located Diagnostics (Severity = Error | Warning | Info, stable code, Location{transitionIndex, field, path}). Empty == clean.

It is tooling, never a consensus surface — never called from validateSignedUpdate or any combiner (CLAUDE.md #2/#3). It reads its vocabulary (the _-directive set and the context roots) straight from ReservedKeys — the same constants EffectExtractor/ContextProvider dispatch on — so it cannot drift from the runtime.

Checks (RFC §2 a–e)

  • (a) var-path resolution — every {"var":…} first segment resolved against the engine's actual roots → unknown-root Error / undeclared-state-read (Error with a shape, Warning without) / undeclared-event-read / undeclared-dep-read (a machines.<uuid> not in dependencies).
  • (b) the F4 killer — an _-prefixed effect key outside the recognized ReservedKeys set ⇒ unknown-directive Error (with nearest-directive suggestion); a non-_ key one edit from a directive ⇒ likely-dropped-underscore Warning.
  • (c) reachability — BFS from initialStateunreachable-state Error / dead-end-state Warning.
  • (d) conformance — write/read against the MachineShape if supplied, else internal-consistency (a field written by any transition is known for reads).
  • (e) depth — reuses the existing FiberRules.definitionExpressionsWithinDepthLimitsexpression-too-deep. (Gas estimation left out — the RFC marks it optional.)

Tests — 14/14

DefinitionLinterSuite reads all 9 shipped riverdale-economy/*.definition.json and asserts each lints clean (no Errors), plus an embedded copy so the property holds without file access; and asserts the diagnostics fire: a typo'd _trigerunknown-directive, a never-written state.X read ⇒ undeclared-state-read, an unreachable state ⇒ unreachable-state, and shape-based undeclared read/write ⇒ Errors. sharedData/Test/compile + scalafmtCheckAll clean.

Follow-ups (noted, not in scope)

A TypeScript mirror for the e2e runner / SDK editor-time checks, and wiring the dry-run into the runner pre-send, are the natural next step (the SDK template work is #227).

🤖 Generated with Claude Code

ottobot-ai and others added 2 commits June 26, 2026 18:29
Proposal 01 (fiber-ergonomics/01-authoring-safety): a pure, advisory-only
validator over a StateMachineDefinition. Resolves every {"var":...} path
against the context roots ContextProvider injects, rejects misspelled
_-directive keys (the F4 killer), checks reachability, checks read/write
conformance (shape-based or internal-consistency), and surfaces the existing
expression-depth cap -- as structured, source-located Diagnostics.

Vocabulary (directives + roots) is read from ReservedKeys so the linter can
never disagree with the runtime. PURE: no F[_], no CalculatedState, no
network. NOT a consensus surface -- never called from validateSignedUpdate or
a combiner (CLAUDE.md rules #2/#3).

Test: all 9 shipped riverdale *.definition.json lint clean of Errors, plus
synthetic cases for unknown-directive / undeclared-state-read /
unreachable-state / shape-based errors. 14/14 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
Adds the advisory static recipient-shape check to DefinitionLinter, the
offline shift-left for the chain's object-form-only `_transferAsset` recipient
(EffectExtractor now raises CombineRejected at combine on anything else).

The resolved recipient value is checked at combine (it's usually dynamic);
this catches the STATIC shape offline: a literal bare-string recipient (the
removed legacy form) is a Warning, a literal object that can never decode to
an AssetHolder (wrong/typo'd variant, or a Fiber/Wallet wrapper missing its
inner field) is an Error, and a dynamic {"var":..}/computed recipient is left
to the combiner. Advisory only — no hard registration gate (the maintainer-
chosen posture). +3 suite cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5TUSJPD8FCtJagf7siXgt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant