bus — dispatcher and `.bus` command-file execution
Overview
bus is the single entrypoint for BusDK. It dispatches user commands to independent module CLIs (for example bus journal ..., bus bank ...). Modules are responsible for their own data resources and domain validation.
This SDD adds direct support for executing .bus files:
bus 2024-01.busbus 2024-02.busbus 2024-03.bus-
./2024-01.buswhen the file has a#!/usr/bin/busor#!/usr/bin/env busshebang
A .bus file (Busfile) is a deterministic list of bus commands (one per line). Execution always performs a full syntax preflight of all provided busfiles before running any command. Workspace-level atomicity (all-or-nothing apply) is optional and configurable; Git-based atomicity is supported as one possible provider, but Git is not required.
Notes:
-
bus runis not special-cased here. Ifrunexists, it is treated as a normal dispatch target (a module/subcommand), and this SDD does not redefine its behavior. -
bus-replayis expected to export.busfiles once this format exists.
Motivation
Month-sized command files are a simple, reviewable, and (when used with Git) auditable way to build bookkeeping periods:
- easy to diff and code-review
- easy to replay in a clean workspace
- easy for agents to generate deterministically
- enables a strong safety property: detect syntax errors across the whole batch before anything runs
Workspace-level atomicity is optional and configurable because Git-based approaches can be slow with large datasets and some workspaces may not use Git at all.
Goals
- Support
.busfiles as first-class inputs to thebusdispatcher:-
bus <file>.busexecutes file(s). - Shebang execution works (
./file.bus) without extra flags.
-
- Mandatory syntax preflight across all provided busfiles before executing any command:
- tokenizer/quoting correctness
- non-empty command token
- optional dispatch target resolution
- Keep default behavior fast:
- default validation is syntax-only
- data validation is optional (best-effort/configurable)
- Standardize module check mode:
- BusDK modules are expected to implement
--checkfor non-mutating validation -
bus --check <file.bus>must fail fast when a referenced module does not support--check
- BusDK modules are expected to implement
- Make workspace-level atomicity configurable:
- Git is optional
- alternative providers are allowed
- provider selection can come from
datapackage.jsonand/orbus-preferences
- Keep implementation minimal:
- dispatcher and busfile code paths should remain as small and direct as possible
- avoid unnecessary abstraction layers, dependencies, and feature surface
Non-functional requirements
- Performance testing is mandatory and must be extensive for busfile workloads.
- The implementation must be performance-verified for large multi-file runs, long command batches, and preflight-heavy paths.
- Performance regressions in parsing, preflight, dispatch, and apply orchestration are treated as release blockers.
- Code must remain minimal and maintainable:
- prefer simple, explicit control flow
- keep moving parts small and deterministic
- add complexity only when justified by measured behavior
Non-goals
- Not a general scripting language (no loops, variables, conditionals, pipes).
- Not guaranteed rollback for external side effects (network calls, filings, emails).
- Not a replacement for module-level validation contracts (
--checkremains module-owned behavior).
Terminology
- Busfile: a
.buscommand file. - Syntax preflight: tokenization and command-line structural validation without executing module code.
- Data validation: executing module logic in a non-mutating mode (if supported).
- Workspace atomicity: all-or-nothing apply for workspace state (implementation-dependent and optional).
CLI behavior
Normal dispatch (unchanged)
-
bus <module> <args...>dispatches to module CLI. - No special handling is required for
run; it is dispatched like any other target.
New: busfile mode
If the first non-flag argument is recognized as a busfile, bus enters busfile mode.
Synopsis
bus [BUSFILE_OPTS] <file.bus> [<file2.bus> ...]
./file.bus
Busfile options
Busfile options are parsed only while in busfile mode:
-
--check- executes in check-only mode
- always performs syntax preflight
- if
validation.level=dataand supported, run best-effort data validation - MUST NOT apply workspace changes
-
--transaction <provider>- overrides configured transaction provider for this invocation
- allowed values:
none,fs,git,snapshot,copy
-
--scope <scope>- overrides how multiple busfiles are applied
-
file(default): each file is its own unit (syntax preflight remains global) -
batch: all files are one unit
-
--trace- prints each parsed command (
file:line) before executing it
- prints each parsed command (
Busfile recognition
A path is treated as a busfile when any of the following is true:
- It ends with
.bus. - The file is executable and begins with a shebang line referencing
bus:#!/usr/bin/bus#!/usr/bin/env bus
bus must not treat arbitrary readable files as busfiles unless they match those rules.
Busfile format
A busfile is UTF-8 text.
Lines
- Blank lines are ignored.
- Comment lines are ignored when first non-whitespace char is
#. - A trailing
\continues the command on the next physical line. - All other lines are treated as one
buscommand line.
Tokenization and quoting
Each command line is tokenized using shell-like quoting rules:
- whitespace separates tokens
- single quotes
'...'preserve literal content - double quotes
"..."preserve literal content (no interpolation) - backslash
\escapes the next character
Explicitly disallowed:
-
$VARexpansion -
$(...)command substitution - backticks
- pipes
|, redirections>,< -
;command separators
If a line cannot be tokenized (for example unterminated quote), it is a busfile syntax error and execution must stop before any command executes.
Command shape
First token is the dispatcher target, same as normal CLI:
journal add ...
bank import ...
vat report ...
Execution semantics
Phase 1: Syntax preflight (mandatory; global)
Before executing any command, bus must perform syntax preflight across all provided busfiles:
- read all files
- tokenize each executable line
- ensure first token exists
- optional recommended target-resolution check without running targets
If syntax preflight fails, bus must:
- print
file:line: <message> - exit non-zero
- make no workspace changes
Phase 2: Data validation (optional; best-effort)
Controlled by bus.busfile.validation.level:
-
syntax(default): no module-level validation before execution -
data(optional): best-effort validation pass before apply
Degrade gracefully if module lacks non-mutating validation:
- default: skip validation for that command
- strict mode: fail early if
bus.busfile.validation.strict=true
Required convention for BusDK modules:
- module supports
--check(or an explicitly documented compatibility alias) -
--checkperforms non-mutating validation and exits non-zero on clearly invalid input - when module-level check is unavailable,
bus --checktreats that command as unsupported and fails
Phase 3: Apply (execution)
After successful preflight (and optional data validation), execute commands in file order, respecting scope and transaction provider.
- fail-fast on first command error
- stdout/stderr pass through
- with
--trace, echofile:line: bus <...>before execution
Scope behavior for multiple busfiles
- scope
file(default): execute files sequentially; each file its own apply unit - scope
batch: all files are one apply unit
Transaction providers (optional)
Workspace atomicity is implemented by a transaction provider. Providers are optional and configurable. Default is none.
Provider none (default)
- no workspace-level atomicity
- direct execution in current workspace
- partial changes possible on later failure
Provider git (optional)
- atomicity via isolated Git branch/worktree and merge on success
- if workspace is not Git repo or Git unavailable, provider must fail clearly or fall back to
nonewhen configured - may be slow on large datasets
Provider snapshot (optional)
- atomicity via filesystem snapshot/rollback or equivalent platform mechanism
- environment-specific plug-in provider
Provider copy (optional)
- atomicity via temporary copy, execute there, apply on success
- may be slow on large datasets
Current implementation status
-
noneis fully implemented and default. - busfile dispatch is automatic per target:
- use in-process module runner when available
- otherwise use external
bus-<target>shell lookup when enabled
- shell lookup is configurable via
bus.busfile.dispatch.shell_lookup_enabled(defaulttrue) frombus-preferencesordatapackage.json. -
fsconfiguration is recognized and implemented for in-process transaction-capable module runners. - If any target requires external shell dispatch,
fsis treated as unavailable:busfalls back tononeonly iffallback_to_none=true; otherwise it exits with usage error.
Provider interface (internal)
Begin(scope) -> contextRunCommand(context, argv, env, cwd) -> exitCodeCommit(context)Rollback(context)Cleanup(context)
Configuration
Transaction and validation behavior must be configurable via:
bus-preferencesdatapackage.json
Precedence:
- CLI overrides (
--transaction,--scope,--check) - bus-preferences
- datapackage defaults
- built-in defaults
Proposed config keys
{
"bus": {
"busfile": {
"validation": {
"level": "syntax",
"strict": false
},
"transaction": {
"provider": "none",
"scope": "file",
"fallback_to_none": true
},
"dispatch": {
"shell_lookup_enabled": true
}
}
}
}
Error handling and exit codes
Error format
- syntax/tokenization error:
2024-02.bus:17: syntax error: unterminated quote
- dispatch failure (if target resolution is enabled):
2024-02.bus:23: dispatch error: unknown target "bnak"
- command failure:
2024-01.bus:42: command failed (exit 1): journal add --date ...
Exit codes
-
0success -
1command execution failed -
2usage error (invalid flags, missing file) -
65syntax/tokenization error
If a module returns a specific non-zero code, bus should return that same code for command-failed cases.
Environment signals for modules
During busfile execution, bus must set:
BUS_BATCH=1BUS_BUSFILE=<path>BUS_BUSFILE_LINE=<n>
Ecosystem integration
bus-replay export format
Once busfile support exists, bus-replay should export deterministic command logs as .bus files:
- default output one file per month/period (configurable), for example
YYYY-MM.bus - stable replayable
bus <module> ...command lines - directly executable via:
bus YYYY-MM.busbus YYYY-MM.bus YYYY-MM+1.bus ...
Examples
Single busfile content
#!/usr/bin/env bus
# 2024-02-29 Bank import-bank-00001
bank add transactions \
--set bank_txn_id=import-bank-00001 \
--set import_id=import-bank-2024 \
--set booked_date=2024-02-29 \
--set value_date=2024-02-29 \
--set amount=-861.6800000000 \
--set currency=EUR \
--set counterparty_name='Example Vendor' \
--set counterparty_iban='' \
--set reference='REF-00001' \
--set message='EXAMPLE PAYMENT MESSAGE' \
--set end_to_end_id=import-e2e-00001 \
--set source_id='bank_row:00001'
journal add \
--date 2024-02-29 \
--desc 'Bank import-bank-00001 Example Vendor payment' \
--debit 2949=861.68 \
--credit 1910=861.68 \
--source-id bank_row:00001:journal:1
Multi-file busfile orchestration
#!/usr/bin/env bus
2024-01.bus
2024-02.bus
2024-03.bus
Implementation design
Dispatcher entrypoint changes
- Parse existing global flags.
- Identify first non-flag argument.
- If recognized as busfile, enter busfile mode.
- Else dispatch as normal module invocation.
No special-case dispatch is required for run.
Busfile parsing
Implement a small tokenizer (or vetted shellwords parser) that:
- supports whitespace splitting, single/double quotes, backslash escapes
- performs no expansions
- preserves token order/content
Track:
- file path
- line number
- raw line
argv []string
Provider implementations
-
none: direct execution -
git: isolated branch/worktree -
snapshot: external/specialized provider -
copy: temp copy + apply
Testing
Unit tests
- tokenizer:
- quotes, escapes, whitespace, comments, blank lines
- failure case: unterminated quotes
- busfile recognition:
-
.busextension and shebang detection - collision avoidance with module names
-
- configuration precedence:
- defaults vs datapackage vs preferences vs CLI override
Integration tests
- syntax preflight across multiple files:
- three files, one syntax error, verify no command executed
- provider
none:- verify preflight then fail-fast execution
- provider
git(when Git available):- verify all-or-nothing semantics for
fileandbatchscopes
- verify all-or-nothing semantics for
Cross-module conformance tests (required)
- validate that each
bus-*module used by busfile workloads accepts--check - verify that invalid inputs fail in
--checkmode without mutating workspace data
Performance tests (required)
- benchmark tokenizer and preflight on large
.busfiles - benchmark multi-file global preflight (
Nfiles, mixed command sizes) - benchmark end-to-end apply orchestration in
noneprovider mode - where enabled, benchmark
gitprovider overhead forfileandbatchscopes - enforce regression thresholds in CI for key hot paths
Documentation updates
- Update
docs/modules/bus:- running
.buscommand files - month-based workflow examples
- syntax preflight and optional atomicity providers
- running
- Mention configurable atomicity via datapackage/bus-preferences and that Git is optional.
- Reference
bus-replayas exporter of.busfiles once available.