Skip to main content

Online edition for BLOGE 0.9.8-RC1 · facts verified 2026-09-15 · 中文

Chapter 9 — Tooling Workflow

Promise: By the end of this chapter you will understand how the BLOGE tooling stack — from its TypeScript authoring path and Java execution path to the editor, the visual studio, and the CLI linter — works together, and you will know how to set up a productive authoring loop for .bloge files that catches errors before code review and production.


Learning Goals

  1. Map the two implementation paths: TypeScript bloge-lang for LSP/Studio authoring, and Java bloge-dsl plus bloge-lint for runtime/CI.
  2. See how diagnostics flow from the lexer, parser, compiler, and lint rules through the LSP to red squiggles in your editor.
  3. Use format-on-save to keep .bloge files consistent via the DSL code generator.
  4. Run the bloge-lint CLI in CI and understand why its Java rule set overlaps with, but is not identical to, editor diagnostics.
  5. Trace operator-metadata.json from bloge-maven-plugin into BLOGE Studio's operator palette, then use split view to round-trip between DSL text and the canvas.

Prerequisites

Source Examples

FileWhat it shows
bloge-lang/README.mdTypeScript authoring core: lexer, parser, compiler, codegen, schema
bloge-lsp/README.mdLSP server capabilities and architecture
bloge-lsp/src/server.tsLSP server entry — capability registration
bloge-lsp/src/document-manager.tsFive-stage analysis pipeline (lex → parse → compile → lint → index)
bloge-lsp/src/features/hover.tsHover docs for keywords, built-in functions, nodes, schemas, and durations
bloge-lsp/src/features/completion.tsContext-aware auto-completion
bloge-lsp/src/features/formatting.tsWhole-document formatting via DslCodeGenerator
bloge-lsp/src/lint/lint-rule.tsLintRule interface and LintRunner
bloge-vscode/src/extension.tsVS Code extension entry — LSP client wiring
bloge-vscode/syntaxes/bloge.tmLanguage.jsonTextMate grammar for syntax highlighting
bloge-intellij/README.mdIntelliJ plugin — dual-mode architecture (PSI + optional LSP overlay)
bloge-studio/README.mdVisual DAG editor — React 19 + React Flow
bloge-studio/public/operator-metadata.jsonBuilt-in operator catalog (name, description, I/O schema)
bloge-maven-plugin/README.mdMaven plugin that generates operator-metadata.json from @BlogeOperator classes
bloge-lint/README.mdJava CLI linter — rules, .blogerc.json config, exit codes
ticket-routing.blogeExample used throughout this chapter
missing-timeout.blogeAntipattern caught by lint

Why This Matters

You can write syntactically correct .bloge files in a plain text editor. But without tooling:

  • Typos in node IDs are discovered only at graph-engine start time.
  • Unused nodes and missing timeouts slip past review unnoticed.
  • Formatting drift makes PRs noisy and obscures real changes.
  • Operator schema mismatches between the Java code and the DSL are invisible until a runtime ClassCastException.

The BLOGE tooling chain is designed so that the fastest feedback loop is also the cheapest: red squiggles appear as you type, formatting normalises on save, and the CLI linter runs in CI as a final gate. These surfaces do not share one implementation: TypeScript powers the LSP and Studio, while Java powers runtime parsing and CLI lint. Conformance fixtures and exported contracts keep the paths aligned.


Mental Model

Think of the tooling stack as two feedback paths joined by explicit compatibility evidence:

Diagram: 09-tooling-workflow figure 1

The critical insight: shared contracts are the alignment boundary, not shared code. bloge-lang supplies the TypeScript parser/compiler used by bloge-lsp and Studio. Runtime and Maven flows use Java bloge-dsl; bloge-lint adds Java CI rules; IntelliJ Community keeps a PSI/JFlex path and can add an LSP overlay where supported. Because implementations can drift, compatibility must be checked rather than assumed.


First Working Example

Open the ticket-routing.bloge example in VS Code with the bloge-vscode extension installed. You should see immediately:

  1. Syntax highlighting — keywords like graph, node, branch, depends_on are coloured; doc comments (///) get documentation-comment scopes; operator references like FetchCustomerOperator are highlighted as types.

  2. Diagnostics — if you deliberately misspell a depends_on target (say, change fetchCustomer to fetchCustmer), a red underline appears before you save.

  3. Hover — hover over timeout and you see: "Sets the maximum execution time for a node." Hover over 3s and you see: "Duration: 3s = 3000ms". Hover over analyzeSentiment and you see its operator ref, doc comment, dependencies, and timeout.

  4. Completion — inside an input { } block, type = and the LSP suggests ctx, all node IDs, and built-in functions like concat, size, and coalesce.

  5. Formatting — trigger "Format Document" and the entire file is re-emitted through DslCodeGenerator.generate(ast), normalising indentation and spacing.

All five features are powered by a single pipeline defined in document-manager.ts:

// Phase 1: Lexing → tokens
// Phase 2: Parsing → AST (GraphDef)
// Phase 3: Compile → CompiledGraph + diagnostics
// Phase 4: Lint → LintRunner.run(ast, compiled)
// Phase 5: Index → buildSymbolIndex(ast, uri, text)

This five-stage pipeline runs on every keystroke (via documents.onDidChangeContent in server.ts), and the cached ParseState is reused by hover, completion, definition, references, and rename handlers. After those five phases, the DocumentManager also refreshes import-graph bookkeeping so cross-file features stay in sync.


Follow One Pull Request Through the Feedback Loop

A toolchain earns its place when it shortens the distance from an edit to a reviewable fact. Consider a pull request that removes timeout from an external call in missing-timeout.bloge. The useful story is not a tour of editor features; it is one defect travelling through four feedback surfaces.

Diagram: edit, diagnose, lint, test, review loop

One defect, one stable identity

SurfaceReader seesRequired hand-off
EditorInline diagnostic at the noderuleId=missing-timeout and source location
CLI lintReproducible non-zero resultSame rule id and severity as CI
Scenario testRuntime behaviour for a representative caseCase verdict plus node/effect evidence
Pull-request reviewWhy the edit is safeDiagnostic cleared and focused test result

Autocomplete can help write syntax, but it cannot close this loop. The shared contract is the stable diagnostic identity plus a command that another person can rerun.

Diagnose before testing everything

The shortest useful sequence is:

EDIT one declaration
→ DIAGNOSE at the source location
→ LINT with the same rule set as CI
→ TEST the smallest Scenario that exercises the changed path
→ ATTACH both results to review

If lint still fails, return to the declaration; a full integration suite adds noise. If lint passes but the Scenario fails, inspect runtime evidence; changing the lint configuration cannot repair business behaviour. Only after both focused signals pass should the broader build run.

This keeps claims bounded: lint proves a declared authoring rule, the Scenario proves behaviour under its fixtures, and neither alone proves production correctness.


Break It Apart

1 — The shared language core: bloge-lang

bloge-lang is a pure TypeScript library with zero dependencies. It exports:

  • Lexer — hand-written scanner producing tokens from .bloge source.
  • Parser — recursive descent with precedence-climbing expression parsing. Handles imports, graph-level input/output schemas, signal_schema, oneOf(...), union types, ?. safe navigation, and more.
  • compile(ast, options?) — frontend semantic analysis: dependency resolution, cycle detection, schema validation, union narrowing, branch exhaustiveness checking. When a CompileOptions.importResolver is provided, imports are resolved recursively.
  • DslCodeGenerator.generate(ast) — AST-to-source formatter. This is the same code path used by both the LSP formatting feature and Studio's export.

Usage in three lines:

import { Lexer, Parser, compile, DslCodeGenerator } from 'bloge-lang';

const tokens = new Lexer(source).tokenize();
const ast = new Parser(tokens).parse();
const result = compile(ast);
// result.diagnostics → errors, warnings
// DslCodeGenerator.generate(ast) → formatted source

2 — The LSP: bloge-lsp

bloge-lsp wraps bloge-lang in a Language Server Protocol server that communicates over stdio. The server.ts entry point registers these capabilities:

CapabilityHandler fileWhat it does
Diagnosticsdocument-manager.tsReal-time error/warning/info from lexer + parser + compiler + lint
Completionfeatures/completion.tsContext-aware: keywords, node IDs, schema types, built-in functions, oneOf(...) and snippet templates
Hoverfeatures/hover.tsDocs for keywords, ~70 built-in functions, node/schema/transform declarations, durations
Go to Definitionfeatures/definition.tsSymbol-index-backed jump to declaration's nameRange; cross-file via resolveSymbolCrossFile
Find Referencesfeatures/references.tsAll usages of a node/schema/transform, including transitive importers
Renamefeatures/rename.tsRenames declaration + all usages; propagates to transitive importers; conflict-checked
Formattingfeatures/formatting.tsWhole-document via DslCodeGenerator.generate(ast)
Code Actionsfeatures/code-actions.tsQuick fixes for missing-timeout, missing-doc-comment, unused-node, no-duplicate-node-id; plus a Source Fix All action for safe bulk fixes
Semantic Tokensfeatures/semantic-tokens.tsFull-document semantic highlighting: graph names, operator refs, node refs, path segments, literals, keywords — refines TextMate scopes with AST-aware token classes

Import resolution is automatic for file:// documents — the DocumentManager wires a file-backed ImportResolver that resolves paths relative to the open file and appends .bloge when needed. Errors in imported files surface as diagnostics in the importing document.

3 — Lint rules

BLOGE lint is tri-layered. Knowing which layer a rule lives in tells you where it runs, what context it can see, and which artifact you must rebuild to add a new rule.

LayerWhere it runsWhat it seesExamples
Core (15)bloge-lint CLI + LSP (translated to TS)A single .bloge file's AST after parse + compileno-duplicate-node-id, no-duplicate-schema-name, unused-node, missing-timeout, excessive-fan-out, missing-doc-comment, no-cycle, no-unresolved-dependency, no-unresolved-branch-target, max-script-nodes, script-timeout-required, script-line-limit, loop-exit-completeness, cron-expression-valid, deadline-in-past
Compiler-driven (3)Inside the compiler itselfWhole-graph IR after symbol resolution and type checkingschema-validation-strictness-mismatch, dynamic-subgraph-output-untyped, ambiguous-timer
Extension (2 today, more via SPI)Discovered when the matching *-ext JAR is on the classpathThe extension's own DSL surfacesession-backward-transition-guard (from bloge-session-ext), state-machine-structure (from bloge-state-ext)

Core rules — LSP subset

The LSP bundles the six interactive rules (bloge-lsp/src/lint/rules/):

Rule IDSeverityWhat it catches
no-duplicate-node-idErrorNode ID declared more than once
no-duplicate-schema-nameErrorSchema name declared more than once
unused-nodeInfoNode never referenced downstream
missing-timeoutInfoNode without a timeout
excessive-fan-outInfoNode with >5 outgoing edges
missing-doc-commentInfoNode missing /// documentation

The Java-side CLI linter (bloge-lint) includes these same core rules for batch checks and adds the graph-level and script-focused rules listed above, plus the rules contributed by extensions via the LintExtensionContributor SPI.

Compiler-driven rules

These three rules are emitted by the compiler, not the rule runner. You cannot disable them in .blogerc.json — they reflect structural checks the compiler must run to produce a valid Graph. They surface as ordinary diagnostics in the LSP and CLI just like core rules.

Extension rules

Adding bloge-session-ext or bloge-state-ext to your project also adds that extension's lint contributions. You can register your own rules via LintRuleProvider (Java SPI) for the CLI and BLOGE_LSP_EXTRA_RULES for the LSP. Severity levels and rule parameters are configurable through .blogerc.json:

{
"rules": {
"missing-timeout": "warning",
"missing-doc-comment": "off"
}
}

Decision-table rules

The bloge LSP and CLI both ship two lint rules specifically for decision_table nodes. Use these recipes to keep tables exhaustive and intentional.

Rule IDSeverityTriggers when
decision-table/missing-otherwiseWARNINGA first, unique, or any table has no otherwise clause
decision-table/collect-otherwiseINFOA collect table has an otherwise clause (always appends — may be unintentional)

Recipe 1 — Silence decision-table/missing-otherwise by adding otherwise:

// [WARN] decision-table/missing-otherwise
decision_table credit_tier(score = ctx.score) hit=first -> String {
rule (score: score >= 750) -> "platinum"
rule (score: 680 <= score < 750) -> "gold"
// no otherwise — what happens if score < 680?
}

// ✅ Fixed
decision_table credit_tier(score = ctx.score) hit=first -> String {
rule (score: score >= 750) -> "platinum"
rule (score: 680 <= score < 750) -> "gold"
otherwise -> "rejected"
}

Recipe 2 — Acknowledge decision-table/collect-otherwise if it is intentional:

// [INFO] decision-table/collect-otherwise
// "baseline" is ALWAYS appended, even when other rules also matched.
decision_table discounts(score = ctx.score) hit=collect -> String {
rule (score: score >= 700) -> "loyalty"
otherwise -> "baseline" // unconditional — is this intentional?
}

// ✅ If intentional, suppress with a .blogerc.json override:
// { "rules": { "decision-table/collect-otherwise": "off" } }
// If not intentional, remove the otherwise clause.

For a full list of compiler and runtime error codes, see Appendix F — Decision Table.

4 — Editor extensions

VS Code (bloge-vscode):

  • TextMate grammar at syntaxes/bloge.tmLanguage.json provides syntax highlighting scopes for keywords, doc comments, strings, operators, types, and built-in functions.
  • Language configuration at language-configuration.json provides bracket matching, auto-closing pairs, comment toggling (//, /* */), and block folding.
  • The extension entry (extension.ts) launches bloge-lsp as a child process over stdio and wires it as the LSP client for the bloge language ID.

IntelliJ IDEA (bloge-intellij):

  • Community edition — PSI-only mode: hand-written JFlex lexer, lightweight PsiBuilder parser, ExternalAnnotator delegates to bloge-dsl Parser and bloge-lint LintRunner, structure view, code folding, semantic highlighting. No external process required.
  • Ultimate / commercial editions — optional LSP overlay: BlogeLspServerSupportProvider launches the bundled bloge-lsp Node server; overlapping PSI providers (diagnostics, completion, formatting) stand down to avoid duplicate results. Requires Node.js on PATH or BLOGE_NODE_PATH.

5 — BLOGE Studio

BLOGE Studio is a browser-based visual DAG editor built with React 19 + React Flow 12. It depends on bloge-lang as a local file: dependency and reuses the same parser, compiler, and code generator.

Key features relevant to your authoring workflow:

  • Three view modes: Visual (canvas only), Code (Monaco editor only), Split (canvas + editor side by side). The Zustand store synchronises between them.
  • Operator Palette: loaded from public/operator-metadata.json. Operators are tagged by layer (infra / capability / domain) and searchable. Drag to canvas to create a node.
  • Diagnostics Panel: DiagnosticsPanel.tsx — displays errors, warnings, and info from the compiler and lint rules. Click a diagnostic to select the offending node on the canvas.
  • Undo/Redo: DSL-snapshot-based history (up to 50 steps).
  • Export: validates → runs code generator → downloads .bloge file.

6 — The operator metadata pipeline

The link between your Java operator code and the visual tooling is bloge-maven-plugin:

Diagram: 09-tooling-workflow figure 2

The plugin scans @BlogeOperator-annotated classes at process-classes phase, reflects Operator<I,O> generic parameters, calls SchemaIntrospector.introspect() for each type (with Record and POJO support), and writes a JSON file. Studio loads this file to populate the operator palette and provide I/O schema information for field completion.


The Embedded Ops Console — /bloge-console

Studio is great for authoring graphs. Production needs a different view: "which executions are running right now, which timer fires next, which operator is throwing." That view is /bloge-console — a small, read-only operator UI that ships with bloge-spring-web and mounts at the configured base path.

spring:
bloge:
console:
enabled: true
base-path: /bloge-console # default
require-role: BLOGE_OPS # Spring Security role gate

What it shows:

  • Executions — running, waiting, completed, failed; click into the audit journal for a single run.
  • Timers — currently armed durable timers with their next fire time; manual fire now (gated by require-role).
  • Leases — current owners with TTL remaining.
  • Schema — Flyway currentVersion, pending/applied migrations, and UP_TO_DATE, PENDING, or FAILED status.
  • Operator registry — every registered operator, its module, and its @BlogeOperator metadata for quick discovery.

The console is observational by design. It does not let you edit graphs (use Studio) or rewrite checkpoints (use a migration). The "fire timer" and "release lease" actions are explicit operator-recovery tools and every action is recorded in the audit journal under the calling principal.

Brain check: when should you reach for /bloge-console vs Studio? Hint: Studio writes the graph; the console reads the runtime.


Common Trap

❌ Assuming the editor and CI check the same rules

The LSP lint rules (TypeScript, in bloge-lsp) and the CLI lint rules (Java, in bloge-lint) are two separate implementations with overlapping but not identical rule sets. The LSP has unused-node and excessive-fan-out; the CLI has no-unresolved-dependency, no-unresolved-branch-target, and no-cycle. If you rely only on green squiggles in your editor, CI may still fail on rules the LSP does not check.

# Run the Java linter locally before pushing:
java -jar bloge-lint.jar check src/main/resources/bloge/

# Or configure it in your build:
# .blogerc.json controls severity overrides for both editor and CLI usage.

Best practice: run bloge-lint check as a pre-commit hook or CI step so that the full rule set is applied before merge.


Guided Rewrite

Take the missing-timeout.bloge antipattern file and use the tooling to fix it:

  1. Open it in VS Code with the bloge-vscode extension. You should see an info diagnostic on loadReport from the missing-timeout lint rule.

  2. Hover over loadReport to confirm there is no timeout listed in the hover popup.

  3. Add a timeout:

    node loadReport : LoadReportOperator {
    input {
    reportId = ctx.reportId
    }
    timeout = 5s
    }
  4. Format the document (Shift+Alt+F in VS Code). The DslCodeGenerator normalises the indentation.

  5. Add a /// doc comment above loadReport. Confirm the missing-doc-comment diagnostic disappears.

  6. Run the CLI linter against the file:

    java -jar bloge-lint.jar check src/main/resources/bloge/antipatterns/missing-timeout.bloge

    With the timeout and doc comment added, the output should report zero errors.

  7. Open the same file in BLOGE Studio (npm run dev in bloge-studio/, then use "Open" in the toolbar). Switch to Split view and observe the canvas and DSL code in sync. Check the Diagnostics panel at the bottom — it should be empty.


Brain Check

  1. Which module does the LSP's formatting feature actually delegate to? (The DslCodeGenerator from bloge-lang. See formatting.ts — it calls DslCodeGenerator.generate(state.ast).)

  2. How does the LSP detect that a depends_on target is misspelled? (compile(ast) in bloge-lang runs dependency resolution. An unresolvable dependency produces an error-level diagnostic, which the DocumentManager converts to an LSP DiagnosticSeverity.Error.)

  3. What happens in the IntelliJ plugin when the IDE is Community edition and Node.js is not installed? (The plugin operates in PSI-only mode — all features are provided by the built-in JFlex lexer, PsiBuilder parser, and ExternalAnnotator. The LSP overlay never activates because com.intellij.modules.lsp is absent.)

  4. How does BLOGE Studio know which operators to show in its palette? (It loads public/operator-metadata.json on startup. This file can be generated by bloge-maven-plugin's export-metadata goal, or hand-authored.)

  5. Can you disable a lint rule in the CLI? How? (Yes — create a .blogerc.json file with a rules map. Set the rule ID to "off". For example: { "rules": { "missing-doc-comment": "off" } }. See LintConfig.java.)


Lab

  1. Editor setup: Install the bloge-vscode extension (or run the IntelliJ plugin sandbox with ./gradlew runIde in bloge-intellij/). Open ticket-routing.bloge. Verify that:

    • Syntax highlighting covers all keywords and doc comments.
    • Hovering over analyzeSentiment shows its operator, description, dependencies, and timeout.
    • Ctrl+Click on fetchCustomer in the depends_on list jumps to the node declaration.
    • Renaming classifyPriority updates the branch condition and all depends_on references.
  2. Break a graph on purpose: In ticket-routing.bloge, change depends_on = [analyzeSentiment] on classifyPriority to depends_on = [nonExistentNode]. Observe the diagnostics that appear. Then run bloge-lint check on the file and compare the CLI output to the editor squiggles.

  3. Operator metadata round-trip: If you have a Maven project with @BlogeOperator classes, run mvn bloge:export-metadata and copy the resulting operator-metadata.json into bloge-studio/public/. Start Studio and confirm your operators appear in the palette with correct I/O schemas.

  4. Studio split-view: Open ticket-routing.bloge in BLOGE Studio. Switch to Split view. Add a new node in the code editor and watch it appear on the canvas. Delete it and use Undo (Ctrl+Z) to bring it back.


Bridge: from tooling to a verification gate

Editor diagnostics and bloge-lint shorten authoring feedback; they do not execute an approved business Scenario. In a normal customer module, enter the 0.9.8-RC1 verification path by compiling the bootstrap and running the Preview goal against .blogeverify.yaml in one Maven invocation:

mvn test-compile \
com.leanowtech.bloge:bloge-maven-plugin:0.9.8-RC1:verify

The generated artifact still needs source binding and a receipt retained outside its mutable directory. In RC1, this book's loan starter fails closed because its com.leanowtech.bloge.starter.* bootstrap hits the plugin classloader boundary; run its JUnit Scenario tests instead, and do not interpret TEST_CLASSPATH_UNAVAILABLE as a business failure. Chapter 32 — Change Attribution, Maven, and Release Decisions explains source installation, the known limitation, the command ladder, and the claim boundary.


Experiment acceptance card

  • Expected and observed: A missing-timeout defect crosses edit, diagnose, lint, Scenario, and review as one loop.
  • Failure and recovery: Remove timeout to fail the gate; restore it and rerun the smallest check set.
  • Proof boundary: Proves the toolchain blocks a declared defect, not an unencoded rule.
  • Exercise contract: One PR; change one defect; deliver diagnostic, test, and review receipts; stop when all point to one rule.

Recap

  • bloge-lang is the TypeScript authoring core used by the LSP and Studio; it is not the Java runtime parser.
  • bloge-dsl is the Java runtime/Maven parsing path; conformance fixtures and exported contracts are the compatibility boundary between the two implementations.
  • bloge-lsp wraps bloge-lang in an LSP server that provides diagnostics, completion, hover, go-to-definition, find references, rename, and formatting over stdio.
  • bloge-vscode provides TextMate syntax highlighting and language configuration, then delegates all intelligent features to bloge-lsp.
  • bloge-intellij works in PSI-only mode on Community edition and adds an optional LSP overlay on Ultimate/commercial editions.
  • BLOGE Studio is a browser-based visual editor with a three-way view (visual / code / split), a diagnostics panel, and an operator palette driven by operator-metadata.json.
  • bloge-maven-plugin bridges Java operator code to the visual tooling by generating operator-metadata.json from @BlogeOperator annotations.
  • bloge-lint (Java CLI) runs the same style of lint rules in CI; its rule severity is configurable via .blogerc.json.
  • The LSP and CLI lint rule sets overlap but are not identical — always run the CLI linter in CI as a final gate.

Next Step

In Chapter 10 — Reuse with Subgraphs, you will use this feedback loop while extracting reusable graph behavior behind an explicit parent/child contract.


Coding Agent: Open the versioned task guide.