AutoSys->Stonebranch migration compiler: JIL frontend, semantic IR, linter, Mermaid visualizer, equivalence validator, UC backend, Python DSL.
Project description
dsl41 (codename)
Migration compiler for scheduler estates: AutoSys (JIL) frontend, semantic IR, linter, Mermaid visualizer, formal equivalence validator, Stonebranch Universal Controller backend, and a Python DSL extracted from patterns observed in the synthetic test corpus.
Start here, in order:
- docs/autosys-semantics.md - what JIL actually means (SEM entries)
- docs/stonebranch-semantics.md - target model + AutoSys->UC mapping (UCS/M entries)
- docs/ir-design.md - AST / IR-F / IR-G / oracle / equivalence design
- docs/jil-statement-syntax.md - statement scanner spec
- docs/decision-log.md - why things are the way they are
- CLAUDE.md - working agreement + implementation order
Status: all ten compiler phases built and tested; the phase-11 runner
(docs/runner-design.md)
is built — 11a (engine core + bisimulation gate), 11b (process
lifecycle tier: wrapper shim, real adapters, WAL journal, crash-recovery
resume; spool contract frozen in
docs/supervisor-protocol.md),
11c (calendar scheduler, preflight, control socket, headless CLI), 11d
(Textual TUI), 11e (serve via textual-serve), and 11f (the detached
supervisor tier) are all done. See the memo below for the source map.
CLI
One entry point (pyproject [project.scripts]): dsl41 = dsl41.cli:app. Run
uv run dsl41 --help, or install the package and call dsl41 directly. Every
command takes one or more JIL files that together form one catalog, and all
share the exit-code contract: 0 success/clean; 1 findings (lint, equiv
only); 2 the input never reached the tool (unreadable file, JIL parse error,
or DL-07 lowering refusal). --permit-unknown is the DL-07 escape hatch on
every command: carry unknown attributes verbatim instead of refusing.
Lint a catalog
dsl41 lint jobs.jil globals.jil # errors fail (exit 1)
dsl41 lint --strict jobs.jil globals.jil # warnings fail too
Runs L001-L018 (IR-F rules, truth-table rules, graph rules over the derived
graph, dangling-name checks). --strict is the migration gate: refuse to ship a catalog that lints
dirty.
Visualize the dependency graph
dsl41 viz jobs.jil -o graph.md # Markdown report of Mermaid charts
dsl41 viz --direction TD --collapse-threshold 20 jobs.jil
dsl41 viz --elk jobs.jil # ELK layout (VS Code; GitHub ignores it)
The report renders each independent workflow as its own chart (largest first),
with a legend and appendices for everything the charts thin out: standalone
admin-wrapper jobs (charted again with --include-singletons), assumed-edge
assumptions, redesign flags, OR shapes, and cycles. Within a chart: boxes are
subgraphs, edges carry their E/A/R migration class (solid/dashed/thick-red),
file watchers and schedules are marked as triggers, mutual exclusions render
as lock links or a shared lock hub, and boxes with more direct members than
the collapse threshold (default 12) fold into a single node. Any Mermaid
renderer works (GitHub, mermaid.live, IDE preview).
Migration report
dsl41 report jobs.jil -o report.md
Per-catalog markdown from the UC backend: refused (R) constructs, recorded
per-edge assumptions (A rows), and the open U-question table. Always exits 0
once generated -- the report IS the loud channel; use lint --strict as the
pass/fail gate.
Prove two catalogs equivalent
dsl41 equiv new.jil --against old.jil # all tiers
dsl41 equiv new.jil -b old.jil --tier c --scripts 50 # more oracle runs
dsl41 equiv new.jil -b old.jil --rename OLD=NEW --case-fold # renamed estate
Tier a is structural (canonical-form diff), tier b enumerates per-job truth tables (defers, never fails, on too-large state spaces), tier c compares oracle traces over seeded deterministic event scripts. Identical canonical hashes short-circuit to equivalent. Exit 1 on any divergence. Typical use: refactor a catalog (by hand or via decompile-edit-rebuild), then prove nothing changed.
JIL -> DSL (decompile)
dsl41 decompile jobs.jil -o catalog.py
Emits a runnable Python module over the phase-10 builders. Executing the module rebuilds a catalog whose canonical form equals the original's (the round-trip property, tested corpus-wide).
DSL -> JIL (build)
The reverse direction is a Python API, not a CLI command:
from dsl41.dsl import CatalogBuilder
b = CatalogBuilder()
b.machine("prod1")
with b.box("nightly"):
b.job("extract", command="/opt/etl/extract.sh", machine="prod1")
b.job("transform", command="/opt/etl/transform.sh", machine="prod1")
b.job("load", command="/opt/etl/load.sh", machine="prod1")
b.sequence("extract", "transform", "load")
jil_text = b.to_jil() # JIL text, byte-for-byte what the front end accepts
catalog = b.build() # ...or parse+lower it through the real pipeline
job() keyword names are JIL attribute names; sequence() wires s()-chains
and parallel() fans out/in, both refusing to merge into an existing
condition (DL-17: no silent loss). There is no second lowering path -- the
builder generates JIL and reuses parse -> lower, so lint, viz, and
equiv all apply unchanged to DSL-built catalogs. The round-trip workflow:
decompile an estate to Python, edit it, run the module, equiv the result
against the original.
Run an estate (phase 11)
dsl41 run jobs.jil --run-root ./run1 # headless engine + control socket
dsl41 sendevent STARTJOB -J job_a -S ./run1/control.sock
dsl41 query status -S ./run1/control.sock # JSON: statuses, timers, log paths
dsl41 ui -S ./run1/control.sock # attach the TUI; q detaches
dsl41 run jobs.jil --run-root ./run1 --ui # ...or one terminal owning both
dsl41 rehearse jobs.jil --hours 24 # virtual clock: a day in seconds
dsl41 serve -S ./run1/control.sock # the same TUI over the web
The TUI (jobs table with pending timers and alarms, explain pane with
per-atom condition truth, log tail, sendevent console) is the optional
[ui] extra: pip install 'dsl41[ui]'. It is a thin client of the run's
control socket — the same protocol sendevent/query speak.
Detached mode (phase 11f)
By default a run is tethered: killing the engine terminates its jobs
(durably recorded even under kill -9, ss6a). For long-running estates that
must survive an engine restart (an upgrade), add --detached:
dsl41 run jobs.jil --run-root ./run1 --detached # CMD jobs run under a supervisor
# ...stop the engine (SIGINT) -- jobs keep running under the supervisor...
dsl41 run jobs.jil --run-root ./run1 --detached --resume # reattach, no re-run
dsl41 supervise list --run-root ./run1 # what the supervisor is holding
dsl41 supervise shutdown --run-root ./run1 # stop it (TERM->grace->KILL)
A per-run-root supervisor (runner_supervisor.py, stdlib-only, one process
per run root) owns the wrappers' lifelines, so the jobs' parent is the
supervisor rather than the engine. Stopping or crashing the engine leaves
the jobs running; --resume --detached reconnects and reattaches to the
still-alive runs (no reconciliation injection, no re-execution) and resolves
any that finished meanwhile from the spool. The engine holds a single
fencing lease; the supervisor's socket protocol is frozen in
docs/supervisor-protocol.md
ss5. supervise is read-only by default (DL-42).
Serving the TUI over the web (phase 11e)
dsl41 serve -S ./run1/control.sock wraps
textual-serve around the
same app: every browser tab gets its own dsl41 ui --socket subprocess
attached to the run (the ss11 one-instance-per-viewer split), rendered as a
terminal in the page. textual-serve ships no authentication, so the
default bind is loopback (127.0.0.1:8000) — reach it from elsewhere via a
reverse proxy or an SSH tunnel, never by widening --host:
# tunnel: from the operator's machine
ssh -L 8000:localhost:8000 runhost
# or an nginx location block on the run host
location /dsl41/ {
proxy_pass http://127.0.0.1:8000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Put auth (basic auth, an OIDC gate, client certs — whatever the estate
already trusts) in that proxy layer; dsl41 has none of its own here. The
control socket is 0600 from birth (ss10), so serve only ever sees what
its own user could already reach directly — it does not widen access, it
makes existing access reachable from a browser.
Implementation memo
All ten phases from CLAUDE.md's implementation order are implemented and tested, in
build order: ast_jil, conditions, ir, lint, derive, viz, oracle, equiv, backend_uc, dsl.
Phase 11 (the runner,
docs/runner-design.md)
is underway: 11a — the sans-IO engine loop, VirtualClock, FakeAdapter, and the two
oracle additions, gated by the ss13 bisimulation suite — and 11b — the process
lifecycle tier (per-run wrapper shim, LocalCommand/FileWatcher adapters, WAL journal,
crash-recovery resume with the reconciliation ladder; spool contract frozen in
docs/supervisor-protocol.md)
— and 11c — the ss5 calendar scheduler, ss8 preflight, ss10 control socket
(sendevent parity + queries + subscribe), and the headless
run/rehearse/sendevent/query CLI verbs — and 11d — the ss11 Textual TUI
(dsl41 ui against a running engine, or dsl41 run --ui; optional
dsl41[ui] extra) — and 11e — dsl41 serve via
textual-serve, same extra
— and 11f — the ss6a Tier-1 supervisor (dsl41 run --detached,
dsl41 supervise), a stdlib-only runner_supervisor.py speaking the frozen
docs/supervisor-protocol.md
ss5 socket protocol — are all built. The suite spans 22 test files
(pytest --collect-only -q for the current count) plus the 15-file
synthetic/doc-derived JIL corpus under tests/corpus/.
Source map
- src/dsl41/init.py — module map docstring only (no exports); records the ten-phase build order
- src/dsl41/ast_jil.py — JIL statement-level scanner + AST + preserve/canonical
renderers; fidelity contract F1-F4: byte-exact
render(parse(x)) == x(F1, fuzzed by F3), canonical-mode fixpoint (F2), escaped-colon torture (F4) - grammars/condition.lark — condition-expression grammar (lark, LALR); carries the Q1
precedence switch as two start rules,
start_flat(default, flat & / |) andstart_prec(C-style, & binds tighter) - src/dsl41/conditions.py — lark loader + Tree->Cond transformer for condition/box_success/box_failure expressions; lookback + span retention
- src/dsl41/ir.py — IR-F Pydantic entity models + AST->IR-F lowering; DL-07 firewall
refuses unknown attributes unless
permit_unknownis set - src/dsl41/lint.py — Violation model + rules L001-L018 (pure IR-F rules L001-L005/L015, truth-table rules L006/L007 joined in phase 8, graph rules L008-L014 over the derived graph, dangling-name rules L016-L018)
- src/dsl41/derive.py — IR-F -> IR-G: seven analysis passes producing edges, mutex pairs, box tree, same-cycle detection, M01-M36 mapping-row classification
- src/dsl41/viz.py — IR-G -> Markdown report of per-workflow Mermaid charts (DL-35): component split, trigger/lock visual grammar, E/A/R edge-class arrows, collapse threshold, appendices for everything the charts drop
- src/dsl41/oracle.py — AutoSys discrete-event semantics interpreter; script-driven completion, edge-triggered re-evaluation, per-SEM-entry trace tests
- src/dsl41/equiv.py — equivalence validator: canonical form + tier a (structural), tier b (per-job state-space enumeration), tier c (oracle trace comparison)
- src/dsl41/backend_uc.py and src/dsl41/uc_oracle.py — UC backend pair: backend_uc builds the UC twin model, classifies edges, and emits the migration report (record emission blocked on U3); uc_oracle is the UC-side twin interpreter that runs the P-Mxx expected-divergence pairs against it, sharing Event/TraceEntry with oracle.py
- src/dsl41/dsl.py — builder surface (job/box/sequence/parallel) + decompiler, extracted from corpus-observed patterns only (phase 10, last by design)
- src/dsl41/runner.py — phase-11 engine: single-writer loop over the oracle (dispatch table, time-ordered event queue, stale-completion gate), VirtualClock + RealClock, FakeAdapter + LocalCommandAdapter + FileWatcherAdapter, inputs-only WAL journal, resume/reconciliation ladder, calendar scheduler (ss5), preflight (ss8), control-socket server (ss10: sendevent parity, status/trace/explain/plan, subscribe); and the ss6a Tier-1 detached path (SupervisorClient + SupervisedCommandAdapter: SPAWN through the supervisor, await the exit push, detach-stop vs oracle-kill cancellation, resume-time reattachment)
- src/dsl41/runner_supervisor.py — the ss6a Tier-1 supervisor (phase 11f): stdlib-only (same enforced boundary as the wrapper), one per run_root, owns the wrapper lifelines so an engine restart reattaches instead of killing jobs; the frozen docs/supervisor-protocol.md ss5 socket protocol (SPAWN/SIGNAL/LIST/SHUTDOWN/PING + lease), same-uid peer-cred, Linux subreaper
- src/dsl41/runner_tui.py — the ss11 Textual TUI (optional
dsl41[ui]extra): a thin client of the control socket only (jobs table with pending timers/alarms, explain pane with per-atom truth, log tail of the ss6 std files, sendevent console); subscribe is a wake-up signal, every rendered view comes from the idempotent ss10 queries - src/dsl41/runner_wrapper.py — the ss6a Tier-0 per-run lifecycle recorder: stdlib-only (enforced DL-42 extraction boundary), records spawn.json/status.json durably, kills and records on lifeline EOF; spool contract in docs/supervisor-protocol.md
- src/dsl41/cli.py — typer entry points:
lint,equiv,report,viz,decompile,journal(render-by-replay of a run WAL),run(headless executor: wall clock, real processes, control socket; stop with SIGINT/SIGTERM;--detachedruns CMD jobs under a supervisor that survives engine restarts),rehearse(virtual clock + scripted adapters: a 24h estate in seconds, same engine path),sendeventandquery(clients of a running engine's control socket),supervise(11f:list/shutdowna run-root's detached supervisor, read-only by default),ui(the ss11 Textual TUI attached to a running engine;run --uistarts both in one terminal), andserve(11e: wraps textual-serve around the same app, onedsl41 uisubprocess per browser session; optionaldsl41[ui]extra, loopback by default) (exit 2 = catalog load/usage failure everywhere, incl. preflight refusals; exit 1 = findings forlint/equiv, a mid-run engine failure forrun/rehearse—reportalways exits 0 once generated: the report itself is the loud channel) - src/dsl41/main.py —
python -m dsl41, needed becauseservespawns each session's app as<sys.executable> -m dsl41 ui --socket <path>
Tests
- tests/test_ast_fidelity.py — F1-F4 round-trip fidelity, scanner structure and error paths, whitespace-sensitive edge cases
- tests/test_condition_grammar.py — grammar-level accept/reject cases, doc-derived only
- tests/test_conditions.py — Cond model shapes, lookback semantics, span retention, Q1 precedence switch
- tests/test_ir.py — IR-F lowering decisions: SEM-30/31/32/33/34, subcommand support v1, type-inapplicable attributes
- tests/test_lint.py — L001-L005/L015 rules plus the lint CLI exit-code contract
- tests/test_derive.py — the seven IR-G passes plus the graph-rule lint additions L008-L014
- tests/test_viz.py — Mermaid render structure (balanced blocks, id-safety, one golden render), the DL-35 markdown report (components, appendices, mutex encodings) plus the viz CLI
- tests/test_oracle.py — AutoSys oracle trace tests against the SEM entries, citing dossier §8's sparse T-ID index (T01–T34 range, not contiguous; T03/precedence is pinned at parse time in test_condition_grammar.py, not here); every test runs twice — Oracle-direct and Engine(VirtualClock, inert FakeAdapter) via tests/bisim_harness.py — the runner-design ss13 bisimulation gate
- tests/test_runner.py — phase-11a runner suite: oracle additions (next_timer_due/advance), VirtualClock, engine dispatch/cancellation/horizon discipline, the stale-completion gate, and the feed-only vs advance+feed and oracle-vs-engine hypothesis properties
- tests/test_runner_lifecycle.py — phase-11b lifecycle tier: wrapper process matrix (pgid separation, parent-loss kills, fd hygiene), the DL-42 phase-boundary kill matrix, spoofed-record/boot-flip guards, the engine-SIGKILL crash-recovery integration test (tests/runner_crash_driver.py is its engine subprocess), and the DL-44 review-finding regressions (kill-wins gate, advance-record replay)
- tests/test_runner_journal.py — WAL record shapes, read_journal tolerance/refusals,
catalog-hash sensitivity, replay fidelity, journal-first source tagging, and the
journalCLI - tests/test_runner_adapters.py — RealClock, LocalCommandAdapter end-to-end (SEM-09 boundary, append/stdin/profile semantics, KILLJOB kill path), FileWatcherAdapter steady-size polling under VirtualClock, and the AdapterResult mapping
- tests/test_runner_scheduler.py — phase-11c scheduler occurrence math (days/times/ start_mins, timezone + DST corners, E10 defaults), engine integration under the virtual clock, resume re-anchoring + the E9 missed-tick drops, and the ss8 preflight rule fixture pairs
- tests/test_runner_control.py — phase-11c control socket (sendevent parity verbs, status/trace/explain/plan queries, subscribe backfill/live seam, socket hygiene), the DL-45 commit-discipline regression, the run/rehearse/sendevent/query CLI, and the DL-46 status-response fields (pending_timers, log paths)
- tests/test_runner_tui.py — phase-11d TUI (skips without the [ui] extra): the sendevent console parser, ControlClient against a real ControlServer (round trip, reconnect, subscribe), and the ss13.6 pilot smokes (table, explain atoms, pending timers, log tail, key-driven STARTJOB)
- tests/test_runner_serve.py — phase-11e
serveCLI: missing-socket and missing-extra exit-2 paths, the constructed textual-serve command (quoting a socket path with a space), default loopback bind, bind-failure exit 2 — the real textual-serve Server is always monkeypatched (ss13.6 posture, thinner still: a CLI wrapper, not a pilot) - tests/test_runner_supervisor.py — phase-11f supervisor tier: the frozen ss5 socket protocol (unknown verb / bad version / malformed line, lease held / expire / re-acquire fencing monotonicity / stale token, SPAWN idempotency, SIGNAL pid-reuse refusal, peer-cred, stale-socket reclaim), the import-boundary AST test, Linux-only subreaper, and the detached kill matrix (SIGKILL engine → survive + reattach; kill -9 supervisor → spool-resolve TERMINATED; orderly SHUTDOWN; detach-stop SIGINT → reattach SUCCESS; oracle KILLJOB detached)
- tests/test_equiv.py — canonical form, tiers a/b/c, the L006/L007 lint rules (tested here because they share equiv's truth-table machinery), and the equiv CLI
- tests/test_backend_uc.py — edge classification, migration report, report CLI, the U3 block itself
- tests/test_uc_oracle.py — UCS-entry trace semantics (UCS-01/02/03/09/13) plus the P-Mxx expected-divergence pairs against the UC twin interpreter
- tests/test_dsl.py — the four corpus-extracted builders, cond_to_source fidelity, and the decompile round-trip property
What's not done
UC record emission is blocked on U3 (pull /resources/openapi.json from a live
controller, freeze docs/uc-edge-schema.md, generate client) — until then backend_uc
emits only the migration report + edge classification. Open questions Q1-Q7 (autosys
dossier §9), U1-U8 (stonebranch Part III), and the runner's E5-E10
(runner-design ss15) are unresolved. Those with a behavior default in code
(Q1-Q4, Q7, U1-U5, U8, E5-E10) run on a documented default marked
# PENDING: Qn/Un/En; Q5, Q6, U6, and U7 have no code switch yet — they live in the
dossiers and in backend_uc's migration-report question table. The Q1 precedence sentinel
test stays until Q1 resolves. Q1-Q3 need a live AutoSys instance; U3 needs a live UC
controller. The runner is complete through phase 11f (the detached supervisor
tier); phase 10 (the DSL surface) remains the last design item.
License
dsl41 is dual-licensed:
- Open source: GNU AGPL-3.0-only. Distributing modified versions, or offering them as a network service, requires offering the complete corresponding source under the same terms.
- Commercial: organizations that cannot accept AGPL obligations can obtain a commercial license — see COMMERCIAL.md.
Copyright (C) 2026 dsl41 authors. External contributions require a signed CLA preserving the dual-licensing right; corpus hygiene rules also apply (see LICENSING.md).
Most of the code is written with the assistance of industrial coding agents — primarily Anthropic's Claude — while the original ideas and design are my own.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dsl41-0.2.0.tar.gz.
File metadata
- Download URL: dsl41-0.2.0.tar.gz
- Upload date:
- Size: 550.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee7df18aec2d7bca750f82fe03c814f779b467453ab5495df17f38ae03c0f2e8
|
|
| MD5 |
7152cfd8cb227f26a0167e22c837cebd
|
|
| BLAKE2b-256 |
071f961ce18a7dff085aaac5f2f9d33f21f463aca013328453512901173b3e7a
|
Provenance
The following attestation bundles were made for dsl41-0.2.0.tar.gz:
Publisher:
release.yml on mrbald/dsl41
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dsl41-0.2.0.tar.gz -
Subject digest:
ee7df18aec2d7bca750f82fe03c814f779b467453ab5495df17f38ae03c0f2e8 - Sigstore transparency entry: 2150238395
- Sigstore integration time:
-
Permalink:
mrbald/dsl41@ece1e41e6c79a29ccf7afd2ffa2df03ac6d195c7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/mrbald
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ece1e41e6c79a29ccf7afd2ffa2df03ac6d195c7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file dsl41-0.2.0-py3-none-any.whl.
File metadata
- Download URL: dsl41-0.2.0-py3-none-any.whl
- Upload date:
- Size: 218.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2f1d30049e91ab38e70118306772461d2fe848631cfcf15a9fd9af14ccb8fa0
|
|
| MD5 |
46b5599a880d937c1313c57aaf3e7c80
|
|
| BLAKE2b-256 |
a147b26f8a74e3c784d6b976b35880ac49d63532cb87654aac9c160b9b5a7880
|
Provenance
The following attestation bundles were made for dsl41-0.2.0-py3-none-any.whl:
Publisher:
release.yml on mrbald/dsl41
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dsl41-0.2.0-py3-none-any.whl -
Subject digest:
e2f1d30049e91ab38e70118306772461d2fe848631cfcf15a9fd9af14ccb8fa0 - Sigstore transparency entry: 2150238974
- Sigstore integration time:
-
Permalink:
mrbald/dsl41@ece1e41e6c79a29ccf7afd2ffa2df03ac6d195c7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/mrbald
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ece1e41e6c79a29ccf7afd2ffa2df03ac6d195c7 -
Trigger Event:
push
-
Statement type: