Online edition for BLOGE
0.9.8-RC1· facts verified 2026-09-15 · 中文
Chapter 8 — Turning a Personal DSL Draft into a Team Asset
Promise: You will follow an order graph through five small commits and see how a draft that merely compiles gains contracts, useful diagnostics, intent, and a reuse boundary—until a teammate can change it safely.
Learning goals
By the end of this chapter, you can:
- Explain why zero compiler errors does not mean a DSL file is maintainable.
- Turn hidden assumptions into a team contract with graph input, field types, and stable output.
- Use
invalid-expression-pathto locate the path and schema that disagree. - Decide whether an explanation belongs in a
///doc comment or in code-review discussion. - Put shared behavior behind an import alias instead of copying nodes.
This chapter adds one variable
The first seven chapters gave the order flow enough behavior to load a customer, read products, calculate a price, and decide whether to create an order. This chapter adds no new business behavior. It adds one team reality: someone else must safely change this graph next week.
That changes the definition of done. A personal draft only needs its author to remember where ctx.customerEmail came from. A team asset must let the compiler, editor, and reviewer trace that assumption.
Opening problem: why a green draft can worry a reviewer
The first order-summary draft is only a few lines:
graph orderDraft {
transform orderSummary {
userId = ctx.userId
customerEmail = ctx.customerEmail
}
}
Predict before reading on. If compilation reports errors=0 warnings=0, which claims follow?
| Claim | Proven by zero diagnostics? |
|---|---|
| The current compiler accepts the syntax | Yes |
Every caller supplies customerEmail | No |
| The field may be absent | No |
| A downstream consumer knows the output shape | No |
| A rename six months later will be caught | No |
Green means the compiler found no violation of a declared contract. When no contract exists, many mistakes are not yet checkable.
Run the whole trajectory first
The book includes five independent snapshots and an RC1 probe. Install core and DSL from the pinned BLOGE submodule, then run it:
cd submodule/bloge
mvn -pl bloge-core,bloge-dsl -am -DskipTests install
cd ../../probes/ch08-dsl
mvn test
The important output observed on 2026-09-14 at BLOGE commit cc38fbe5 is:
commit-1 graph=orderDraft errors=0 warnings=0 firstRule=none
commit-2 graph=orderContractDraft errors=0 warnings=1 firstRule=invalid-expression-path
commit-3 graph=orderTypedDraft errors=0 warnings=0 firstRule=none
commit-4 graph=orderReviewable errors=0 warnings=0 firstRule=none
commit-5 graph=orderTeamAsset errors=0 warnings=0 firstRule=none
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Commit 2 is not worse than commit 1. It turns an invisible assumption into a contract, so the compiler can finally point at the problem.
Commit 1: expose the business path
The first draft projects only userId and an email address. It is useful for a personal experiment: “Can this data flow be expressed?”
It is not ready for a team mainline because ctx is opaque. The compiler cannot distinguish “the caller really supplies a top-level customerEmail” from “the author misremembered customer.email.”
The review result is therefore not “failed.” It is:
syntax accepted
input contract = opaque
safe refactor boundary = unknown
Commit 2: declare the input and invite the error out
The second commit declares the actual order-request shape:
input: OrderRequest
schema OrderRequest {
userId: String
customer: {
name: String
email: String?
}
}
The summary still reads ctx.customerEmail. The compiler now knows that the top level contains only userId and customer, so it reports:
warnings=1
firstRule=invalid-expression-path
Path 'ctx.customerEmail' — field 'customerEmail' not found in graph input schema
That warning gives three coordinates: the root is graph input (ctx), the failing path is customerEmail, and the comparison contract is OrderRequest.
The recovery is not to disable schema validation. Decide whether reality contains a top-level email or a nested customer email, then align the schema and expression.
Commit 3: fix the path and state optionality
In this example, reality says customer.email, and some customers have no email:
transform customer {
name: String = ctx.customer.name
email: String? = ctx.customer.email
}
transform orderSummary {
userId: String = ctx.userId
customerEmail: String? = customer.output.email
}
Compilation returns to zero warnings. Two different relationships are now explicit:
- A path relationship:
orderSummarydepends oncustomer.output.email. - A type relationship:
String?says that absence is allowed by contract.
Do not make every field optional merely to silence diagnostics. Optionality is a business fact. If order notification requires an email, keep String and fail early when it is missing.
Commit 4: tell the reviewer why the DSL exists
Correct paths still do not explain intent. The fourth commit adds a graph output contract and a responsibility statement:
output: OrderGraphResult
schema OrderGraphResult {
orderSummary: OrderSummary
}
/// Projects boundary fields; it does not make a business decision.
transform orderSummary { ... }
A /// comment enters the AST and attaches to the element that follows, so editors and documentation tools can read it. A regular // comment helps source readers but is not contract metadata.
| Comment | Review value |
|---|---|
/// Creates order summary | Low: the name already says it |
/// Projects boundary fields; makes no business decision | High: it defines responsibility |
/// This is important | Low: nothing can be checked |
Good documentation states responsibility and boundary. It does not narrate the syntax.
Commit 5: reuse behavior through an explicit alias
The order graph now needs payment. Copying payment nodes looks quick, but it creates multiple sources of truth for retries, amount checks, and status mapping. The fifth commit extracts a graph and names the dependency:
import "./payment-flow" as paymentFlow
node payment : paymentFlow {
input {
orderId = ctx.orderId
amount = ctx.amount
}
}
An alias is not cosmetic shorthand. It is this file's stable name for an imported capability. The compiler must resolve the relative path, compile the imported graph, validate current bindings against its input schema, and expose its output schema downstream.
If the file is missing, imports form a cycle, or two imports use one alias, compilation should stop before a graph is created. Runtime must not guess what an alias means.
Mechanism: stronger declarations create better feedback
The five commits exercise a feedback pipeline:
source
→ lexer/parser: is the syntax legal?
→ import resolver: which graph is referenced?
→ schema resolver: what do fields and types mean?
→ path/boundary validator: are connections compatible?
→ compiled graph: the runtime model
Later conclusions depend on earlier layers. Valid syntax cannot replace import resolution; a resolvable import cannot replace schema compatibility; schema compatibility cannot prove business correctness.
That is the core value of a team DSL: assumptions that once lived in an author's head become feedback that machines and people can inspect.
Failure experiment: a bad alias is not a retry problem
Keep two imported files unchanged but give both as paymentFlow. RC1 produces the stable diagnostic duplicate-import-alias. Recovery means giving distinct capabilities distinct names, or removing the duplicated source of truth. Retrying runtime execution is meaningless because the graph was never built.
Likewise, invalid-expression-path calls for fixing the contract or path, not adding a node retry. Compile-time feedback and runtime resilience operate in different worlds.
Deliberately moved out of the main story
The complete schema type catalog, derived aliases for bare imports, the import resolver SPI, schema evolution, and ValidatedSchema policy belong in the reference layer. They would interrupt the five-commit story here.
In particular, ValidatedSchema is a Java runtime wrapper that attaches STRICT, COERCE, or LENIENT policy to a schema; it is not new .bloge syntax required in this chapter. The compiler unwraps its expected schema for structural checks. Runtime policy details belong in the DSL engineering reference.
Real-world transfer: from a personal recipe to a restaurant card
A home cook may understand “salt to taste.” Restaurant handoff needs ingredient specification, allergens, output form, reasons for critical steps, and a shared sauce version.
| Restaurant handoff | Team DSL |
|---|---|
| Ingredient list | graph input schema |
| Finished-dish specification | graph/output schema |
| Station instructions | nodes/transforms and dependencies |
| Why a step exists | /// responsibility comment |
| Shared sauce recipe | imported graph plus alias |
| Test-cook feedback | compiler/linter diagnostics |
The goal is not a longer recipe. It is safe change by someone who knows when they departed from the standard.
Your turn: make five reviewable commits
Choose one .bloge file you maintain. Make one kind of change per commit:
- Save the current parseable draft and list its undeclared input assumptions.
- Declare graph input and let the compiler expose path problems; do not fix them yet.
- Fix only paths and optionality, then record the diagnostic change.
- Add a stable output and responsibility boundary; do not paraphrase syntax.
- Extract one genuinely shared subflow and connect it through an explicit alias.
Produce one review card: which assumption changed, which diagnostic appeared, and who can review more safely as a result. Stop and split the work if one commit changes a business branch, types, and imports together; otherwise you cannot attribute the feedback.
What this chapter proved—and did not prove
- Zero diagnostics cover declared contracts only; opaque input can create false calm.
- Schema makes bad paths, missing fields, and optionality visible to the feedback loop.
///records responsibility and boundary; it does not replace good naming and structure.- Import aliases make shared graphs explicit dependencies rather than copied nodes.
- Syntax, references, structural contracts, and business correctness are successive claims. None substitutes for the next.
This probe proves that the five snapshots produce the recorded RC1 compiler feedback. It does not prove that an imported payment service is available, that the business price is correct, or that a production deployment is safe.
The next chapter adds one variable: Chapter 9 — Tooling Workflow turns the maintainable DSL into a repeatable editor, lint, and review loop before Chapter 10 introduces subgraph boundaries.
Experiment acceptance card
- Expected and observed: Five commits expose a bad path, repair the type, and consolidate an alias, ending at 0/0 diagnostics.
- Failure and recovery: Introduce a duplicate alias or bad path; repair one declaration and rerun the parser.
- Proof boundary: Proves localizable DSL feedback, not a correct business answer.
- Exercise contract: Team DSL; make one change per commit; deliver five diagnostics; stop at 0 errors/0 warnings with each transition explained.
Exact fact entry points
DslFeedbackLoopProbeTest.java— executable five-commit feedback.01-personal-draft.blogethrough05-team-asset.bloge— complete snapshots.order-process-v6.bloge— the pinned remote examples order graph.DslCompiler.java— compilation and diagnostic source.FileSystemGraphResolver.java— relative import resolution.ValidatedSchema.java— runtime validation-policy wrapper.
Coding Agent: Open the versioned task guide.