Skip to content

Sense 23: The Engine's Seam — Where the Engine and the Tenant Disagree

Sense 23 · Forming · Last touched 2026-08-20

  • last_verified: 2026-07-27

Synced from docs/design/sense_23_the_engines_seam.md in the engine repo — that’s the source; this page is a build-time mirror.

The pack is a starter kit; the tenant is a life. The engine, it turns out, is a third party at the table — and it has been evolving without asking anyone’s permission.

Status: proposed Date: 2026-05-01 Author: the owner + Claude (travel-day conversation) Sibling of: Sense 20 — The Seam


Sense 20 named the boundary between pack and tenant. It carried four invariants — pack blindness, tenant sovereignty, no silent regressions, pack-out-of-reach — and gave them vocabulary: scope, provenance, merge base, conflict taxonomy, promotion.

But Sense 20 didn’t notice a third party in the room.

The engine evolves too. Compilers grow new fields. Registry tables gain columns. Macros get renamed. Schema expectations shift. None of this is pack/tenant territory; the engine moves on its own cadence, shaped by code commits in the engine repo.

When the engine moves, every tenant in the world is affected. Yet tenants are not in the room when the engine ships a release. They find out the next time someone runs jin make.

Three incidents — the same shape.

DateWhat movedWhat broke
2026-04-04Engine renamed probe_entity_typesignal_entity_type (Sense 14 vocabulary rename)Tenant entity_registry.sql still emitted the old column name; JinDesk queries against the new one returned errors
2026-04-17 (rmc)
2026-04-21 (inspire)
Engine + pack rolled forward on is_priority boolean formTenant SQL referencing the old priority (string) silently produced wrong results until the build failed downstream
2026-03-26 (intermittent)Engine’s afs update overwrote a tenant’s deliberate ASOF JOIN optimizationTenant lost an analytical optimization without anyone noticing for a week

Each looked like a bug. Each was a symptom of the same missing thing: the engine has no governed relationship with the tenant. It moves; the tenant catches up by accident.

This is the gap Sense 23 fills.


The Engine’s Seam is the boundary between the engine and any tenant that runs on it.

It is not the same as The Seam (Sense 20). The Seam is bilateral between two repositories of analytical content (pack ↔ tenant). The Engine’s Seam is unilateral in shape: the engine moves; tenants accept or are stuck. But the invariants of Sense 20 still apply, with different weight per invariant.

A healthy Engine’s Seam has the same four properties Sense 20 named, adjusted for the engine-tenant axis:

  1. It knows its sides. The engine declares what AFS shape it accepts. The AFS declares what engine version it was authored against. Neither side has to guess.
  2. It remembers. Every engine release that changed the AFS shape carries a migration script. The migration is the trace of what moved.
  3. It names its crossings. Soft deprecations are loud warnings. Hard breakages refuse to run and point at the migration. No silent rewrites.
  4. It refuses ambiguity. If the AFS predates a migration the engine considers mandatory, jin make halts. It does not “do its best” with old shapes.

The rest of this Sense is the vocabulary that makes those four properties operable.


Every engine release carries a version. Today this is set in pyproject.toml and reachable as jinflow.__version__. Tenant AFSes already declare a compatibility hint in jinflow.yml:

jinflow_engine_version: ">=0.1.0"

In Sense 23 this becomes a contract, not a hint:

  • The AFS declares the engine version it was authored against. Set on jinflow init and updated by migrations.
  • The AFS also declares the migration baseline — the oldest engine version whose shape this AFS still matches.
  • The engine, on every jin make, reads both and refuses to proceed if the AFS sits below the engine’s current migration floor.

The two-version tracking matters because authoring intent (when this AFS was first laid down) is different from compatibility state (what shape this AFS is currently in). After a migration, the second moves; the first is permanent history.


A migration is an idempotent script the engine ships that updates an AFS from version N to version N+1.

Migrations are first-class engine artifacts:

jinflow/migrations/
v0.43_signal_vocabulary_rename.py # probe → signal, etc.
v0.46_classifications_block_optional.py
v0.47_thresholds_via_dbt_vars.py
...

Each migration:

  • Names the engine version it bridges to.
  • Reads the AFS, writes the AFS. Pure file-tree transformation.
  • Is idempotent — running twice is a no-op.
  • Carries a one-line description (“renamed probe_entity_type to signal_entity_type in entity YAMLs”) and a longer explanation the operator can read before running.
  • Is reviewed by the engine team like any code change. The engine’s migration directory is the canonical record of every shape-shift.

Migrations run via an explicit command:

Terminal window
jinflow migrate numetrix.rmc
jinflow migrate numetrix.rmc --check # dry-run
jinflow migrate numetrix.rmc --to 0.47.0 # explicit target

Never silently inside jin make. The boundary between “I know what’s happening to my AFS” and “I don’t” is the migration command. jin make may suggest running it; it never runs it.


At the start of every jin make, the engine performs a handshake:

  1. Read engine’s current version: 0.47.0
  2. Read AFS’s authored version: 0.43.0
  3. Read AFS’s current migration baseline: 0.45.2
  4. Look up engine’s migration window: “I accept AFSes from baseline ≥ 0.45 without migration; ≥ 0.40 with auto-migration; below 0.40 refuse.”
  5. Decide:
    • Match → proceed silently.
    • Auto-migratable gap → emit a soft warning (“AFS is at 0.45.2; 0.46 added classifications support — auto-applying. Run jinflow migrate for clarity.”), apply minimal migrations in memory, build.
    • Migration required → halt. Print the migration command. Refuse to build.
    • AFS ahead of engine → halt. “This AFS expects engine ≥ 0.50; you are running 0.47. Upgrade the engine.”

The handshake is the named crossing. Without it, every jin make is a silent gamble.


Engine moves are not all the same. Sense 23 names two kinds:

Soft deprecation — the engine recognizes both old and new shapes for a window, prefers the new, and warns when it sees the old. Example: if a future release renames tooltips: to legends: (it shouldn’t, but as a worked example), the engine reads either, builds correctly, and emits “you’re using tooltips: — this works for now; please update by v0.50.”

Soft deprecation has a window. The release notes name the version where the old shape stops being read.

Hard breakage — the engine no longer accepts the old shape at all. Migration is mandatory before jin make will run. Reserved for shape-shifts where dual support is genuinely costly (registry column removal, file-layout changes, semantic incompatibilities).

The discipline:

  • Every breaking change ships with a migration.
  • Major version bumps are the only place hard breakages happen.
  • Minor versions only soft-deprecate.
  • Soft deprecations announce their hard-breakage version in their warning text.

This is not novel — it’s the discipline most mature systems carry. The Engine’s Seam is the formalization of it for jinflow.


The same four invariants Sense 20 named, in engine-tenant language. Two of them already hold; two are gaps that have bitten us.

The engine knows nothing about any specific tenant. No tenant name, no domain, no business context appears in engine code. A new hospital tomorrow runs jin make against the same engine as the hospital that contributed yesterday’s bug fix. This invariant is already true and worth preserving — code that “knows about rmc” or “handles inspire’s special case” should never land in the engine.

Tenant sovereignty against engine drift ✗ gap

Section titled “Tenant sovereignty against engine drift ✗ gap”

Currently jin make may auto-commit changes to tenant YAMLs as a side effect of building. We’ve seen this happen in surprising ways — a jin make auto-commit has captured changes the operator didn’t expect to be committed. This invariant says: the engine does not silently rewrite the tenant’s authored content. Migrations rewrite, named explicitly. Compilation reads, never writes back to authoring artifacts.

This is the Phase 2 of consistency_plan.md spirit, generalized to the engine boundary. Today’s jin make violates this in places it should not.

Column renames, schema removals, behavior changes — these have all happened silently. The engine moved; the tenant found out by debugging. This invariant says: every engine evolution that affects the AFS announces itself in a release-note entry that the operator sees, and (for soft deprecations) at runtime when the old shape is detected.

Today there is no such announcement infrastructure. The engine ships; release notes describe code-level changes; nothing tells the operator “your tenant’s entity_registry.sql was generated against an old column name; your queries may fail.”

Engine-out-of-reach at runtime ✓ already

Section titled “Engine-out-of-reach at runtime ✓ already”

The engine is not a runtime dependency of tenant data. The KLS does not call back to engine code; JinDesk reads the KLS directly; SIS does not invoke compilers. An engine version mismatch breaks jin make (the boundary) but does not break a built KLS that’s already on disk. This invariant already holds and should never be weakened. (P2P2P depends on it; the Heartbeat depends on it; many operating modes depend on it.)


With Sense 23 named, the full picture has three seams, all governed by the same four invariants:

Engine ────────── ? ──────────► Tenant (Sense 23)
(Sense 20)
Pack
(deferred:
consortium)
Sister tenant

The engine evolves behind every pack and every tenant. The pack evolves through promotion from tenants. The tenant lives its life between alignment events with both.

A given jin make involves negotiating both seams: the engine must agree to run against this tenant’s AFS shape, and the AFS must be internally consistent with whatever pack-borrowed content it carries. Two compatibility checks, two named crossings.


  • No migration framework code. The migration directory layout, the script API, the dependency declaration — those are implementation. This Sense fixes the concept and the responsibilities.
  • No version-bumping policy. When does a change deserve a major version? That’s a release-engineering decision, not a Sense decision. This Sense only says: major versions are where hard breakage lives.
  • No specific deprecation list. The current engine probably has a handful of “you should be using X now” cases that haven’t been surfaced as deprecation warnings. Naming and warning each is follow-up work.
  • No retrofit for tenants below the migration baseline. If someone has a tenant on engine 0.13 and we’re shipping 0.50, the migration chain may not connect cleanly. That’s a content-rescue problem, not a Sense problem. We accept that some tenants may need manual triage.
  • No tooling for engine-team review. When an engine PR introduces a breaking change, who decides if it qualifies for major-version promotion vs auto-migration? That’s a code-review discipline question, addressed by the team’s process, not the Sense.

These are the next conversations. Sense 23 is the place they can begin from a shared vocabulary.


Three things this Sense unlocks once named:

  1. The is_priority class of incident becomes detectable. Today each one is forensic; with engine version handshake + migration discipline, the same incident next time announces itself before it bites.
  2. Pack-back becomes safer. Sense 20’s pack-back ritual depends on the pack and tenant agreeing on shape. When the pack moves forward via engine migration AND a tenant moves forward via afs update, the two crossings need to agree. Without Sense 23, that agreement is implicit; with it, it’s contractual.
  3. The engine’s release cadence becomes legible. Operators of tenants can read the release notes and see “this is a soft change, this is a hard one, this needs migration.” Today they have to grep diffs.

The Engine’s Seam is the door we did not notice was already in the wall. Naming it lets the team finally walk through deliberately instead of stumbling across it.


  • Sense 20 — The Seam. Direct sibling. Same four invariants, different bilateral. Both seams operate during a jin make / afs update cycle; both must agree.
  • Sense 21 — The Heartbeat. Beat 4 (sync) and Beat 5 (notify) carry engine-evolution news. A new engine release should pulse the operator: “engine 0.50 is out; your AFS is at 0.45 baseline; one minor migration available.”
  • consistency_plan.md. Phase 2’s “make is read-only” matches the tenant-sovereignty invariant here. Phase 4’s schema migrations are exactly the engine-side migrations this Sense names.
  • sense_25_identity_and_passes.md. The pass to run jinflow migrate is a higher-authority capability than jin make. Migrations rewrite authored content; only operators with write authority on the AFS should be able to run them.

The pack and the tenant have always known about each other. The engine has been pretending it doesn’t matter. Three regressions in two months say otherwise.

Naming the Engine’s Seam is the door we did not notice was already in the wall. The mechanics — version handshake, migrations, soft deprecation, hard breakage discipline — are familiar from every mature system that ships software people depend on. We are catching up to common practice, not inventing it.

What we keep, and what makes it ours: the four invariants. Engine blindness, tenant sovereignty, loud deprecations, runtime out-of-reach. The same four that govern The Seam, the same four that will govern The Federation when we name it. The shape of healthy seams everywhere.


Not yet manifest in the engine. Status: proposed. See the implementation phases / open questions above for the path from this paper to running code.


Numerical neighbors:Sense 22: The Legend — labels that explain themselves · Sense 24: The Steward — jinflow Operating On Itself

jazzisnow jinflow is a jazzisnow product
v0.64.7 · built 2026-09-20 19:48 UTC