Skip to content

Sense 43: The Cascade — Configuration as Layered Authority

Sense 43 · In bloom · Last touched 2026-08-20

  • last_verified: 2026-06-05

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

One mechanism. Multiple namespaces.

Configuration in jinflow is layered. Each authority — engine, pack, tenant, entity, explorer — can speak on any key. The resolver consults them top-down at the latest possible moment, and the chosen value carries its provenance. The KLS bakes the resolution so every snapshot is self-describing.

Status: Proposed Date: 2026-05-24 Author: the owner + Claude (in conversation, during Sense 14.2 Phase 3 design) Family: principle for Configuration Engine (the implementation reference); piloted by Sense 14.2 Typed Signals; narrated by Sense 18 The Ledger; Lab-room overrides per Sense 13 The Canvas.


Configuration in jinflow has historically been scattered across many homes — some YAML, some Python constants, some Svelte hardcodes, some TypeScript identity literals. Adding a new configurable dimension has meant touching several files across several layers, each with its own loading path and override mechanism. The Configuration Engine (2026-03) solved this for display config. But the same shape applies beyond display — wherever there are defaults that can vary by pack, by tenant, by entity, or by exploration.

There are two related failures of the pre-cascade world:

Hidden authority. When a default lives in code, no one outside the engine team knows it exists. The pack author cannot override it; the tenant owner cannot pin it; the explorer cannot temporarily overrule it for a what-if. The default is silently authoritative because it has no challenger.

Hidden provenance. When the value finally arrives at a render or a SQL query, there is no record of which layer chose it. If a number surprises, you cannot ask “whose decision was this?” — the answer is hidden behind a stack of imports.

The Cascade names the architectural answer: configuration is a typed inheritance chain, namespaced by concern, resolved at the latest possible moment, with provenance recorded in the artifact it produces.


Any setting whose value can legitimately differ across packs, tenants, entities, or exploration moments belongs in the cascade — not in code.

The cascade has one shape that all namespaces share:

LevelOwnerPersistence
L0 — enginejinflow codeHardcoded fallback only
L1 — packpacks/<pack>/config.ymlPack git
L2 — tenanttenants/<tenant>/config.ymlTenant AFS git
L3 — entity / perspectiveYAML in entities/, signals/, perspectives/Tenant AFS git
L4 — transientURL params, REPL stateNone — single view only

L0 through L3 are persistent. They bake into the KLS via jinflow make, get committed to git via the AFS round-trip, and the artifact’s _jinflow_* metadata tables record the resolved value along with the level that supplied it.

L4 is transient. It changes what the explorer sees, never what is stored. Which rooms admit L4 depends on the namespace, not on the room itself — see the Board, Lab, and the L4 contract section below for the namespace-axis breakdown. The short version: display L4 applies everywhere; analytical L4 is Lab-only.

The full mechanism — resolver algorithm, file structure, baked tables, CLI surface — is documented in the Configuration Engine reference. This Sense holds the principle and the namespaces; that doc holds the wiring.


The cascade governs more than one kind of configuration. Each namespace is a top-level block in config.yml with its own schema and its own stakes. New namespaces are added only when a concrete need surfaces — each one introduces a new region of governance.

How things look and how JinDesk behaves at runtime. The scope is broader than the name suggests: it covers presentation (precision, locale, branding), pagination behavior, search defaults, column visibility, recents, and other runtime UI choices. The block name was chosen early (when “display” covered most of what was in it); the scope grew. Renaming the block to something like presentation: is a future migration if the undersell ever becomes noisy in practice.

display:
paging: { default: 50, options: [25, 50, 100] }
pagination: { position: bottom, align: center }
precision: { ... }
columns: { hide_patterns: [...] }
recents: { ... }
nav_labels: { ... }

This namespace has been shipping since March 2026 and is the original use case for the Configuration Engine. It covers presentation, pagination, search behavior, formatting locale, column visibility, branding, and JinDesk UI defaults.

The risk it manages: wrong values mislead the eye — a price shown to one decimal when three are needed, a column hidden when it should be visible. The number itself is correct; only its presentation is off.

2. Analytical defaults — analytical_defaults:

Section titled “2. Analytical defaults — analytical_defaults:”

How numbers are computed when the YAML does not pin the answer.

analytical_defaults:
currency:
anchor: transaction_date # transaction_date | reporting_date | period_end
gap_policy: nearest_prior # for weekends/holidays
time:
window_anchor_resolution: max_observation_date
gap_policy: nearest_prior
quality:
min_observations_for_aggregate: 12

This namespace is proposed here and lands with Sense 14.2 Phase 3 — currency conversion is the forcing case. The pilot key is currency.anchor: do retroactive conversions anchor at the transaction date (audit truth), the reporting date (comparability), or period-end (accounting convention)? All three are legitimate. The choice belongs in AFS, not in a Python constant.

The risk it manages: wrong values mislead the mind — the same data produces different numbers depending on which default applied. A signal could be flagged “confirmed” under one anchor and “not_observed” under another. The presentation is fine; the answer itself has shifted.

Migration policy — proactive inventory, deliberate sweep. The current set of code-resident defaults that change numbers is finite and surveyable. As of this Sense, the surveyed candidates are: currency.anchor (Phase 1 pilot), currency.gap_policy, time.window_anchor_resolution, time.gap_policy, rounding.aggregation, and quality.min_observations_for_aggregate. Phase 1 lands the pilot; Phase 2 is a deliberate sweep of the rest — each key surveyed for its current code location, migration risk, and review owner. From Phase 2 onward, new analytical defaults go straight into the cascade. Never into code.

The cascade scales to additional sibling blocks when new classes of configuration warrant their own region of authority — for example, identity: if/when Sense 25 migrates its config from TypeScript into the cascade. Each addition is a deliberate decision; this Sense does not pre-commit to any of them. The principle: new namespaces join display: and analytical_defaults: as siblings under the same cascade, with the same baking strategy and the same L4 mechanics.


A pack author editing display precision and a tenant owner deciding how historical conversions anchor are doing different kinds of work. They have different reviewers, different audit implications, different blast radius. Putting both choices in a single undifferentiated config: blob would erase that distinction at the schema level.

Sibling top-level blocks make the distinction visible in the file. The schema mirrors the governance: each namespace is a separate region of authority, with its own validation rules, its own bake strategy, and its own provenance trail.

It also leaves room for namespaces to evolve independently. The display cascade has been stable since March; the analytical cascade is still finding its keys. Forcing them through one schema would slow both down.


  1. The resolver reads each namespace’s blocks from L1, L2, and any relevant L3 YAMLs.
  2. Compilers that emit SQL consume resolved values via the resolver interface.
  3. The KLS bakes one metadata table per namespace:
    • _jinflow_config — the existing display-config bake.
    • _jinflow_analytical_defaults — new, lands with Phase 3.
    • (Future namespaces get their own tables in the same shape: key, value, level, source_file, resolved_at.)
  1. JinDesk reads from the baked tables when it needs to display “this number was conversion-anchored at transaction date” or “this column’s precision was set at the tenant level.”
  2. URL params apply L4 overrides, namespaced to match the config block:
    • ?display.precision.price=4 — single-view display override.
    • ?ad.currency.anchor=reporting_date — single-view analytical override.
  3. A “posture chip” renders wherever an override is in effect, making the override visible to the eye.

The Board is the URL idiom for authored truth (/board/<instance> — the Lens-browser room from Sense 27). The Lab is the URL idiom for exploration (/lab/<instance> — chart-based, supports exploratory what-ifs). Studio composes authored views, like the Board.

Which L4 overrides each room admits is decided by the namespace, not the room. The original framing of “Lab admits L4, Board does not” forced a single principle to do work it couldn’t carry. The honest axis is the kind of override:

NamespaceWhere L4 appliesWhy
display:Board + Lab + Studio (everywhere)Render-time only. Precision, locale, column visibility — overriding these doesn’t change what the number is, only how it is shown. No authored-truth claim is violated; the override is equivalent to resizing a column.
analytical_defaults:Lab onlyChanging currency.anchor shifts what the number means. On a Board or Studio (authored-truth rooms), that’s epistemically dishonest — the viewer thought they were looking at the authored picture, now they’re not. Lab is the room where what-ifs belong.

?display.<key>=<value> is honored on Board, Lab, and Studio alike. The posture chip appears; the override applies; the URL is shareable. Same nature as any per-view display setting users already control (sort, filter, pagination).

Analytical L4 — Lab only, with a Board fallback

Section titled “Analytical L4 — Lab only, with a Board fallback”

?ad.<key>=<value> is honored in Lab. On Board (and Studio), a non-blocking banner appears instead of silent rejection:

ℹ Override ?ad.currency.anchor=reporting_date isn’t applied here. Board shows authored truth only. [Open in Lab →] [Why?] [✕]

The CTA routes to /lab/<same-instance>?<all-params> — same Lab counterpart with all URL params preserved (filters, sort, pagination, AND the analytical L4 override). Where no Lab counterpart exists, the fallback CTA opens the default Lab with the params attached. The “Why?” link expands a short inline explanation of the namespace-axis distinction.

Only recognized L4 keys trigger the banner or chip. The resolver knows the schema of display.* and ad.* keys; arbitrary URL noise (?utm_source=…, ?ref=…) is ignored.

L4 overrides live in the URL. URLs are shareable and bookmarkable — so a pasted-to-Slack L4 URL renders the override for any recipient who opens it. “Transient” means never persists to the cascade (never becomes a new L1/L2/L3 default). It does not mean the URL itself is one-shot.

Why a visible banner instead of silent rejection (for analytical on Board): silent operations are exactly the hidden authority failure mode this Sense is fighting. The banner is informational (does not block Board rendering), teachable (the “Why?” link explains the mechanism on first encounter), and recoverable (one click opens the view that honors the override).

When Sense 18 lands, every Validation panel can quote the resolved cascade:

Margin: CHF −5.80 per item. Window: P12M ending 2026-04-30 (latest delivery), analytical_defaults.time.window_anchor_resolution resolved from tenant.yml. Currency: anchored at transaction date, analytical_defaults.currency.anchor resolved from pack default.

Every assumption traceable. Every assumption changeable. None hidden.


  • It does not redefine display configuration. The Configuration Engine has been shipping the display: cascade since March 2026 and continues unchanged. This Sense names the principle the engine already realises and opens it to additional namespaces.
  • It does not absorb every constant in the codebase. Only defaults whose value changes a number (analytical) or a presentation (display) belong here. Implementation constants, algorithm parameters that aren’t user-meaningful, and internal thresholds stay in code.
  • It does not govern Subject Matter. Subject Matter declares facts about the world; analytical defaults declare how to compute facts about the world. Adjacent and complementary; not the same.

Phase 1 — Schema + resolver + first analytical key ✅ SHIPPED 2026-05-24

Section titled “Phase 1 — Schema + resolver + first analytical key ✅ SHIPPED 2026-05-24”

Engine commit 6f19e059 (under the “Sense 14.2 sub-phase 3.0” label — the currency arc was the forcing function).

  • jinflow/config.py resolver with ENGINE_DEFAULTS (L0 fallbacks), Resolved dataclass, resolve() / resolve_all(). Reads <afs_root>/config.yml for the analytical_defaults: block.
  • bake_analytical_defaults() in jinflow/cli/commands/baking.py writes _{tenant}.analytical_defaults (key, value, level, source_file, resolved_at) per tenant.
  • Wired into jinflow/cli/commands/make.py next to bake_metadata.
  • Pilot key currency.anchor consumed end-to-end by the perspective compiler (Sense 14.2 sub-phases 3.2.b + 3.2.c).
  • 17 unit + bake tests in tests/test_config_resolver.py.

Currently collapses pack (L1) and tenant (L2) into a single afs level because tenant AFSes are file-copies of the pack at init; disentangling waits for Sense 36 The Lineage birth-manifest consultation.

Phase 2 — Proactive sweep of the analytical-defaults inventory

Section titled “Phase 2 — Proactive sweep of the analytical-defaults inventory”

Partially shipped — survey revealed the inventory was over-specced.

A 2026-05-28 audit of the five candidates the original Sense draft listed found that only one was a real, in-code constant ready to migrate:

CandidateVerdict
currency.gap_policy✅ shipped 2026-05-28 — was hardcoded nearest_prior in the convert_currency macro; now read from the cascade.
time.window_anchor_resolutionDeferred — no signal currently relies on a default (every signal declares anchor: explicitly). Add when a real driver appears.
time.gap_policyDeferred — lives implicitly in pack-authored SQL (asof left join in gold_io_metering, gold_price_series). Making it cascade-aware is a cross-repo lift; wait for a forcing case.
rounding.aggregationDeferred — speculative. Doesn’t exist in code yet.
quality.min_observations_for_aggregateDeferred — speculative. Doesn’t exist in code yet.
display.* (every display.… key)Spec’d 2026-05-30, implementation pending. The display: namespace claims to cascade but the runtime path is single-layer. Design choice (a) — bake-time deep-merge — adopted. Full spec in the next section below.

currency.gap_policy migration details (commit-shipped 2026-05-28):

  • Added to ENGINE_DEFAULTS with value nearest_prior. Valid values: nearest_prior (default), skip. interpolate reserved for future.
  • convert_currency macro reads {{ var('currency_gap_policy', 'nearest_prior') }} and gates the rate lookup accordingly. nearest_prior keeps the original ASOF semantics; skip requires exact-date match and returns NULL otherwise — paired with the perspective compiler’s not_null test (Sense 14.2 sub-phase 3.2.c), skip turns missing rates into a hard build failure.
  • jinflow make now injects every resolved analytical_defaults key as a dbt var automatically. Convention: cascade key a.b.c becomes dbt var a_b_c. Future cascade keys ride the same wire with no per-key plumbing.

Future Phase 2 keys join the cascade one at a time, when a real driver arrives. The Sense doesn’t pre-commit beyond the keys the migration audit has already identified as real.

Phase 2 sub-task: display: cascade fix (spec, 2026-05-30)

Section titled “Phase 2 sub-task: display: cascade fix (spec, 2026-05-30)”

The display: namespace cascade was documented but never implemented at the bake/runtime path. This spec defines the fix. The implementation lands as a follow-up commit, against this spec verbatim.

Background — the gap, recapped. bake_metadata reads only the tenant’s afs/config.yml and stores it as a single YAML blob in _<tenant>.config.config_yaml. JinDesk (configEngine.ts) reads that blob, parses it, and uses it. The pack’s config.yml is never consulted at bake time and there are no engine fallbacks. Tenants who want their pack’s defaults to apply must copy-paste them into their own config.yml (which numetrix tenants do); a pack that omits a key has no way to set a default for its tenants (hrcentral.vai has no display.grouping because the hrcentral pack didn’t ship one). Both failure modes hide the fact that the cascade doesn’t work.

Choice adopted: (a) bake-time deep-merge. Engine fallbacks ⊕ pack config.yml ⊕ tenant config.yml, merged at jin make, the result baked into the existing _<tenant>.config.config_yaml. JinDesk’s read path stays single-blob. Per-key provenance lands in a sibling table.

Deep merge with the following rules, applied recursively:

ShapeBehavior
Two mapsKeys union; conflicting keys recurse.
Two scalarsRight-side wins.
Two listsRight-side fully replaces (no append, no element-wise merge).
Map vs scalar / list vs mapRight-side fully replaces. Type mismatch is a warning during bake.

“Right side” means the higher-precedence side — tenant beats pack beats engine.

Worked example:

# engine fallback (L0 — built into jinflow.config)
display:
grouping:
patterns: "_type|_status|_year"
pagination:
page_input: true
# pack config.yml (L1 — numetrix)
display:
grouping:
patterns: "_type|_group|_source|_status|_year|_category|_code"
pagination:
begin_end: true
align: right
# tenant config.yml (L2 — rmc)
display:
pagination:
align: left
# resolved (baked into _<tenant>.config.config_yaml)
display:
grouping:
patterns: "_type|_group|_source|_status|_year|_category|_code"
pagination:
page_input: true # from engine
begin_end: true # from pack
align: left # from tenant (overrode pack's `right`)

The display.grouping.patterns string at L1 fully replaces L0’s shorter list (string replace, not pattern concatenation). The tenant didn’t touch grouping so the pack value carries through. pagination.align cascades L0 → L1 → L2 by full replacement; the last writer wins.

List replacement rationale: the existing pack/tenant display.recents.dimensions and display.grouping.patterns are authored as full specifications, not as fragments to extend. Append semantics would require pack authors to write delta lists which is a new authoring concept. Replace keeps current YAML files working untouched.

Aggregation semantics (gap acknowledged, deferred)
Section titled “Aggregation semantics (gap acknowledged, deferred)”

The merger rules above are uniform — every non-map value replaces, full stop. V1 ships with this rule because the forcing case (vai missing grouping.patterns) is solved by the L0 engine fallback, not by aggregation between levels.

But uniform replacement is not the whole picture, and a future spec will need to address it deliberately. Three gaps the V1 rule papers over:

Gap 1 — Per-key aggregation policy. Some keys want replace semantics (the last pagination.align writer wins, period). Others want aggregation (a tenant adding _orgcode to the pack’s existing _type|_status|_year grouping patterns shouldn’t have to copy-paste the whole alternation). The right answer is per-key policy, not a uniform rule. Candidate policies:

PolicyBehaviorLikely keys
replaceRight-side wins (V1 default)pagination.align, paging, table.sticky_header
unionSet-union of unique elementsdisplay.locales, list of dossier categories
appendLeft then right, dedupe preservedrecents templates if ever made list-valued
concatString join with separator (e.g. | for regex alternation)grouping.patterns, columns.hide
deep_merge_specialCustom per-key mergerrare, escape hatch

Gap 2 — Value-shape sensitivity. Aggregation rules depend on the value’s shape. grouping.patterns is a string containing a regex alternation, not a YAML list — appending two patterns means string concatenation with a separator, not list append. Three shape categories the future merger has to handle distinctly:

ShapeAggregation candidate
Scalar (number, bool)replace only
Stringreplace, or concat with separator
YAML listreplace, append, or union
YAML mapalready recursive (V1 deep-merge)

Gap 3 — Axis-dependent aggregation. Per the [Outlook](#outlook ---additional-resolution-axes), future role/identity/ruleset axes will compose on top of the layer cascade. A role adding patterns will almost certainly want aggregation (not replacement) over the layer-merged result — otherwise a role overlay would have to re-state the pack’s full set. The aggregation policy may also differ by axis: layer cascade replaces, role overlay appends.

Why V1 still ships as-is:

The vai case — the immediate forcing function — needs L0 engine fallback, not inter-level aggregation. The grouping patterns aren’t being added to anything; they’re absent and need a baseline. The engine catalog (line 512) covers this.

Tenants who want to extend a pack’s regex string today still have only one path: copy-paste-extend. That was true before this spec and remains true after. The V1 fix does not make this worse. It does not preclude a future fix either — the per-key policy can be introduced later by adding an AGGREGATION_POLICY catalog in jinflow.config and consulting it inside the merger, with the V1 uniform-replace as the default policy when a key isn’t listed.

Forcing cases that will trigger the future Aggregation Sense sub-task (when one shows up — none has yet):

  • A tenant author asks: “How do I add _orgcode to my pack’s grouping patterns without restating the whole string?”
  • A role overlay needs to additively widen a pack’s pattern set without erasing it.
  • A new namespace (e.g. governance:) ships a list of required reviewers, where pack + tenant should both contribute, not override.

The V1 spec records the gap so future-Claude or future-sister doesn’t have to re-discover it.

A sibling per-tenant table, mirroring analytical_defaults:

CREATE TABLE "_<tenant>".config_resolved (
key VARCHAR PRIMARY KEY, -- dot-path, e.g. 'display.grouping.patterns'
value VARCHAR, -- JSON-encoded scalar/list/map
level VARCHAR, -- 'engine' | 'pack' | 'tenant'
source_file VARCHAR, -- relative path; NULL when level='engine'
resolved_at TIMESTAMP
)

One row per leaf path in the merged tree. For the worked example, five rows:

keyvaluelevelsource_file
display.grouping.patterns"_type|_group|…"packconfig.yml
display.pagination.page_inputtrueengineNULL
display.pagination.begin_endtruepackconfig.yml
display.pagination.align"left"tenantconfig.yml
display.pagination (parent)not emittedn/an/a

Only leaf keys get rows. Parent maps are reconstructable from the flat key paths if JinDesk ever wants to render a tree view.

PhaseSurfaceBehavior
Bake time (jin make)bake_metadata in baking.pyReads engine fallbacks (from a new jinflow.config.DISPLAY_DEFAULTS map), pack config.yml (located via afs.pack_root() or the Sense 36 birth-manifest lookup; see open question below), tenant config.yml. Deep-merges. Writes the merged YAML to _<tenant>.config.config_yaml (existing table — no schema change). Writes per-key provenance to _<tenant>.config_resolved (new table).
Runtime (JinDesk)configEngine.ts loadConfig()Unchanged. Continues to read _<tenant>.config.config_yaml as one blob.
Runtime (JinDesk, optional, future)loadConfigProvenance(tenant, key)New helper that reads _<tenant>.config_resolved. UI surfaces “who provided what” when a viewer asks “why is this column hidden?” Not part of the V1 cut — ship the merge first.

JinDesk change for V1 is zero. The fix is invisible to runtime code except that values that used to be missing (vai grouping) now appear, and tenants who delete their copy-pasted pack defaults still get them at bake time.

These keys ship with engine-level L0 defaults. A pack omitting them still gets a working baseline; a tenant adopting a pack that doesn’t ship them (hrcentral.vai today) gets the engine value instead of nothing.

KeyEngine defaultRationale
display.grouping.patterns"_type|_status|_year"A bare minimum that catches the common axis columns. Packs override with their domain set. Closes the vai gap.
display.pagination.page_inputtruePage-number input is universally useful; no reason to hide it by default.
display.pagination.align"right"Convention.
display.sort.nulls"standard"SQL standard: ASC → NULLS LAST, DESC → NULLS FIRST. Matches current pack behaviour.
display.columns.locale_filtertrueLocale columns hidden by default; packs can opt out.
display.table.sticky_headertrueUniversal JinDesk convention.
display.table.max_height"auto"Universal JinDesk convention.
display.search.case_sensitivefalseILIKE default (matches numetrix pack).
display.search.default_mode"startswith"Matches numetrix pack.

Keys without engine defaults (purely pack/tenant concerns):

  • display.paging (the numeric breakdown is pack-specific)
  • display.recents.* (templates depend on pack entity vocabulary)
  • display.column_actions.* (entity-specific routing)
  • display.layout.* (per-pack visual identity)

Existing tenants with copy-pasted pack values: those values land at L2 (tenant) and override the same key at L1 (pack). The merged result is identical to today’s. Nothing breaks.

After the fix lands, a tenant author can delete the duplicated section from their config.yml. On the next jin make, the same values flow in from L1 (pack). The KLS is byte-identical.

For vai: the engine display.grouping.patterns default lands at L0 immediately. The first make after the fix produces a KLS with a working grouping config. No manual tenant edit required.

FileChange
jinflow/config.pyAdd DISPLAY_DEFAULTS dict (the engine catalog above). Add resolve_display(*, pack_config_path, tenant_config_path) -> tuple[dict, list[Resolved]] returning the merged YAML + per-key provenance. Reuses the Resolved dataclass.
jinflow/cli/commands/baking.pybake_metadata calls resolve_display, writes merged YAML to _<tenant>.config.config_yaml (existing path), provenance to _<tenant>.config_resolved (new table). The current line that reads only afs_root / "config.yml" is replaced.
tests/test_config_resolver.pyAdd resolve_display tests: pack-only, tenant-only, both, conflict (tenant wins), list replacement, engine fallback for missing key, vai-grouping case (pack and tenant both missing → engine wins).
tests/test_config_resolver.pyAdd bake table tests for _<tenant>.config_resolved mirroring the existing analytical_defaults tests.
docs/design/config_engine.mdUpdate the implementation reference to mention the merger + provenance table.
tests/packs/testpack/Add a config.yml at the pack root with a display: section the tests can exercise. Currently testpack has no config.yml.

No JinDesk changes for V1. The provenance read helper is a future-3.b-or-later concern.

Where does the bake step find the pack’s config.yml?

Two paths:

  1. Via afs.pack_root() — already used by bake_metadata for the brand-cascade case (baking.py:981). Returns the path to the pack repo on disk. Works for live-tenant builds where the packhub repos sit at ~/jinflow-packhub/jinflow-pack-<name>/.
  2. Via the Sense 36 birth manifest (.pack-init.yml) — already in the AFS. Records pack_origin, but resolving the path requires either consulting ~/jinflow-packhub/ by convention or shipping the pack snapshot inside the AFS at init time.

Path 1 works today and matches the existing brand-cascade fallback. Path 2 is more durable (the AFS becomes truly self-sufficient, no pack repo dependency at make time) but requires a small Sense 36 extension. Recommendation: Path 1 for V1; revisit when Sense 36 Phase 2 lands.

This V1 cascades along one axis — layer — and resolves at one time — bake. Both choices are intentional simplifications. The longer trajectory layers in dimensions and timings the design needs to accommodate without preempting:

  • Identity-axis overrides. A specific user Z may resolve a key to a different value than users A-Y do, even with the same pack/tenant cascade behind them. A named identity (an authenticated principal) carries an overlay that fires on top of the layered resolution.
  • Role-axis overrides. A role (e.g. auditor, analyst, board-member) carries an overlay distinct from any one user’s preferences. A user’s effective resolution composes their role(s) ⊕ their personal identity ⊕ the L0-L2 cascade.
  • Named-ruleset overlays. Bundled overrides activated by name (“audit mode,” “Q4-close mode,” “compliance review”). Orthogonal to identity — anyone running with the ruleset gets its values.
  • Time-scoped rulesets. Overlays that fire only during certain windows (feature flags scoped to date ranges).

These dimensions are orthogonal to layer, not subordinate. They are not L4 instant overrides either — they’re durable, attributable resolutions that depend on who is asking rather than what room the URL points at.

Implications for V1 (constraints, not work):

  • The Resolved.level field is Literal['engine', 'pack', 'tenant'] for now. The implementation does not assume that vocabulary will never grow — extending to 'role', 'identity', 'user', 'ruleset' should require new entries in the enum, not a refactor of the resolver shape.
  • The bake table _<tenant>.config_resolved records “the resolution at make time for an anonymous, no-identity, no-ruleset reader.” Future tables may sit alongside (config_resolved_by_role, config_resolved_by_principal) or the same table may grow a resolution-context key — both options stay open.
  • JinDesk’s loadConfig() reads the merged blob today. It must be able to layer an identity-scoped or ruleset-scoped overlay later without the layered result needing to be recomputed from primary sources at request time. The bake stays the layer-axis truth; identity/ruleset overlays apply on top.
  • The merger function does not bake assumptions about resolution order beyond layer. When identity overlays arrive, the resolver will compose them in a specific order; the layer cascade stays the same shape, just one stage of a longer pipeline.

What V1 explicitly does NOT do:

  • Implement any identity-, role-, or ruleset-axis resolution.
  • Plumb identity context through JinDesk request path.
  • Define the storage shape for per-identity overlays.
  • Pre-commit to an order of composition (layer first, then role, then identity? or some other arrangement?).

These are decisions for the moment a real driver arrives — likely Sense 25 (The Pass), when identity becomes first-class. The current spec leaves them open.

See [[project_sense_43_resolution_strategies]] in memory for the fuller architectural reflection that motivated this outlook.

  • L3 (entity-level overrides) for display config. No driver yet.
  • Per-key aggregation policy (union, append, concat). V1 uses uniform replace; the gap is acknowledged in the Aggregation semantics section above.
  • The runtime provenance helper (loadConfigProvenance). Bake emits the data; JinDesk consumes it in a follow-up.
  • Pack-level analytical_defaults (the cascade resolver already ignores pack config; same gap exists, same fix shape, but separate implementation — handle when forced).

Split by namespace, following the L4 contract above:

  • 3.a ✅ shipped 2026-05-29 — Display L4 (everywhere). ?display.<key>=<value> applies on Board, Lab, and Studio. The posture chip renders wherever an override is active. No banner — display overrides don’t violate any authored-truth claim. Pilot key: ?display.precision=N overlays a synthetic catch-all precision rule before the baked patterns. Analytical (?ad.*) keys are parsed and surfaced on the chip but currently no-op at runtime (Phase 3.b). Files: configWithOverrides.ts, PostureChip.svelte, layout wire-through, 11 unit tests + a Playworks smoke spec.
  • 3.b — Analytical L4 (Lab-only with Board fallback). Deferred until either a real driver appears or runtime SQL re-emit arrives (likely with Sense 18). The current arc bakes analytical defaults into compiled SQL at make time, so a URL param can’t change the result without re-materialising. When 3.b lands: ?ad.<key>=<value> honoured in Lab; Board/Studio show the “Open in Lab” banner.
  • Sense 18 narrates resolved cascade in Validation panels.
  • Pending — blocks on Sense 18.
  • Snapshot comparison surfaces cascade diffs alongside data diffs — “this snapshot was built with anchor=transaction_date; that one with reporting_date.”

The Cascade is the layered configuration mechanism. Each authority — engine, pack, tenant, perspective, transient Lab override — gets its own layer, and any property resolves through the cascade L0 → L1 → L2 → L3 → L4. The same mechanism serves two namespaces: analytical_defaults (what changes a number) and display (what changes how a number is shown). The Configuration Engine is the runtime resolver.

  • Layers: L0 engine fallback → L1 pack config.yml → L2 tenant config.yml → L3 perspective override → L4 transient Lab override
  • Namespaces: analytical_defaults (per Sense 14.2 ground rules) · display (paging, columns, table behaviour, format, recents, dates, column actions, layout)

Status: in bloom. Both namespaces are live and resolved on every page load; deeper override surfaces (perspective L3, transient L4) are forming.


  • Configuration Engine reference — the technical companion. This Sense holds principle; that doc holds resolver algorithm, file structure, baking, CLI, and open questions.
  • Sense 14.2 — Typed Signals — Phase 3 currency.anchor is the pilot analytical key. Phase 5 temporal inheritance is the next migration candidate.
  • Sense 13 — The Canvas — Board / Lab split maps onto L3 (authored) vs L4 (transient) authority. Board reads authored cascade only; Lab admits L4 overrides.
  • Sense 18 — The Ledger — will narrate cascade resolution as part of every Validation panel.
  • Sense 25 — The Pass — a future candidate for identity: as its own namespace.
  • Sense 36 — The Lineage — the birth manifest records initial L1 declarations; pack upgrades reconcile cascade declarations the same way they reconcile any other artifact.
  • The Four Convictions — Declarative: behavior comes from data, not code. The Cascade is the largest single move in that direction.

Configuration is layered authority — one mechanism, many namespaces, resolved at the last possible moment, with provenance preserved in the artifact it produces.


Numerical neighbors:Sense 42: The Landscape · Sense 44: The Forge

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