Docs · Architecture

The Data-Shape Ledger

every boundary-crossing shape, with producer, spec, consumers, stability

Extracted against the code of the v0.5.5 release line (2026-08-19). This follows the DATA, not the control: where the invocation map walks the verbs, this walks the values. Spec coverage comes from the live registry, not from memory — all 118 src/ namespaces loaded into one JVM yield 675 keyword specs across 58 spec namespaces and 101 fdefs, a census this ledger reconciles station by station. The worked trace uses real captured EDN from a live scenario run.

The chain in one paragraph

Two files enter (the project’s shiftlefter.edn, the machine’s ~/.shiftlefter/config.edn); project-context/resolve turns cwd + flags into location facts; runner.config/load-config merges and validates the project config. From there the chain forks and rejoins: the run leg loads glossary + intents, parses features into pickles, binds them into run plans (where the SVO map is stamped), threads a ctx stash through execution, and folds step → scenario → suite → run results; the query leg (orient/glossary/explain) rides build-projection over the same loaders and emits version-stamped EDN. Everything the run wants to say crosses ONE seam — the reporter envelope discipline — and leaves as the locked --edn summary (payload by attachment ref), the bus event envelope, or a rendered report in results/<stamp>/. The stability story is deliberately tiered: inputs are spec-validated at the boundary, outputs are contracted by docs + additive discipline (§ Stability tiers).

diagram diagram

The ledger

One station per boundary shape. Producer pins are definition lines at the 2026-08-19 extraction (§ 1’s config pins re-verified 2026-08-23); “spec” is the registered s/def keyword (— means none registered: the contract is docstring/doc, see § Stability tiers).

1. Project config — shiftlefter.edn

Shape Producer Spec Consumers Stability
loaded config map runner.config/load-config config.clj:326 (safe wrapper :400; normalize :792; lint-config :556) ::config config.clj:154 + ~44 sub-specs (::runner :98, ::interfaces :129, ::costumes :136 + ::costume-def :135, ::svo :145, ::glossaries :151, ::timing :109, …) projection (project_projection.clj:94), run pipeline (runner/core.clj:622), doctor, repl, usage-index; all downstream reads go through the accessor surface config.clj:417-511, :831-918 additive; unknown top-level keys are WARNINGS never errors (forward-compat ruling, config.clj:556-568)

2. Machine config — ~/.shiftlefter/config.edn

Shape Producer Spec Consumers Stability
user config map config.user/load-user-config user.clj:61 (safe :83; merge :101) costume (costume.clj:239,281,356,474), doctor probes (doctor.clj:151,195,201), etaoin adapter spawn path key set #{:chrome-path :chromedriver-path} lives in the ns docstring; enforced only by doctor’s literal allowlist doctor.clj:110

3. Project context

Shape Producer Spec Consumers Stability
project-context map project-context/resolve project_context.clj:153; resolved once per invocation at core.clj:913, threaded as opts :project-context runner.config :248/:348, projection :68-96, runner.core :179-213, results.clj :69/:130, teardown-marker :37, wardrobe :65, usage-index :76, daemon :150, doctor, orient, resolution doc-contract; ownership rule “location facts only, prints nothing, diagnostics are data” (ns docstring)

4. Projection

Shape Producer Spec Consumers Stability
projection map project-projection/build-projection project_projection.clj:490 (assembler complete-projection :420; bounded view project-view :604) orient (orient.clj:208; --edn emits it verbatim), resolution (resolution.clj:943), usage-index (usage_index.clj:57), sieve/contract.clj:78 :projection/version = 1 (project_projection.clj:23), stamped on every branch including errors; fingerprint = content identity

5. Resolution outputs — sl glossary / sl explain --edn

Shape Producer Spec Consumers Stability
resolution EDN maps glossary-edn resolution.clj:355, explain-edn :843, locator-owners-edn :857, vanilla-edn :80, no-project :105 external tooling/agents (the machine surface) :resolution/version = 1 (resolution.clj:42) on all five shapes; “--edn is the stable machine surface; the human format is EXPLICITLY UNSTABLE — do not parse it” (ns docstring)

6. Glossary

Shape Producer Spec Consumers Stability
merged glossary map svo.glossary/load-all-glossaries glossary.clj:732 (strict variant :774 — returns {:ok g :warnings […]}, defaults :392; merge :414; normalize :522) ::glossary glossary.clj:175 + sub-specs ::subject-entry :118, ::verb-entry :169, ::frame :160, ::glossary-error :183 binder opts via compile/build-binding-opts compile.clj:68; bind.clj :575/:599/:617; svo.validate :150; usage-index :150; projection :488; repl; agent-doc builtins-gen explicit additive contract: “::glossary is req-un, so consumers that don’t know about [added keys] conform unchanged” (glossary.clj:465-467)

7. Intents

Shape Producer Spec Consumers Stability
loaded intents map intent.loader/load-all-intents loader.clj:491 → {:ok {:lookup :regions :boundaries :intents :intent-files}} — (loader specs the AUTHORED EDN only: ::interface-locator :50, :intent/location :64, ::collection :73, …) bind.clj:683 (grammar hints), svo.validate :42 (object-slot checks), browser/intent.clj:297, locator-index :46, projection :293 open-map + optional :path are the declared forward-compat guards (loader.clj:56-59); loaded-map shape is doc-contract
parsed intent ref intent.resolve/parse-intent-ref resolve.clj:95 ::parsed-ref resolve.clj:57 (::segment :55, ::path :56) resolve-intent-ref :152, static validators :193/:266, resolve-location :299 spec’d; the O-slot value grammar (Intent.element[n].child)

8. Pickles

Shape Producer Spec Consumers Stability
pickle gherkin.pickler/pickles pickler.clj:432 (public wrapper gherkin.api/pickles api.clj:151) ::pickle pickler.clj:117 (:req namespaced), ::pickle-step :93, fdefs :460-475 runner parse stage (runner/core.clj:126/:137 → :all-pickles), compile-suite (compile.clj:150), bind-pickle (bind.clj:420), usage-index :52, compliance harness spec’d; :pickle/feature-name is a documented local extension over cucumber pickles

9. Bound plans — the SVO map

Shape Producer Spec Consumers Stability
binding map bind/bind-step bind.clj:246 (match branch :371-393) ::binding-map bind.clj:72 (all opt-un — open) step loop (invoke-step step_loop.clj:258), provisioning :308/:339, bindings-lint :57 spec’d-open
bound step same ::bound-step bind.clj:84; ::status :77 #{:matched :undefined :ambiguous :synthetic} exec loop, suite-lint :231, reports spec’d
run plan bind/bind-pickle bind.clj:407 (literal :429-432) ::run-plan bind.clj:98 (:req :plan/id :plan/pickle :plan/steps :plan/runnable?) execute-suite (cleanup.clj:348), execute-scenario (step_loop.clj:329), reports (:plan/pickle read by console/edn/junit), hooks, schedule spec’d; :plan/hooks rides ADDITIVELY (runner/hooks.clj:236, assoc’d only when non-empty — byte-identity for hook-less suites)
suite result bind/bind-suite bind.clj:653 (pipeline :688-692: bind-pickle → stamp-costumes :563 → stamp-slot-kinds :632); outer producer compile/compile-suite compile.clj:127 ::bind-suite-result bind.clj:128 run pipeline compile stage, dry-run verdict spec’d
stepdef SVO metadata (authored) registry/register! (stepdef shape registry.clj:4-16; keyed [pattern-sig iface-type] :118) ::stepdef-svo registry.clj:76 (:req-un subject/verb/frame; ::capture-ref :63 = :$N) binder, projection :stepdefs, bindings-join spec’d
extracted SVO (runtime) svo.extract/extract-svo extract.clj:155 (literal :197-201) ::svo extract.clj:44 (:req-un subject/verb/object/interface) validate-svo (validate.clj:405), provisioning (:svo read at :308/:339), :step/svo event payload (step_loop.clj:139) spec’d

10. Ctx stash

Shape Producer Spec Consumers Stability
scenario ctx map seeded by execute-scenario/cleanup wrapper (cleanup.clj:219: {:seed …} iff :run-seed); mutated by steps + Before hooks — for the map; ::bindings/::binding-name bindings.clj:57-60 for the :sl/bindings plane step loop (step_loop.clj:211), provisioning :41, hooks (:101/:161), cleanup enumeration :103, capture reserved-namespace contract: :cap/*, :run/*, :sl/* families ARE the stability guarantee; everything else is open (0.5.x; write-fence is 0.6 contracts work)

11. Result records

Shape Producer Spec Consumers Stability
invoke result invoke-step step_loop.clj:22/:66 ::invoke-result exec.clj:47; ::status :41 #{:passed :pending :failed :skipped :error}; ::error :44 step loop spec’d; error types :step/invalid-return, :step/exception
step result make-step-result provisioning.clj:29 (:binding = binding minus :fn); enriched step_loop.clj:282-289 with :duration-ms :bindings/produced :attachments ::steps exec.clj:52 (coll-of map — loose) scenario rollup, envelopes wrapper :status derived from children (rollup step_loop.clj:187)
scenario result execute-scenario step_loop.clj:329 (finalize :295; hook-wrapped cleanup.clj:152; Before-failure hooks.clj:223) ::scenario-exec-result exec.clj:64 suite fold, scenario-envelope full runtime keys: :status :plan :steps :scenario-ctx :error :hooks :attachments :capability-cleanup :duration-ms; status flips to :error on provisioning/capture-infra/After-hook failure (step_loop.clj:322-327, cleanup.clj:265-267)
suite result execute-suite cleanup.clj:348 (literal :404-414) ::suite-result exec.clj:84; ::counts :78 run stage :error count absent unless positive (exec.clj:73-76) — byte-identical-golden discipline
run result execute! runner/core.clj:1885 → :1264-1280 — (component specs: :shiftlefter.runner.run/status reporter.clj:131) CLI exit path, programmatic callers {:exit-code :run-id :status :counts :result} + opt :dropped-events :results-dir :group-root; :result is the RAW exec result BY DESIGN (core.clj:1282-1285) — envelopes are a projection, programmatic callers keep fidelity

12. Reporter envelopes — the seam

Shape Producer Spec Consumers Stability
scenario envelope reporter/scenario-envelope reporter.clj:263 (ALLOWLIST projection) ::scenario-result reporter.clj:122 (req only ::status) all reporters, HTML island, bus payloads drops :scenario-ctx, :plan/steps, :capability-cleanup — live objects die here
error envelope reporter/error-envelope reporter.clj:241 failures sections :value is pr-edn-str‘d exactly ONCE here; downstream passes through, never re-encodes (edn.clj:108-117)
run-ctx run-start-ctx runner/core.clj:491 (+ :results-dir/:artifacts-root merged :1191-1195) ::run-ctx reporter.clj:117 reporters on-run-start, JUnit properties “deliberately open maps; keys added by later beads must extend, never break” (reporter.clj:77-81)
run summary runner/core.clj:1266-1276 ::run-summary reporter.clj:139 reporters on-run-end ::dropped-events pos-int? — the absent-when-zero anomaly-marker convention

13. The --edn run summary + attachment refs — the locked contract

Shape Producer Spec Consumers Stability
run summary EDN report.edn/build-summary edn.clj:363; printed prn-summary :432; reporter EdnReporter :453 — by design external tooling/agents (stdout) LOCKED: “keys are additive, never repurposed — code written against the record survives releases” (docs/REPORTS.md:34-36); canonical shape doc edn.clj:7-73; form-stream contract :75-93; goldens test/fixtures/golden/edn-*.txt
attachment ref attachments/normalize-ref :143, finalize-refs! :222 ::ref attachments.clj:69 (:req :attachment/kind :attachment/path), ::after-return :82 (closed, lean-strict) summary :attachments index + inline on :failures (deliberate duplication, REPORTS.md:70-84) payload by reference — “the envelope never holds bulk text”; excerpts capped 4096

14. Bus event envelope

Shape Producer Spec Consumers Stability
event envelope events/make-event events.clj:242; :seq stamped in publish! :125 ::event-envelope events.clj:65 (:req-un type/ts/run-id/payload; :opt-un seq; :opt :scenario/id) observe plane: graph emission, telemetry, future workers locked alongside the summary (docs/AGENT.md:196-201); delivery is offer!-based drop-and-count (buffer 1024, dropped-events :212); total order guaranteed only WITHIN a scenario (events.clj:14-20)

15. Reports + the run-results directory

Shape Producer Spec Consumers Stability
console report report/console.clj:535 n/a humans (stderr) EXPLICITLY UNSTABLE — never parse
EDN report report/edn.clj:453 see station 13 machines (stdout) locked
HTML report report/html.clj:229 (island {:run-ctx :scenarios :diagnostics :summary} :257-260) humans + the island is machine-readable EDN dual-write: <group-root>/report.html + byte copy to --html path
JUnit XML report/junit.clj:426 CI systems written only to the configured path
results dir results/ensure-run-dir! results.clj:119 (stamp :54, spec ::stamp :50) ::stamp results.clj:50 attachments, HTML, humans <config-root>/results/<STAMP>/ (20260712T153042Z); self-ignoring .gitignore; explicit setup.clj groups get <run-dir>/<group-slug>/, an implicit single group does NOT; :defaults contexts, dry runs, and planning failures create nothing (results.clj:130)

Stability tiers

The chain’s contracts are deliberately tiered — the strength of the guarantee tracks who consumes the shape:

Tier Guarantee Shapes
Locked keys additive, never repurposed; doc + goldens are the contract; no version key needed --edn run summary, bus event envelope, attachment refs, exit-code table (strengthen-only until the contracts release)
Version-int */version integer stamped on every emission; bump = breaking projection (:projection/version + fingerprint key-set), resolution outputs (:resolution/version), macro entries (:representation-version)
Spec’d additive registered s/keys specs, mostly :opt-un/open; boundary validation on load project config, glossary, pickles, bind shapes, exec results, reporter run-ctx/summary
Reserved-namespace key FAMILIES are the contract, map stays open ctx stash (:cap/* :run/* :sl/*; open ctx until the 0.6 write-fence)
Doc-contract docstring + literal producer define the shape; no spec, no version project-context, user config, loaded intents map, compile failure shapes

The pattern behind the tiers: inputs are spec-validated at the boundary (the Clojure-module convention — no unvalidated external data reaches core functions), while machine OUTPUT surfaces are contracted by docs + additive discipline (the --edn summary is the established precedent).

The worked trace — one scenario, four machine surfaces

All captures REAL, from examples/04-sms-2fa at the 2026-08-19 extraction, one scenario (“Alice resets her password using a code sent to her phone”, 8 steps, two interfaces, zero custom steps), run live with ChromeDriver + the example’s fixture server. Substrate stamps the claim — the :git-sha names the development tree the trace ran on, quoted as captured: {:config-source :discovered, :git-sha "39468a25", :git-dirty? false}.

Station 1-2 (config in). The project config declares two interfaces and three :svo levels — and deliberately omits :unknown-object:

{:interfaces {:web {:type :web :adapter :etaoin :config {}}
              :sms {:type :sms :adapter :sms-mock}}
 :svo        {:unknown-subject :error
              :unknown-verb    :warn
              :unknown-interface :error}}

Station 3-4 (context → projection). sl orient --edn emits the projection verbatim. :mode :shifted (the :svo key is present). The conditional default is visible: intents are configured, so the effective levels gained :unknown-object :warn:

:svo-effective {:unknown-subject :error, :unknown-verb :warn,
                :unknown-interface :error, :unknown-object :warn}
:projection/id "proj-87a5c62e-341b-4045-8447-26809c0500b8"
:projection/version 1
:fingerprint "cdbc7c77844aa95b17c645a4be4e81761886db918764845b6574707b9a3effdc"

Each contributing file rides :inputs with a digest — content identity, not just a path:

{:kind :config,
 :path ".../examples/04-sms-2fa/sl/shiftlefter.edn",
 :exists? true,
 :digest "d9bbd9a37a13223e5c08719eeec5c51e8db08882d7cfd4af2c2d6bf26d4b546d"}

The adapter registry surfaces as :adapter-metadata per interface, with PROVENANCE: an interface on an undeclared builtin adapter shows the default entry’s full fields (:provenance :builtin-default, :impl-key, :provides, :capture-kinds, :has-on-provision?). An adapter that setup.clj declares shows :provenance :declared with the declaration’s data. Example 04’s :sms-mock pins its runtime fields as pure data (:impl-key nil, :on-provision? true), so its entry carries them outright and the registry contract verifies the live entry against the pin at station 10:

{:known? true, :provenance :declared,
 :provides [:shiftlefter.sms.protocol/ISMS
            :shiftlefter.sms.protocol/ISMSInbound],
 :capture-kinds [], :impl-key nil, :has-on-provision? true}

A declaration that does NOT pin them instead gets :runtime-unknown [:impl-key :has-on-provision?]: those are facts of the registry :start returns at station 10, not of the plan-time declaration, so the projection names them unknown rather than guessing from the default entry — a :start may legitimately return an override without the default’s hook or with a different impl shape, and a guessed :has-on-provision? true would then be a lie.

Station 5 (resolution). sl glossary --edn (:resolution/version 1) shows the corpus-riding usage markers — full-corpus scope, so the key is :unused?, never :referenced-in-selection?:

{:type :user, :description "Standard application user",
 :instances [:alice], :singleton? false,
 :provenance :user-file, :unused? false, :use-count 8}

Stations 6-9 (glossary/intents → pickles → bound plan). sl run --dry-run --edn stops at compile — a plan verdict, not a run verdict (:run/status :dry-run, exit 0 = the selection binds). The setup.clj group names itself, and the SVO validation tier already speaks:

{:run/id "83064bcd-…", :run/seed 1187363736201345408,
 :run/exit-code 0, :run/status :dry-run,
 :counts {:scenarios 1, :steps 8}, :group "sms-2fa",
 :diagnostics {:svo-issues [ 2 issues ], :counts {:svo-issue-count 2}}}

Stations 10–13 (execution → envelope). The live run. In flight, the (?<code>\d{6}) named group on the SMS receive step binds code into :sl/bindings; two steps later {code} resolves it back into the fill — the scenario data plane doing its one job. The captured summary (stdout; stderr carried only the run seed: line):

{:run/id "3de3400c-b959-430c-93cf-9c5663a3f930",
 :run/exit-code 0, :run/status :passed,
 :run/seed 5788014003369720924,
 :counts {:passed 1, :failed 0, :pending 0, :skipped 0,
          :scenarios 1, :steps 8},
 :diagnostics
 {:svo-issues
  [{:type :svo/unknown-object, :object "Enter Verification Code",
    :message "Invalid intent reference: …",
    :location {:step-text ":user/alice should see 'Enter Verification Code'",
               :step-id #uuid "5b43275a-…", :uri ".../password_reset_sms.feature",
               :line 11, :column 5},
    :severity :warn}
   {:type :svo/unknown-object, :object "Code verified for alice", }],
  :counts {:svo-issue-count 2}}}

Every ledger claim is visible in miniature: the locked summary’s key vocabulary; counts with :error absent (zero); #uuid tagged literals surviving typed (EDN-native stance); the ::svo-issue shape with its closed :type and full :location; and the two :warn diagnostics are the default from station 1 firing on the two free-text assertions — config decision in, diagnostic severity out, one chain.

Station 15 (disk). The run created sl/results/20260819T144724Z/sms-2fa/ — stamp from the group’s :started-at, slug subdir because setup.clj declares an explicit group. No summary.edn — the record went to stdout.

Siblings

How this map stays true

This page is a mechanical projection of a live-maintained internal map: re-verified against the code by probe runs at each re-stamp, regenerated — never hand-edited — by the derivation pipeline, and drift-guarded by the test suite (a hand edit here fails a test). File:line pins are re-verified at each re-stamp.