Behavioral Knowledge IR · open draft

Your code's observable behavior, turned into data.

BKIR is a language- and transport-neutral intermediate representation of what a feature or business rule does —its contract, its flow, its effects and all of its outputs— so you can regenerate it in another language or framework while preserving what is observed from the outside.

What it is

A black box that can actually be regenerated

BKIR captures features extracted from source code (EJB/RMI, REST, SOAP, gRPC, RPC…) or designed from scratch. It does not model the internal architecture: it models the observable boundary. The unit is the Unit (one .bkir file), and Units reference and orchestrate one another.

Two entry points

Design: define applications (ideally from a graphical tool) → BKIR → multiple implementations. Extraction: read code → BKIR; what is repetitive may be promoted to a reusable Unit.

The IR is the source of truth

Generated code is disposable and never hand-edited. What is not captured is lost: stable identity, deterministic and diffable generation, files in git.

Verification by equivalence

From the contract + the outputs + the guards, tests (golden / property) are derived and run against the original and the regenerated code to prove they produce the same.

Why BKIR?

Why an IR instead of translating directly to the target framework?

The fair question: if you want to go from EJB to Spring Boot, why not translate the code directly? Because direct translation transcribes the mechanism (the layers, the exception classes, the source idioms) and produces "Java written in Go". The IR captures only the observable behavior, and that unlocks advantages direct translation cannot offer.

Direct translation — one translator per pair: EJB→Spring · EJB→Quarkus · EJB→Go · Spring→Go · … → N × M translators.

With BKIR — the IR in the middle: [EJB, Spring, …] → BKIR → [Spring, Quarkus, Go, …] → N extractors + M generators. Adding a language is +1, not ×N.

Idiomatic code, not transcribed

Direct translation drags along the source's accidental architecture. BKIR preserves the contract, flow, effects and outputs: the generator produces code that is natural to the target, not a transliteration.

A verifiable source of truth

From direct translation you can only check "it compiles". From BKIR —contract + outputs + guards— equivalence tests are derived and run against the original and the regenerated code. The IR is what makes it possible to trust the result.

Reviewable and durable

You review the behavior —a readable model, with stable ids and provenance— not tens of thousands of generated lines. Diffable, versionable and a basis for a graphical tool.

It concentrates the scattered contract

The output catalog, the transaction boundaries, the security requirement, the error codes… in the original they live spread across annotations, config and handlers. A direct translator would have to rediscover all of that for each target.

It decouples the "what" from the "where"

Spring Boot 1.5 → 6, EJB → Spring, on-prem → cloud: behavior is invariant; only the binding changes. BKIR isolates the logic from the transport, so a framework change is regenerating, not rewriting.

Honest gaps, not silent errors

What is not formally captured is marked explicitly (foreign, reviewed: false). Direct translation carries over what it recognizes and silently breaks the rest.

When is it NOT worth it? For a one-off migration, to a single target, that you won't maintain or need to verify, translating directly (or with an ad-hoc LLM) is faster and enough. BKIR pays off when there are several targets, you need to trust the result, you will regenerate over time (framework upgrades) or you want a durable model of the system, independent of any framework. That is why the mode is "the IR is the source of truth".

Guiding principle

Collapse the internal mechanism, expand the observable variation.

Two internal paths that produce the same output are the same output. An exception is modeled only if it produces a distinct output at the boundary; its Java class or its mechanism is irrelevant.

What is in scope: the contract, the flow, the effects (writes, mutations, events) and their atomicity, because partial state is observed in later invocations. Decomposing into Units is allowed, but as semantic reuse, not as a reflection of the code's layers or classes: the generator decides whether to inline or invoke each reference.

The model

Seven kinds of Unit

Each Unit declares a common envelope (bkirVersion, id, kind, name, uses, source, provenance) plus its kind-specific fields. The id carries a short, stable prefix.

VOC_ vocabulary

Domain terms + fact types (lightweight SBVR). Provides stable, readable names.

TYPE_ type

Reusable canonical type: Record, Enum, Union, alias with constraints. No Java types.

COND_ condition

Named business condition with a signature. Detection via predicate (FEEL) or attempt (when you try and it fails).

FN_ computation

Pure derivation: a FEEL expression, or a foreign block with a contract (for algorithms that aren't expressed declaratively).

IO_ interaction

Read or effect against the environment (DB, service, filesystem, channel). Declares target or channel and the attempt conditions it raises.

CHN_ channel

A referenceable messaging channel: payload + delivery (guarantees). Connects producer → channel → consumer in a navigable way.

OP_ operation

Orchestrator: input + flow + outputs. Exposed via bindings (synchronous) or trigger (channel-driven).

+ FEEL → reference

Expressions (conditions, computations, guards) are written in FEEL (BKIR profile: names = identifiers). Operators, reserved words and functions in the language reference.

How it fits

An operation at a glance

The flow is a graph of nodes that matter to behavior; each one is inline or a reference to another Unit. Observable outputs live in a canonicalized catalog.

OP_RETIRAR_AHORROS.bkir
kind: operation
name: Savings account withdrawal
input:
  cuentaId: { type: ID }
  monto:    { type: Money }
flow:
  - decision: COND_MONTO_NO_POSITIVO          # if (monto <= 0) throw ...
    whenTrue: { to: OUT_MONTO_NO_POSITIVO }
  - read: { bind: cuenta, use: IO_CUENTA_AHORRO_GET, args: { cuentaId: cuentaId } }
    onCondition: { COND_CUENTA_NO_EXISTE: { to: OUT_CUENTA_NO_EXISTE } }
  - decision: COND_SALDO_INSUFICIENTE
    whenTrue: { to: OUT_SALDO_INSUFICIENTE }
  - effect:
      transactional: true                     # explicit atomic boundary
      steps:
        - bind: transaccionId
          use: IO_RETIRO_REGISTRAR
          args: { cuentaId: cuentaId, monto: monto }
  - outcome: OUT_RETIRO_OK
outputs:
  OUT_RETIRO_OK:        { result: success, payload: { type: Record, fields: { transaccionId: { type: ID } } } }
  OUT_SALDO_INSUFICIENTE: { result: error, code: "error.msg.savingsaccount.insufficient.balance",
                            message: "Insufficient account balance.", guard: COND_SALDO_INSUFICIENTE }
  # … rest of the output catalog

Catalog: each Java construct → BKIR →